커널 호출 체인 합치기
커널 함수 다섯 개와, typeshade/fuse의 fuse() 안에서 이를 부르는 호스트입니다. add, mul, relu 호출은 저마다 바로 앞 호출이 같은 인덱스에 쓴 값을 읽습니다. 그래서 WebGPU에서는 스코프가 이 호출들을 디스패치 하나로 실행하고, 리포트에도 그렇게 적힙니다. pairsum은 t[i ^ 1]을 읽는데, 이 원소는 다른 인보케이션이 씁니다. 그래서 이 호출을 가운데 둔 스코프는 디스패치 두 번으로 실행되고, 리포트가 그 줄을 짚어 줍니다. 리덕션 호출로 끝나는 스코프도 있습니다. total로 끝나는 스코프는 add와 mul을 그 루프의 프롤로그로 실행합니다. 그래서 세 호출이 리덕션과 함께 디스패치 하나가 됩니다.
실행 tier WebGPUCPU tier
이 목록은 컴파일러가 확인합니다. tier(호출이 실행되는 백엔드)는 호스트의 실행 전체가 그 tier 하나만으로 기준값을 낼 때만 목록에 오릅니다. 컴파일러의 저니 게이트가 호스트마다 WebGPU와 WebGL2에서 실행하고, 단위 테스트가 CPU tier에서 실행합니다.
"use typeshade";
/* @example{ "title": "Host: fuse a chain of kernel calls", "blurb": "Five kernel functions and a host that calls them inside `fuse()` from `typeshade/fuse`. The calls `add`, `mul` and `relu` each read what the one before wrote at the same index, so on WebGPU the scope runs them as one dispatch, and the report says so. `pairsum` reads `t[i ^ 1]`, an element another invocation writes, so a scope with it in the middle runs two dispatches, and the report names the line. `total` reduces: a scope that ends with it runs `add` and `mul` as a prologue of its loop, in one dispatch with the reduction (change 0058).", "renderable": false, "reason": "no entry point"}*/
// Five operations of a chain, each a kernel function: a loop over arrays with no size. Each// iteration of add, mul and relu reads and writes only element i, so a chain of them can run as// one loop, one invocation per element.
export function add(out: array<f32>, a: array<f32>, b: array<f32>): void { for (let i: u32 = 0; i < out.length; i++) { out[i] = a[i] + b[i]; }}
export function mul(out: array<f32>, a: array<f32>, b: array<f32>): void { for (let i: u32 = 0; i < out.length; i++) { out[i] = a[i] * b[i]; }}
export function relu(out: array<f32>, x: array<f32>): void { for (let i: u32 = 0; i < out.length; i++) { out[i] = max(x[i], 0.); }}
// pairsum reads an element and its pair. Another invocation writes the pair, so pairsum cannot// share a dispatch with the call that wrote `t`.export function pairsum(out: array<f32>, t: array<f32>): void { for (let i: u32 = 0; i < out.length; i++) { out[i] = t[i] + t[i ^ 1]; }}
// total reduces: it adds every element of `xs` into `s`. A reduction can be the last call of a// fused group. The calls before it run as a prologue of each of its iterations, so `add`, `mul`// and `total` are one loop dispatch, and one more dispatch folds each level of partial sums.export function total(xs: array<f32>): f32 { let s = 0.; for (let i: u32 = 0; i < xs.length; i++) { s += xs[i]; } return s;}// Run a chain of kernel calls as one plan (change 0058).//// Inside `fuse()`, a kernel call is recorded, not run. When the body returns, the scope runs the// recorded calls together: on WebGPU, calls that each read only what the call before wrote at// the same index become one dispatch, in one submission. A call that reduces can be the last// call of such a group, and the calls before it run as a prologue of each of its iterations. The// promise resolves to a report of what ran. The module must be built with// `typeshade({ fuse: true })` in `vite.config.ts`; without it, every call runs on its own, and// the report says why.import { resident } from 'typeshade/runtime';import { fuse, type FuseReport } from 'typeshade/fuse';import { add, mul, pairsum, relu, total } from './loop-fuse.shade.ts';
/** One scope's result, read back, and its report. */export interface Fused { readonly values: Float32Array; readonly report: FuseReport;}
/** The sum that the last call of a scope returns, and the report of the scope. */export interface Summed { readonly sum: number; readonly report: FuseReport;}
/** * `relu((a + b) * c)` in one scope, `relu(pairsum((a + b) * c))` in another, and the sum of * `(a + b) * c` in a third. */export async function run( a: Float32Array, b: Float32Array, c: Float32Array,): Promise<{ chain: Fused; split: Fused; reduce: Summed }> { const n = a.length; // The calls in a scope take `Resident`s: the inputs, and `out`, which the host reads. const [ra, rb, rc] = [a, b, c].map((xs) => resident(xs)); const out = resident(new Float32Array(n));
// add, mul and relu: one dispatch on WebGPU. `t` and `u` are scratch arrays the host never // reads, so the fused entry holds them as locals, and no buffer is made for them. const chainReport = await fuse((scope) => { const t = scope.scratch(n); const u = scope.scratch(n); add(t, ra, rb); mul(u, t, rc); relu(out, u); }); const chain = { values: await out.read(), report: chainReport };
// pairsum reads u[i ^ 1], which another invocation of mul writes: two dispatches, and the // second group's `why` names the line. `u` is shared by both, so it becomes a buffer. const splitReport = await fuse((scope) => { const [t, u, v] = [scope.scratch(n), scope.scratch(n), scope.scratch(n)]; add(t, ra, rb); mul(u, t, rc); pairsum(v, u); relu(out, v); }); const split = { values: await out.read(), report: splitReport };
// add, mul and total: the reduction ends the group, and add and mul run as a prologue of each // of its iterations, so `t` and `u` are locals again. The call returns a promise, which // resolves to the sum once the scope has run; the body cannot wait for it. let sum!: Promise<number>; const reduceReport = await fuse((scope) => { const t = scope.scratch(n); const u = scope.scratch(n); add(t, ra, rb); mul(u, t, rc); sum = total(u); }); const reduce = { sum: await sum, report: reduceReport };
for (const r of [ra, rb, rc, out]) r.destroy(); return { chain, split, reduce };}두 파일 모두 커밋 91c2fe31의 컴파일러에 있는 그대로이며, 예제 디렉터리에서 읽어 왔습니다.