CheetahSpec:使用WebGPU与WGSL在浏览器中实现GPU并行加密搜索
2026/9/11 23:34:06 网站建设 项目流程

第一次看到 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 内线程数。

内存方面主要接触四类:

  • var<storage, read_write>:可读写的 storage buffer,适合放候选集、结果集。
  • var<uniform>:统一的只读参数,适合放起始 nonce、步长、目标条件这类小数据。
  • var<workgroup>:workgroup 内共享内存,适合做块内归约或临时聚合。
  • atomic:原子变量,通常放在 storage buffer 里,用于多个线程同时尝试写命中结果时做同步。

这里有一个新手常见误区:把复杂逻辑都塞进同一个函数,然后在每一轮循环里访问很多 buffer。GPU 不是 CPU,它极度依赖数据布局的规律性。如果每个 invocation 访问的地址是跳跃的、非对齐的,带宽就会被浪费,耗时会被成倍拉长。

2.2 数据布局决定性能上限

对于 CheetahSpec 这种搜索类任务,最理想的数据布局是扁平的array<u32>array<u64>。把 nonce 起始值、step、目标条件、结果标志都拆成独立字段,比塞进一个大的嵌套结构更友好。原因很简单:GPU 访存模式喜欢连续、对齐、可预测的访问,不习惯临时从对象里取一个深层的字段。

实际写 shader 时,保存命中结果也别做得很复杂。常见做法是在 storage buffer 里放一个原子计数器和若干结果槽:

// 示例结构,用于理解流程,不代表 CheetahSpec 的真实实现 struct Params { start_nonce : u32, step : u32, target : u32, pad : u32, // uniform 布局需要对齐,常见做法是补一个字段 } struct Found { flag : atomic<u32>, nonce : u32, } @group(0) @binding(0) var<storage, read_write> params : Params; @group(0) @binding(1) var<storage, read_write> result : Found; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid : vec3<u32>) { 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-256,WGSL 里需要自己实现原始哈希逻辑,不能依赖任何内置哈希函数。

2.3 关键参数不是越大越好

性能参数里最优先关注的是@workgroup_size和 dispatch 数量。常见的起点是@workgroup_size(256),然后让 dispatch 次数覆盖整个搜索空间。为什么会这样?因为 GPU 调度以 workgroup 为基本单位,workgroup 太小会导致调度开销占比高,太大则可能超过硬件限制,或者因为共享资源占用过多而降低并发度。256 这个值在很多 GPU 上能较好地平衡占用率和调度效率,但它绝对不是万能值。

调整参数的顺序应该是:

  1. 先把算法跑正确,用一条样例验证输出;
  2. 再用device.queue.onSubmittedWorkDone()配合时间戳查询,记录 GPU 内核实际耗时;
  3. 最后逐步调整 workgroup 大小、dispatch 分块数,找到一个在当前 GPU 和浏览器下的相对稳定值。

如果一上来就想通过调参数实现“更多线程跑得更快”,大概率会碰到内存带宽上限或驱动超时,而不是理想中的线性加速。

3. 一个最小可跑的浏览器 GPU 求解流程

理解完模型,接下来最该做的是亲手把一个最小流程跑通。不需要一开始就写完整 SHA-256,先做一个能反映求解结构的骨架。

3.1 先做环境检查

WebGPU 的浏览器支持情况一直在变。跑代码前,先确认当前浏览器环境有没有暴露navigator.gpu

// 示例结构:WebGPU 支持检查 if (!navigator.gpu) { // 当前浏览器不支持 WebGPU,需要走降级路径 throw new Error('WebGPU not supported'); }

如果这一步没通过,后面所有requestAdapterrequestDevice都会失败。在实际项目中,更好的做法是准备一条降级路径,比如切换到 WASM 或纯 JS 的 CPU 实现。不要因为环境不支持就让整个页面不可用。

3.2 核心流程分五段

浏览器跑 GPU 求解的思路并不复杂,整体可以拆成五段:

  1. 创建 GPU device;
  2. 把所有输入数据和参数写入 GPU buffer;
  3. 创建 compute pipeline 和 bind group;
  4. dispatch 计算,让 GPU 并行扫描候选空间;
  5. 把结果 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_READCOPY_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 字节对齐补字段,或者用var<storage>放参数,避免因为 uniform 布局规则不同导致行为不一致。

第五个坑是只测一次性能。GPU 第一次调度要经历着色器编译、管线预热,和后面的稳定状态差异很大。衡量性能至少要取多次运行的中位数或平均值,而且每次运行之间要有足够间隔,避免连续 dispatch 互相干扰。

第六个坑是忽略浏览器版本差异。WebGPU API 还在演进,不同浏览器对layout: 'auto'dispatchWorkgroups命名、buffer 对齐要求可能存在差异。长期维护时,最好把navigator.gpu检测和 API 适配封装在一个模块里,而不是在业务代码里到处散落版本判断。

4.2 一个典型的排查链路

如果结果不对或者速度异常,按下面顺序排会比瞎调参数高效得多:

  1. 看现象:完全无输出,还是输出错误结果,还是只是慢;
  2. 看输入:nonce 起点、step、buffer 里的字节序、Uint32Array 的数值范围;
  3. 看环境:浏览器是否支持 WebGPU,GPU 驱动是否掉线,是否有其他标签页抢占 GPU;
  4. 看时序:有没有等提交完成,有没有在 mapAsync 完成前读数据;
  5. 看内核:workgroup 数量和 dispatch 数量是否匹配搜索空间,atomic 计数是否被多个线程同时更新;
  6. 最后看算法:约束判断本身在 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 的页面,从最小骨架开始,跑通一次小范围搜索,再慢慢往里面加真正的哈希逻辑。先跑通,再优化,最后工程化——这条路径,在任何算力密集型技术面前都成立。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询