一个围绕 Mandelbrot 集合构建的异构并行计算与连续渲染实验。项目把同一个逐像素递推任务映射到单线程 CPU、OpenMP、多 Web Worker、OpenCL、CUDA 和 WebGPU,用可视化结果与可复现的原生 kernel benchmark 观察不同执行模型的吞吐、精度和调度特征。
在线体验:https://so-cean.github.io/MandelbrotSet-OpenCL/
Mandelbrot 集合由复数递推定义:
[ z_{n+1}=z_n^2+c,\qquad z_0=0 ]
图像中的每个像素对应一个不同的参数 c。像素之间没有数据依赖,单个像素内部则存在连续迭代,因此它是一个很适合研究并行计算的 workload:
- 可以直接把二维图像映射为线程网格;
- 计算以乘加和比较为主,内存流量较低;
- 不同像素逃逸时间不同,会产生真实的 SIMD/SIMT 分支发散;
- 分辨率和迭代上限可以连续调节工作量;
- 计算结果能够立即可视化,容易发现坐标、精度或并行实现错误。
项目最初比较 C++ 单线程、OpenMP 和 OpenCL,随后加入 CUDA 原生实现与 WebGPU 连续渲染。Web 版本让访问者无需安装程序,直接在自己的 CPU/GPU 上观察实时结果;原生 benchmark 则隔离浏览器刷新、颜色映射和显示开销,测量 accelerator kernel 本身。
flowchart LR
W["Shared Mandelbrot workload"] --> C["CPU execution"]
W --> G["GPU execution"]
C --> C1["C++ / OpenMP"]
C --> C2["Web Workers"]
G --> G1["CUDA"]
G --> G2["OpenCL"]
G --> G3["WebGPU / WGSL"]
C1 --> V["Correctness + benchmark"]
C2 --> V
G1 --> V
G2 --> V
G3 --> V
- 原生 C++ 单线程实现作为基础参考;
- OpenMP 按二维像素空间并行;
- Web 端使用独立 Worker 池,按行分片并行计算;
- CPU 与 GPU 各自维护连续循环,较慢的一方不会阻塞另一方。
独立 CUDA benchmark 使用二维 grid/block,每个 CUDA thread 负责一个像素:
const int x = blockIdx.x * blockDim.x + threadIdx.x;
const int y = blockIdx.y * blockDim.y + threadIdx.y;实现包含完整的设备选择、显存分配、二维 kernel launch、预热、CUDA Event 设备计时、错误检查和最终结果校验。输出按 row-major 顺序连续写入;每帧只提交一个融合 kernel,不在迭代循环中往返主机。
当前 standalone benchmark 位于 benchmarks/native/cuda_benchmark.cu。项目还保留了早期 CUDA/OpenGL 版本,并关联独立仓库 Dylan8527/Mandelbrot-set-GPU。
OpenCL 路径采用对应的二维 NDRange:一个 work-item 计算一个像素。独立 benchmark 通过系统 OpenCL.dll 完成平台/设备枚举、运行时编译、command queue 提交和 event profiling,不依赖 Python OpenCL 第三方包。
测试程序会优先选择 NVIDIA GPU,并打印实际 platform、device、driver 与 OpenCL 版本,避免把核显结果误认为独显结果。实现见 benchmarks/native/opencl_benchmark.py。
Web 端使用 WebGPU + WGSL,在浏览器选择的真实 GPU 上持续渲染。GPU 路径使用 fragment shader 将像素计算直接写入 presentation canvas,避免完整中间纹理的额外复制;CPU 路径使用多个 Web Worker。两者共享 viewport、迭代上限、配色与时间驱动的 zoom 轨迹,但独立统计完成帧率。
浏览器无法像 CUDA 一样枚举并指定物理 GPU,因此页面会显示实际 adapter。Windows 多 GPU 机器应确认页面选中了目标独显。
直接 FP32 在像素间距小于中心坐标附近的 ULP 后会失去相邻像素差异。项目因此把普通渲染和深度路径分开:
- 普通尺度直接使用 FP32,保持最高吞吐;
- CPU 以任意精度整数生成一条 reference orbit;
- GPU 对各像素计算相对 reference 的扰动递推;
- 很小的扰动使用“FP32 尾数 + 独立二进制指数”表达,避免 scale 先在普通 FP32 中下溢。
扰动关系为:
[ \delta_{n+1}=2Z_n\delta_n+\delta_n^2+\Delta c ]
这样高精度成本集中在一条参考轨道,大量像素仍由 GPU 的 FP32 单元并行处理,而不是让每个像素执行昂贵的 FP64 或多 limb 大数算术。深度路径、viewport 和 CPU/GPU 对齐测试位于 docs/js/precision/ 与 docs/tests/。
- 连续归一化逃逸值降低整数 iteration 色带;
- 多套循环调色板由 CPU/GPU 共用同一 LUT;
- GPU 使用纹理过滤完成平滑采样;
- 分辨率直接改变 canvas backing size,支持最高 4K;
- zoom 由实际经过时间决定,不依赖某个 backend 的帧数;
Resource budget通过工作/空闲占空比限制持续提交频率,它是 workload budget,不是操作系统 GPU 利用率保证值。
CPU benchmark 使用持久线程池,不把线程创建计入每帧时间;每个 worker 从原子队列领取 4 行 tile,以平衡 Mandelbrot 不同区域逃逸次数不均造成的负载差异。测试运行在 Intel Core i7-14700(20 核、28 逻辑线程),使用 MSVC /O2 /arch:AVX2 /fp:precise,固定 4K、FP32、256 maximum iterations。
| Threads | Median frame | P95 frame | FPS | Speedup | Efficiency |
|---|---|---|---|---|---|
| 1 | 392.093 ms | 519.893 ms | 2.55 | 1.00× | 100.0% |
| 2 | 224.990 ms | 266.060 ms | 4.45 | 1.74× | 87.1% |
| 4 | 110.857 ms | 130.084 ms | 9.02 | 3.54× | 88.4% |
| 8 | 65.349 ms | 68.744 ms | 15.30 | 6.00× | 75.0% |
| 16 | 36.891 ms | 38.539 ms | 27.11 | 10.63× | 66.4% |
| 20 | 30.404 ms | 32.702 ms | 32.89 | 12.90× | 64.5% |
| 28 | 25.864 ms | 27.953 ms | 38.66 | 15.16× | 54.1% |
1→28 threads 将吞吐从 2.55 FPS 提升到 38.66 FPS,获得 15.16× speedup。20→28 threads 仍有收益,但效率下降反映了混合 P/E 核、SMT、调度和有限并行效率。所有线程配置产生相同 checksum,说明动态 tile 组装是确定性的。原始输出见 cpu-scaling-i7-14700-2026-08-06.md。
以下结果不是网页 FPS。它们来自独立 CUDA/OpenCL 程序,只统计 Mandelbrot kernel 的设备执行时间;初始化、kernel 编译、显存分配和最终校验回读均不计入。Kernel FPS 是 1000 / median kernel ms 得到的吞吐等价值,不受显示器刷新率约束,也不等同于端到端渲染 FPS。
- GPU:NVIDIA GeForce RTX 4060 Ti,Compute Capability 8.9;
- Driver:610.88;
- CUDA:13.0 / NVCC 13.0.88,目标
sm_89; - OpenCL:OpenCL 3.0 CUDA;
- 数据类型:FP32;
- viewport:
center = 0 + 1i,scale = 0.25; - maximum iterations:256;
- block/workgroup:16×16;
- 10 帧预热,100 帧采样;
- 三档分辨率保持 16:9,确保视域与平均迭代量可比。
| Backend | Resolution | Median kernel | P95 kernel | Kernel FPS | Giter/s |
|---|---|---|---|---|---|
| CUDA | 1280×720 | 0.050 ms | 0.051 ms | 20,000 | 436.7 |
| OpenCL | 1280×720 | 0.0437 ms | 0.0451 ms | 22,894 | 500.5 |
| CUDA | 1920×1080 | 0.094 ms | 0.095 ms | 10,638 | 518.4 |
| OpenCL | 1920×1080 | 0.0819 ms | 0.0829 ms | 12,207 | 596.5 |
| CUDA | 3840×2160 | 0.339 ms | 0.352 ms | 2,950 | 571.5 |
| OpenCL | 3840×2160 | 0.2877 ms | 0.3000 ms | 3,475 | 675.1 |
这组结果支持两个观察:
- 分辨率提高后 GPU 并行度更充分,迭代吞吐从约 437–501 Giter/s 上升到约 572–675 Giter/s;
- 该 kernel 没有共享内存 tiling、warp primitive 或 CUDA 专用数据复用,CUDA 与 OpenCL 处于同一性能量级。当前 OpenCL 结果快约 14%–18%,说明 API 名称本身并不保证性能,编译器生成代码和具体 kernel 结构更重要。
CUDA/OpenCL 的总 iteration checksum 差异低于 0.006%。两者都使用 FP32,但编译器可能采用不同的 FMA contraction 和浮点指令选择,因此边界附近少量像素的逃逸次数可能不同。程序会输出 checksum 和平均迭代数,便于回归检查。
完整计时口径、环境安装和复现命令见 benchmarks/native/README.md,本次原始输出保存在 benchmarks/native/results/rtx4060ti-2026-08-06.md。
对 4K CUDA launch 使用 Nsight Compute 2025.4.1 采集硬件计数器:
| Metric | Value | 结论 |
|---|---|---|
| Compute (SM) throughput | 93.08% | kernel 活跃期间已充分使用 SM,属于 compute-bound |
| DRAM throughput | 17.66% | 不是显存带宽瓶颈 |
| Achieved occupancy | 83.10% | 理论值 100%;主要损失来自 warp/workload imbalance |
| Branch efficiency | 98.75% | early-exit 分支不是当前 viewport 的主要瓶颈 |
| Active threads / warp | 29.19 / 32 | 大部分 lane 在迭代期间保持活跃 |
| Eligible warps / scheduler | 5.49 | 有足够 warps 隐藏执行延迟 |
| Issued warps / scheduler | 0.94 | 接近每 scheduler cycle 发射一个 warp |
| Registers / thread | 20 | register pressure 不高 |
这个结果解释了为什么 Windows 任务管理器可能显示较低或波动的 GPU 百分比:被测 kernel 只有约 0.3–0.4 ms,系统图表会按时间窗口平均,而且可能默认显示 3D engine;在 kernel 真正执行的时间段内,Nsight 测得 SM throughput 为 93.08%。
Nsight 同时发现 11.22M fused 与 43.42M non-fused FP32 instructions,并给出约 19.85% 的局部 FMA 优化机会。该数字是 profiler estimate,不是保证值;显式 FMA 会改变边界附近的 FP32 舍入,因此后续优化必须同时报告性能和像素/checksum 差异。完整分析见 nsight-analysis-rtx4060ti-2026-08-06.md,原始 metrics 位于 nsight-details.csv。
| 方向 | 项目中的实现 |
|---|---|
| Workload decomposition | 将二维像素域映射到 OpenMP iteration、Worker tile、OpenCL NDRange 和 CUDA grid/block |
| Kernel design | 每像素独立递推、单次融合 dispatch、连续 row-major 输出,并分析边界区域的控制流发散 |
| Performance measurement | 设备 event 计时、预热、median/P95、kernel/wall time 分离、CPU scaling、Mpixel/s 与实际 Giter/s |
| GPU profiling | Nsight Compute 的 SM/DRAM throughput、occupancy、warp scheduler、branch efficiency 与 instruction 分析 |
| Correctness | 确定性 workload、checksum、CPU/GPU 离屏逐像素比较、直接算法与 deep perturbation 交叉验证 |
| Numerical precision | FP32 精度极限检测、任意精度 reference orbit、尾数/指数分离和 GPU perturbation |
| Reproducibility | 固定 CUDA/driver/workload 元数据、Conda 工具链、自动构建脚本、原始 benchmark 记录和测试 |
| Deployment | 同一数学任务同时提供原生计算基线与 GitHub Pages WebGPU 实时演示 |
需要支持 WebGPU 的当前 Chrome 或 Edge:
npm test
npm run serve打开 http://localhost:8080/。Windows 上可用下面的命令启动独立 Chrome profile 并请求高性能 GPU:
npm run chrome:gpupython benchmarks/native/opencl_benchmark.py --frames 100.\benchmarks\native\build_cpu.ps1
.\benchmarks\native\cpu_scaling_benchmark.exe 30 5Windows 需要 CUDA Toolkit/NVCC 和 Visual Studio C++ Build Tools。仓库提供自动定位 Conda CUDA 环境与 MSVC 的构建脚本:
conda create -y -n mandelbrot-cuda -c nvidia cuda-nvcc=13.0.88 cuda-cudart-dev=13.0.96
.\benchmarks\native\build_cuda.ps1
.\benchmarks\native\cuda_benchmark.exe 100管理员 PowerShell 中可运行 Nsight Compute profile:
.\benchmarks\native\profile_cuda.ps1原始 C++/OpenMP/OpenCL/OpenGL 程序使用 CMake + vcpkg:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config ReleaseMandelbrotSet-OpenCL/
├── main.cpp / main_openmp.cpp / main_opencl.cpp
│ └── 原生 CPU、OpenMP 与 OpenCL 实现
├── dist/*.cu
│ └── CUDA/OpenGL 实验实现
├── benchmarks/native/
│ ├── cpu_scaling_benchmark.cpp
│ ├── cuda_benchmark.cu
│ ├── opencl_benchmark.py
│ ├── build_cpu.ps1 / build_cuda.ps1
│ └── profile_cuda.ps1 / results/
├── docs/
│ ├── js/ CPU/GPU backend 与 Worker 实现
│ ├── js/precision 深度缩放实现
│ ├── shaders WebGPU/WGSL shader
│ └── tests CPU/GPU、viewport 与精度测试
├── tools/run-chrome-high-performance.ps1
└── .github/workflows/pages.yml
- NVIDIA, CUDA Programming Guide — Programming Model
- NVIDIA, CUDA C++ Best Practices Guide — Performance Metrics
- NVIDIA, Nsight Compute Kernel Profiling Guide
- NVIDIA, GPU Performance Counter Permissions
- Khronos Group, OpenCL Specification
- Khronos Group,
clGetEventProfilingInfo - W3C GPU for the Web, WebGPU and WGSL
- K. I. Martin, Perturbation techniques applied to the Mandelbrot set
推送到 main 后,GitHub Actions 会运行测试并部署 docs/ 到 GitHub Pages。


