Mandelbrot 集合是一种神奇的分形结构,这种算法跟我们之前实现的 YUV 和 RGB 的转换有着本质的不同,我把它叫做均匀并行和非均匀并行,YUV 和 RGB 转换就是典型的均匀并行,在整个图像中,每个点的计算量是完全一样的,同样的算法步骤,最多有一些简单的分支,计算量没有很大的差别,
而 Mandelbrot 集合的计算则是完全不一样的典型场景,临近的两个点之间计算的次数差异非常大,如果距离稍微远一些,那计算量的差异就更恐怖了。
从数学上来说,利用逃逸次数作图,这种计算量的巨大差距正是 Mandelbrot 集合呈现出如此多变的细节的根本原因。
那么我们简单的 按照 dispatch 的 x y 坐标分配任务的 方式对于 Mandelbrot 这类非均匀并行的场景来说就很不适用了,对于小的计算量无所谓,对于大的计算量来说,就会出现次数很少的线程计算完毕了,还需要等待计算次数非常大的线程完成计算,这样 GPU 的算力利用率就会非常低。
对于这种类似的情况,我们需要 进行任务分配的机制。
spirv 的 workgroup 线程就是 i 针对这种场景而设置的
所谓的 workgroup 共享 数据就是 一个工作组内共享的独立数据,读写非常快,而且支持原子操作。
所以我们用下面的方式来实现 任务的分配
这是一个 任务获取的 workgroup 共享变量
pub fn main(params: Params, buf: Vec<u32>) {
let wrk = spirv_group(); //当前工作组的 id 这个数量由 unsafe { builder.dispatch([32, 32, 1]) }.unwrap(); 确定
let cx = wrk[0];
let cy = wrk[1];
let wrk_id = (cy * 32 + cx) as u32;
let cent_x = params.x + ((cx + 0.5) * 32 - 512) * params.step;
let cent_y = params.y + (512 - (cy + 0.5) * 32) * params.step;
let local_id = task_mgr.atomic_add();
while local_id < 1024u32 {
let local_x = local_id % 32;
let local_y = (local_id / 32) as u32;
let x = cent_x + local_x * params.step - 32 * params.step;
let y = cent_y + 32 * params.step - local_y * params.step;
let esc = escape(x, y, params.max_iter);
let pos = (cy * 32 + local_y) * 1024 + cx * 32 + local_x;
buf[pos] = esc;
local_id = task_mgr.atomic_add();
}
}
复制代码
我们用上面的代码来完成 Mandelbrot 集合的计算
注意 我们使用 task_mgr.atomic_add();获取一个任务 ID 最多 1024 个
这样 我们把 32x32 块的计算 分配在一个原子变量上
不管每个组 启动多少个线程,可以远小于 1024
这些线程就像勤劳的老黄牛一样 一直通过原子操作获取 任务 ID 一直到 超过 1024
这样对于小的任务区别不大,但是如果 特别繁重的 计算任务 GPU 线程可以跑到死!!
我们用下面的代码启动 Shader
println!("{:?}", b.import_module(&mut module, [32, 1, 1]));
let spirv_code = b.assemble();
let mut rt = spirv::Runtime::default();
let mut args = rt.get_args();
args.add_input(Params{x: -0.7454f64, y: 0.1103f64, step: 0.00000004f64, max_iter:12000});
let img = args.add_output_vec::<u32>(1024 * 1024);
rt.prepare(&spirv_code, args)?;
let start = std::time::Instant::now();
rt.run([32, 32, 1])?;
let elaspe = std::time::Instant::now() - start;
println!("use {}", elaspe.as_micros());
save_mandelbrot_png(&img.read()?, 1024, 1024, 12000, "mand.png")?;
复制代码
大家看到 我们 启动了 32 个线程每组,如果任务繁重,这就是更好的方案了
按照这个坐标计算出来的图像是这样的
完美!
评论