
第一次看到 CheetahSpec 这个项目名时我脑子里先冒出来的问题是一个基于 WGSL 的浏览器 cryptosolver凭什么用 native execution speed 当卖点这问题不是质疑性能而是想搞明白一件更本质的事——当我把加密相关的大规模搜索从本地程序挪到浏览器里跑的时候到底发生了什么。这个疑问在我刷 CTF 练习题的时候特别有体感。一道要求扫描 nonce 满足哈希约束的题目用 JavaScript 写循环跑几分钟都不出结果临时编译一个 Rust 小程序又觉得为了单道题去搭工具链太重。假如浏览器自己就能调度 GPU 去完成这种搜索那整条工作流会完全变样。CheetahSpec 这个方向恰恰就是要把这种能力做成一个现实可用的工具。1. 别被“原生速度”四个字骗了它真正换掉的是执行模型先说结论CheetahSpec 这类项目真正体现的不是某个库比某个库快多少而是把浏览器的执行模型从“CPU 上跑 JS/WASM”扩展成了“GPU 上跑计算着色器”。所谓 native execution speed准确理解应该是“以 GPU 硬件本来具备的执行效率运行”而不是在浏览器里把某个语言翻译得很接近汇编。1.1 从一个容易误读的词开始cryptosolver 这个英文词如果只看字面很容易想到“破解密码”或“破解哈希”。但在合规开发和算法研究场景里它通常指的是更小、更明确的一类搜索在一个已知算法和已知条件的前提下去扫描符合约束的输入。比如给定一个哈希前缀条件找一个 nonce 让哈希值满足要求或者在一个封闭题目里穷举有限的参数空间。这些任务有两个共同点验证条件可以精确写出候选空间可以模块化切分。CheetahSpec 如果从标题拆解重点并不在“能解什么题”而在“这个求解过程发生在哪一层”。它基于 WGSL 在浏览器里运行意味着计算逻辑不经过 JavaScript 的逐条解释也不依赖 WASM 的 CPU 单设备算力而是直接交给 GPU 的 compute shader。1.2 为什么过去浏览器很难做这件事以前的浏览器并行方案主力是 Web Worker 加 WASM。WASM 确实比手写 JS 的紧循环快不少但它仍然是 CPU 模型多线程数量受机器核心数制约而且每个线程能直接访问的内存结构也有限制。换个角度看WebGL 也能在 GPU 上算但 WebGL 本质上是图形 API要把通用计算伪装成像素着色器来回搬运纹理数据写起来非常别扭。WebGPU 把这层窗户纸捅破了。它提供了独立的 compute pipeline、storage buffer、uniform buffer 和原子操作。WGSL 作为 WebGPU 的着色器语言直接在 GPU 上执行。换句话说浏览器第一次有了一种“把一批数据送进 GPU让几千个并行线程同时做判断再拿回结果”的通用通道。1.3 这类项目真正的主判断所以我愿意把 CheetahSpec 代表的方向理解成一个可移植的浏览器并行搜索框架而不是一把万能钥匙。它适合的任务必须满足两个前提一是候选解之间彼此独立一个 nonce 算出来合不合法不影响另一个 nonce二是判断条件可以紧凑地表达成 shader 里的整数运算和比较。只要这两个前提满足浏览器就不再只是“展示页面的容器”而是一个能承担真实计算负载的 GPU 后端。2. WGSL 到底有多少并行能力取决于你怎么组织数据WGSL 是一种新的着色器语言语法上和 Rust、TypeScript 都有点神似但真正决定性能的不是语法而是它的执行模型和内存模型。很多人第一次接触时会按照写 C 或写 Python 的思路去设计函数结果发现 GPU 上并不是这么回事。2.1 先把几个关键概念对号入座WGSL 的计算程序会以三个尺度执行一个 dispatch 会启动若干个 workgroup每个 workgroup 里有固定数量的 invocation。对应到 WebGPU API就是你调用dispatchWorkgroups(x, y, z)时指定的 workgroup 数以及着色器里workgroup_size指定的每个 workgroup 内线程数。内存方面主要接触四类varstorage, read_write可读写的 storage buffer适合放候选集、结果集。varuniform统一的只读参数适合放起始 nonce、步长、目标条件这类小数据。varworkgroupworkgroup 内共享内存适合做块内归约或临时聚合。atomic原子变量通常放在 storage buffer 里用于多个线程同时尝试写命中结果时做同步。这里有一个新手常见误区把复杂逻辑都塞进同一个函数然后在每一轮循环里访问很多 buffer。GPU 不是 CPU它极度依赖数据布局的规律性。如果每个 invocation 访问的地址是跳跃的、非对齐的带宽就会被浪费耗时会被成倍拉长。2.2 数据布局决定性能上限对于 CheetahSpec 这种搜索类任务最理想的数据布局是扁平的arrayu32或arrayu64。把 nonce 起始值、step、目标条件、结果标志都拆成独立字段比塞进一个大的嵌套结构更友好。原因很简单GPU 访存模式喜欢连续、对齐、可预测的访问不习惯临时从对象里取一个深层的字段。实际写 shader 时保存命中结果也别做得很复杂。常见做法是在 storage buffer 里放一个原子计数器和若干结果槽// 示例结构用于理解流程不代表 CheetahSpec 的真实实现 struct Params { start_nonce : u32, step : u32, target : u32, pad : u32, // uniform 布局需要对齐常见做法是补一个字段 } struct Found { flag : atomicu32, nonce : u32, } group(0) binding(0) varstorage, read_write params : Params; group(0) binding(1) varstorage, read_write result : Found; compute workgroup_size(256) fn main(builtin(global_invocation_id) gid : vec3u32) { let nonce params.start_nonce gid.x * params.step; // 这里用一个可验证的整数条件代替完整哈希计算只演示并行搜索结构 if ((nonce * 2654435761u) % 100003u params.target) { if (atomicAdd(result.flag, 1u) 0u) { result.nonce nonce; } } }注意这里的写法只能算教学骨架。真实场景里如果多个命中同时发生你要么准备多个结果槽要么在 CPU 侧做二次排序如果条件本身就是完整的 SHA-256WGSL 里需要自己实现原始哈希逻辑不能依赖任何内置哈希函数。2.3 关键参数不是越大越好性能参数里最优先关注的是workgroup_size和 dispatch 数量。常见的起点是workgroup_size(256)然后让 dispatch 次数覆盖整个搜索空间。为什么会这样因为 GPU 调度以 workgroup 为基本单位workgroup 太小会导致调度开销占比高太大则可能超过硬件限制或者因为共享资源占用过多而降低并发度。256 这个值在很多 GPU 上能较好地平衡占用率和调度效率但它绝对不是万能值。调整参数的顺序应该是先把算法跑正确用一条样例验证输出再用device.queue.onSubmittedWorkDone()配合时间戳查询记录 GPU 内核实际耗时最后逐步调整 workgroup 大小、dispatch 分块数找到一个在当前 GPU 和浏览器下的相对稳定值。如果一上来就想通过调参数实现“更多线程跑得更快”大概率会碰到内存带宽上限或驱动超时而不是理想中的线性加速。3. 一个最小可跑的浏览器 GPU 求解流程理解完模型接下来最该做的是亲手把一个最小流程跑通。不需要一开始就写完整 SHA-256先做一个能反映求解结构的骨架。3.1 先做环境检查WebGPU 的浏览器支持情况一直在变。跑代码前先确认当前浏览器环境有没有暴露navigator.gpu// 示例结构WebGPU 支持检查 if (!navigator.gpu) { // 当前浏览器不支持 WebGPU需要走降级路径 throw new Error(WebGPU not supported); }如果这一步没通过后面所有requestAdapter、requestDevice都会失败。在实际项目中更好的做法是准备一条降级路径比如切换到 WASM 或纯 JS 的 CPU 实现。不要因为环境不支持就让整个页面不可用。3.2 核心流程分五段浏览器跑 GPU 求解的思路并不复杂整体可以拆成五段创建 GPU device把所有输入数据和参数写入 GPU buffer创建 compute pipeline 和 bind groupdispatch 计算让 GPU 并行扫描候选空间把结果 buffer 拷回到可映射的 staging buffer 里读取并校验。用代码骨架表达大致是这样// 示例结构标准 WebGPU API 的常见写法不同浏览器版本有差异 const adapter await navigator.gpu.requestAdapter(); const device await adapter.requestDevice(); const shaderModule device.createShaderModule({ code: wgslCode, // 上一节中的 WGSL 骨架 }); const pipeline device.createComputePipeline({ layout: auto, compute: { module: shaderModule, entryPoint: main }, }); // 参数 buffer起始 nonce、step、target const paramsBuffer device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([0, 1, 12345, 0])); // 结果 buffer原子 flag nonce const resultBuffer device.createBuffer({ size: 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); const bindGroup device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: paramsBuffer } }, { binding: 1, resource: { buffer: resultBuffer } }, ], }); const encoder device.createCommandEncoder(); const pass encoder.beginComputePass(); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(Math.ceil(SCAN_SPACE / 256)); pass.end(); device.queue.submit([encoder.finish()]); await device.queue.onSubmittedWorkDone();提交之后不能直接把resultBuffer拿去读。GPU buffer 默认不带MAP_READ而且直接映射正在被 GPU 使用的资源是错误做法。常规流程是先建一个 staging buffer带MAP_READ和COPY_DST权限再用copyBufferToBuffer把结果拷出来最后mapAsync读取。这里最容易漏掉的一点是mapAsync之后必须等待 promise 完成再访问getMappedRange()的 buffer 视图。3.3 小样本对拍是正确性的第一道保险跑流程时一定不要直接全量扫描。先用一个很小的搜索空间比如几百个 nonce同时用 CPU 参考实现跑一遍对比 GPU 和 CPU 是否找到同一个结果。这个步骤看起来笨但它能一次性过滤掉绝大部分问题WGSL 里的整数溢出、nonce 计算公式错误、buffer 绑定错位、结果读取时机不对。我通常会在着色器里先放一条调试分支把“当前 invocation 的编号”写到一个临时 buffer 里确认线程真正跑起来了再去做条件判断。这是快速区分“GPU 没执行”和“GPU 执行了但条件不满足”的最直接方法。4. 从单次跑通到稳定批量使用中间隔着六个坑单次跑通只说明流程能走通离“稳定服务”还有不小的距离。把一个浏览器 cryptosolver 变成能长期使用的工具会在六个地方反复踩坑。4.1 最容易踩的六个点第一个坑是忘了等待提交完成。device.queue.submit是异步的如果提交完立刻去读 staging buffer很可能读到旧数据。要在读之前先await device.queue.onSubmittedWorkDone()或者用mapAsync的状态流转来保证时序。第二个坑是内核执行时间过长。GPU 驱动普遍有看门狗机制单个命令无限循环或执行过久可能导致设备重置。正确的做法是把大搜索空间拆成多个 dispatch每个 dispatch 只处理一个分块中间可以穿插读取进度或做命中检查。第三个坑是 buffer 复用不及时。频繁创建新 buffer 不仅浪费内存也会累积 GC 压力。成熟做法是把常驻 buffer 池化每次计算前用writeBuffer覆盖输入计算后把结果拷走再重用这块内存。第四个坑是 WGSL 布局对齐。把三个u32直接塞进 uniform buffer在某些实现里会触发编译错误或读取错位。实际写结构体时建议按 16 字节对齐补字段或者用varstorage放参数避免因为 uniform 布局规则不同导致行为不一致。第五个坑是只测一次性能。GPU 第一次调度要经历着色器编译、管线预热和后面的稳定状态差异很大。衡量性能至少要取多次运行的中位数或平均值而且每次运行之间要有足够间隔避免连续 dispatch 互相干扰。第六个坑是忽略浏览器版本差异。WebGPU API 还在演进不同浏览器对layout: auto、dispatchWorkgroups命名、buffer 对齐要求可能存在差异。长期维护时最好把navigator.gpu检测和 API 适配封装在一个模块里而不是在业务代码里到处散落版本判断。4.2 一个典型的排查链路如果结果不对或者速度异常按下面顺序排会比瞎调参数高效得多看现象完全无输出还是输出错误结果还是只是慢看输入nonce 起点、step、buffer 里的字节序、Uint32Array 的数值范围看环境浏览器是否支持 WebGPUGPU 驱动是否掉线是否有其他标签页抢占 GPU看时序有没有等提交完成有没有在 mapAsync 完成前读数据看内核workgroup 数量和 dispatch 数量是否匹配搜索空间atomic 计数是否被多个线程同时更新最后看算法约束判断本身在 WGSL 里的整数运算是否符合预期有没有发生 32 位溢出或负数问题。这其实就是一个从“外部”到“内部”的定位过程。先把数据和时序排清再回到着色器逻辑上找原因能省掉很多无用功。5. 什么场景该用它什么场景不该碰没有哪个并行方案是万能的。CheetahSpec 这种浏览器 GPU cryptosolver 路线有明确的适用边界也有明确不适合的场景。提前想清楚边界比代码写得更漂亮更重要。5.1 适合的场景和前置条件适合的场景通常具备这些特征候选空间已知、判定条件可写成纯函数、结果只需要少量可枚举的值。比如场景类型典型例子为什么适合工作量证明类搜索新区块 nonce 搜索、HashCash 风格约束条件明确候选间无依赖典型 GPU 友好密码学题目 / CTF 练习给定前缀找 nonce、给定条件找碰撞搜索空间可控验证简单算法教学与实验模拟暴力搜索复杂度、测试哈希函数硬件表现不需要后端学生能直接看到并行效果参数空间扫描在已知算法里扫描有限参数组合每个参数组合独立适合大规模并行前置条件也很明确浏览器支持 WebGPU本机有独立或集成 GPU任务可以表示为扁平数组和标量参数数据不需要频繁和 CPU 之间来回搬运。5.2 不合适或需要谨慎的场景反过来下面几类场景就不太适合甚至根本不该碰超大未知密钥空间的暴力搜索。如果连明文、密钥结构、有效范围都不知道GPU 并行只是把不可能变成更快的不可行。对未授权系统或第三方用户数据的搜索。任何加密搜索都应该只作用于自己拥有、或明确获得授权测试的数据。这不是技术限制而是基本底线。合规路径下这类项目更适合用于 CTF 题目、私有测试数据和教学实验。延迟敏感的交互式请求。GPU 调度加 buffer 读写会有固定开销如果每次只判断一个候选可能比直接在 CPU 上用简单循环还慢。高频 CPU I/O 型任务。如果问题大部分时间花在磁盘读、网络请求、数据库查询上GPU 再快也救不了整体耗时。5.3 长期使用需要补的工程能力如果要把这类能力做成一个长期可用的服务至少还要补三块拼图一是降级策略WebGPU 不可用时能自动切回 CPU 实现二是状态监控记录每次计算的耗时、命中数、设备丢失情况三是权限边界所有来自页面的输入都要经过校验所有结果都要在 CPU 侧重新做一次验证不能盲信 GPU 回传值。这套思路不复杂但缺了任何一块工具都只停留在一个能演示的 demo 层面撑不起真实业务。6. 把一次 GPU 求解沉淀成可复用流程CheetahSpec 这个方向给我最大的启发不是某个具体算法写得妙而是它把一个看起来必须依赖本地工具的流程搬进了浏览器里的 GPU。顺着这个思路可以沉淀出一套通用方法用来处理未来任何“在浏览器里做大范围并行搜索”的任务。6.1 五步工程法我把它总结成五步每次接到类似任务都可以套用第一步定目标。明确搜索空间是什么nonce 或候选参数从多少开始、到哪里结束满足什么条件算命中命中的结果需要保留哪些字段。把这些写成一个可编辑的参数表不要藏在代码里。第二步划边界。把问题拆成 GPU 内和 CPU 内两部分。搜索、判断、初步聚合放 GPU任务调度、权限校验、结果二次验证、日志记录放 CPU。边界越清晰后端迁移和维护越容易。第三步做对拍。找一个小规模输入同时用参考实现和 GPU 实现跑对比结果字段。不要小看这一步它能拦住 80% 的正确性问题。第四步压规模。从小样本逐步扩大搜索范围观察耗时、命中数、设备稳定性。每次只改一个变量比如 dispatch 数量或 workgroup 大小不要同时调多个参数。第五步接流程。当 GPU 结果稳定后再封装 API加上降级策略、超时处理、日志埋点和错误提示。这一步做完它才从“能跑的脚本”变成“能交给别人的工具”。6.2 真正值得长期关注的不是速度本身浏览器里的 GPU 计算在过去几年已经慢慢从实验走向工程CheetahSpec 这类项目只是其中一个剖面。它值得关注的原因不是让某道题目跑得快了几秒而是让“浏览器能承担原生级并行计算”这件事变得不再稀奇。你不需要为一次小规模搜索去安装编译器、管理依赖、处理操作系统差异只需要打开一个页面GPU 就是你的算力池。但也要承认它的边界它不是本地原生程序的替代品也不是所有密码学任务的银弹。它的价值在于在适当的任务、适当的场景里给你多了一个选择层。如果你读完这篇之后想动手试试我的建议是不要急着写完整算法先开一个支持 WebGPU 的页面从最小骨架开始跑通一次小范围搜索再慢慢往里面加真正的哈希逻辑。先跑通再优化最后工程化——这条路径在任何算力密集型技术面前都成立。