Humuhumu33 commited on
Commit
a24d0c6
·
verified ·
1 Parent(s): 7406205

kernel limiter profiler: read-only vs ALU-intensity vs occupancy sweep

Browse files
Files changed (1) hide show
  1. kernel-profile.html +144 -0
kernel-profile.html ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1">
2
+ <title>Ternary GEMV limiter profile — your GPU</title>
3
+ <style>
4
+ :root{--bg:#0a0d13;--panel:#111825;--ink:#e8ebf1;--dim:#8b95a7;--ac:#7c5cff;--ok:#48c26c;--no:#f0616d;--warn:#e0a94a;--line:#1e2836}
5
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 -apple-system,Segoe UI,Roboto,monospace;padding:24px;max-width:860px;margin:0 auto}
6
+ h1{font-size:20px;margin:0 0 4px}.sub{color:var(--dim);font-size:13px;margin:0 0 18px}
7
+ .card{font-family:ui-monospace,monospace;padding:14px 16px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin-bottom:12px;word-break:break-word}
8
+ table{width:100%;border-collapse:collapse;font-family:ui-monospace,monospace;font-size:13px}
9
+ td,th{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left}th{color:var(--dim);font-weight:600}
10
+ td.n{text-align:right;font-variant-numeric:tabular-nums}
11
+ .verdict{font-size:16px;font-weight:600;margin-top:14px}.bar{display:inline-block;height:9px;background:var(--ac);border-radius:2px;vertical-align:middle}
12
+ </style></head><body>
13
+ <h1>Ternary GEMV <span style="color:var(--ac)">limiter profile</span> — your GPU</h1>
14
+ <p class="sub">Same GEMV access pattern, one knob at a time. <b>read-only</b> = the reads with zero math (memory ceiling at this pattern). <b>dot ×1</b> = the real kernel. <b>×2/×4</b> = same reads, more ALU. If read-only ≫ dot, the ALU is the wall (int8 is the lever); if read-only ≈ dot, the memory access pattern is the wall (int8 won't help).</p>
15
+ <div id="status" class="card">starting…</div>
16
+ <div id="out"></div>
17
+
18
+ <script type="module">
19
+ const $=s=>document.querySelector(s), st=$("#status"), out=$("#out");
20
+ const MODEL_GB=0.69;
21
+ const say=(t,cls)=>{ st.textContent=t; st.className="card"+(cls?" "+cls:""); };
22
+ window.addEventListener("unhandledrejection",e=>say("✗ unhandled: "+(e.reason&&(e.reason.message||e.reason)),"no"));
23
+
24
+ const DOT16=`
25
+ fn dot16(word:u32, v:u32) -> f32 {
26
+ var s4=vec4<f32>(0.0);
27
+ s4=s4+x[v] *(vec4<f32>(f32(word&3u),f32((word>>2u)&3u),f32((word>>4u)&3u),f32((word>>6u)&3u))-vec4<f32>(1.0));
28
+ s4=s4+x[v+1u]*(vec4<f32>(f32((word>>8u)&3u),f32((word>>10u)&3u),f32((word>>12u)&3u),f32((word>>14u)&3u))-vec4<f32>(1.0));
29
+ s4=s4+x[v+2u]*(vec4<f32>(f32((word>>16u)&3u),f32((word>>18u)&3u),f32((word>>20u)&3u),f32((word>>22u)&3u))-vec4<f32>(1.0));
30
+ s4=s4+x[v+3u]*(vec4<f32>(f32((word>>24u)&3u),f32((word>>26u)&3u),f32((word>>28u)&3u),f32((word>>30u)&3u))-vec4<f32>(1.0));
31
+ return s4.x+s4.y+s4.z+s4.w;
32
+ }`;
33
+ // mode: 'read' (xor raw, no unpack) | 'dot' (R× dot16, same reads). T threads/row, ROWS rows/wg (T*ROWS=256).
34
+ const kern=(mode,R,T,ROWS)=>`
35
+ @group(0) @binding(0) var<storage,read> x: array<vec4<f32>>;
36
+ @group(0) @binding(1) var<storage,read> qw: array<u32>;
37
+ @group(0) @binding(2) var<storage,read_write> o: array<f32>;
38
+ @group(0) @binding(3) var<uniform> P: vec4<u32>; // K, N
39
+ var<workgroup> red: array<f32, 256>;
40
+ ${mode==='dot'?DOT16:''}
41
+ @compute @workgroup_size(256)
42
+ fn main(@builtin(workgroup_id) wg:vec3<u32>, @builtin(local_invocation_id) lid:vec3<u32>){
43
+ let K=P.x; let nw=K>>4u;
44
+ let rr=lid.x/${T}u; let t=lid.x%${T}u;
45
+ let n0=(wg.y*65535u+wg.x)*${ROWS}u+rr; let n=min(n0, P.y-1u);
46
+ let rowW=n*nw;
47
+ ${mode==='read'?'var accu=0u;':'var acc=0.0;'}
48
+ var w=t;
49
+ loop{ if(w>=nw){break;}
50
+ let word=qw[rowW+w]; let v=w<<2u;
51
+ ${mode==='read'?'accu=accu^word;':Array.from({length:R},(_,r)=>`acc=acc+dot16(word^${r}u, v);`).join(' ')}
52
+ w=w+${T}u; }
53
+ ${mode==='read'?'let acc=f32(accu&1u);':''}
54
+ red[lid.x]=acc; workgroupBarrier();
55
+ var s=${T>>1}u; loop{ if(s==0u){break;} if(t<s){ red[rr*${T}u+t]=red[rr*${T}u+t]+red[rr*${T}u+t+s]; } workgroupBarrier(); s=s/2u; }
56
+ if(t==0u && n0<P.y){ o[n0]=red[rr*${T}u]; }
57
+ }`;
58
+
59
+ const FILL=`
60
+ @group(0) @binding(0) var<storage,read_write> d: array<u32>;
61
+ @group(0) @binding(1) var<uniform> P: vec4<u32>;
62
+ @compute @workgroup_size(256)
63
+ fn main(@builtin(global_invocation_id) gid:vec3<u32>){ let n=P.x; var i=gid.x; loop{ if(i>=n){break;} d[i]=(i*2654435761u+1u)^((i<<7u)*40503u); i=i+P.y; } }`;
64
+
65
+ const VARIANTS=[
66
+ {id:"read", name:"read-only (no ALU) — memory ceiling", mode:"read", R:0, T:64, ROWS:4, grp:"alu"},
67
+ {id:"dot1", name:"dot ×1 (the real kernel)", mode:"dot", R:1, T:64, ROWS:4, grp:"alu"},
68
+ {id:"dot2", name:"dot ×2 (2× ALU, same reads)", mode:"dot", R:2, T:64, ROWS:4, grp:"alu"},
69
+ {id:"dot4", name:"dot ×4 (4× ALU, same reads)", mode:"dot", R:4, T:64, ROWS:4, grp:"alu"},
70
+ {id:"occ32", name:"dot ×1 · 32 thr/row · 8 rows/wg", mode:"dot", R:1, T:32, ROWS:8, grp:"occ"},
71
+ {id:"occ128",name:"dot ×1 · 128 thr/row · 2 rows/wg", mode:"dot", R:1, T:128,ROWS:2, grp:"occ"},
72
+ ];
73
+
74
+ (async()=>{
75
+ try{
76
+ if(!navigator.gpu){ say("✗ No WebGPU. Open in Chrome/Edge.","no"); return; }
77
+ say("requesting adapter…");
78
+ let ad=await navigator.gpu.requestAdapter({powerPreference:"high-performance"}); if(!ad) ad=await navigator.gpu.requestAdapter();
79
+ if(!ad){ say("✗ no GPU adapter.","no"); return; }
80
+ const L=ad.limits, info=ad.info||{};
81
+ const dev=await ad.requestDevice({requiredLimits:{maxStorageBufferBindingSize:L.maxStorageBufferBindingSize, maxBufferSize:L.maxBufferSize, maxComputeWorkgroupsPerDimension:L.maxComputeWorkgroupsPerDimension}});
82
+ dev.lost.then(i=>say("✗ device lost: "+(i&&i.message||i.reason||""),"no"));
83
+
84
+ const K=4096, nw=K/16;
85
+ const wantBytes=Math.min(L.maxStorageBufferBindingSize, L.maxBufferSize, Math.round(MODEL_GB*1e9));
86
+ const N=Math.floor((wantBytes/4)/nw), qU32=N*nw, wBytes=qU32*4;
87
+ say("allocating "+(wBytes/1048576).toFixed(0)+" MB…");
88
+ const qw=dev.createBuffer({size:wBytes, usage:GPUBufferUsage.STORAGE});
89
+ const xbuf=dev.createBuffer({size:K*4, usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});
90
+ const obuf=dev.createBuffer({size:N*4, usage:GPUBufferUsage.STORAGE});
91
+ const P=dev.createBuffer({size:16, usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});
92
+ const xa=new Float32Array(K); for(let i=0;i<K;i++) xa[i]=(Math.sin(i*12.9898)*43758.5453)%1;
93
+ dev.queue.writeBuffer(xbuf,0,xa); dev.queue.writeBuffer(P,0,new Uint32Array([K,N,0,0]));
94
+
95
+ say("filling weights…");
96
+ { const fmod=dev.createShaderModule({code:FILL}); const fpipe=dev.createComputePipeline({layout:"auto",compute:{module:fmod,entryPoint:"main"}});
97
+ const fp=dev.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}); const wgF=Math.min(L.maxComputeWorkgroupsPerDimension,65535);
98
+ dev.queue.writeBuffer(fp,0,new Uint32Array([qU32, wgF*256, 0, 0]));
99
+ const fbg=dev.createBindGroup({layout:fpipe.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:qw}},{binding:1,resource:{buffer:fp}}]});
100
+ const e=dev.createCommandEncoder(); const p=e.beginComputePass(); p.setPipeline(fpipe); p.setBindGroup(0,fbg); p.dispatchWorkgroups(wgF); p.end();
101
+ dev.queue.submit([e.finish()]); await dev.queue.onSubmittedWorkDone(); }
102
+
103
+ const PASSES=10, ITERS=5;
104
+ const rows=[];
105
+ for(const V of VARIANTS){
106
+ say("compiling "+V.id+"…");
107
+ dev.pushErrorScope("validation");
108
+ const mod=dev.createShaderModule({code:kern(V.mode,V.R,V.T,V.ROWS)});
109
+ const ci=await mod.getCompilationInfo(); const er=ci.messages.filter(m=>m.type==="error");
110
+ if(er.length){ await dev.popErrorScope(); rows.push({V,err:er[0].message}); continue; }
111
+ const pipe=dev.createComputePipeline({layout:"auto",compute:{module:mod,entryPoint:"main"}});
112
+ const bg=dev.createBindGroup({layout:pipe.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:xbuf}},{binding:1,resource:{buffer:qw}},{binding:2,resource:{buffer:obuf}},{binding:3,resource:{buffer:P}}]});
113
+ const se=await dev.popErrorScope(); if(se){ rows.push({V,err:se.message}); continue; }
114
+ const groups=Math.ceil(N/V.ROWS), wgx=Math.min(groups,65535), wgy=Math.ceil(groups/65535);
115
+ async function run(passes){ const e=dev.createCommandEncoder(); for(let k=0;k<passes;k++){ const p=e.beginComputePass(); p.setPipeline(pipe); p.setBindGroup(0,bg); p.dispatchWorkgroups(wgx,wgy); p.end(); } const t0=performance.now(); dev.queue.submit([e.finish()]); await dev.queue.onSubmittedWorkDone(); return performance.now()-t0; }
116
+ say("timing "+V.id+"…");
117
+ await run(2); let best=1e9; for(let k=0;k<ITERS;k++){ best=Math.min(best, await run(PASSES)); }
118
+ const gbps=(wBytes/1073741824)/((best/PASSES)/1000);
119
+ rows.push({V, gbps, tok:gbps/MODEL_GB});
120
+ }
121
+
122
+ say("done · adapter: "+((info.vendor||"?")+" "+(info.architecture||"")+" "+(info.device||"")).trim(),"ok");
123
+ const g=id=>rows.find(r=>r.V.id===id&&r.gbps); const rd=g("read"), d1=g("dot1"), d2=g("dot2"), d4=g("dot4"), o32=g("occ32"), o128=g("occ128");
124
+ const maxG=Math.max(...rows.filter(r=>r.gbps).map(r=>r.gbps));
125
+ const readVsDot = (rd&&d1)? rd.gbps/d1.gbps : 0;
126
+ const aluSlope = (d1&&d4)? d1.gbps/d4.gbps : 0; // >~1.5 ⇒ time scales with ALU ⇒ ALU-throughput bound
127
+ const occGain = d1? Math.max(o32?o32.gbps/d1.gbps:0, o128?o128.gbps/d1.gbps:0) : 0;
128
+ let verdict, cls;
129
+ if(readVsDot>=1.6){ verdict = `ALU-BOUND. read-only reaches ${rd.gbps.toFixed(0)} GB/s but dot ×1 only ${d1.gbps.toFixed(0)} (${readVsDot.toFixed(1)}× gap), and more ALU scales time down (×1→×4 = ${aluSlope.toFixed(1)}×). The f32 unpack is the wall → fewer instructions per weight (int8 dot4I8Packed) is the lever. Spec-decode's batch stays expensive until this is fixed.`; cls="warn"; }
130
+ else if(occGain>=1.15){ verdict = `OCCUPANCY/LATENCY-BOUND. read-only ≈ dot (${readVsDot.toFixed(1)}×) so ALU is hidden, and changing threads/row moved it ${occGain.toFixed(2)}× — the kernel isn't keeping enough memory requests in flight. Lever = restructure parallelism (rows/wg, threads/row, more waves), NOT int8.`; cls="warn"; }
131
+ else{ verdict = `MEMORY/LATENCY-BOUND at this access pattern. read-only (${rd?rd.gbps.toFixed(0):"?"} GB/s) ≈ dot ×1 (${d1?d1.gbps.toFixed(0):"?"}), ALU adds little (×1→×4 = ${aluSlope.toFixed(1)}×), and occupancy tweaks barely move it (${occGain.toFixed(2)}×). int8 will NOT help — the per-row cooperative read pattern is the ceiling; needs a different memory layout. Compare to the 152 GB/s pure-stream roofline: this pattern leaves ${rd?(100*rd.gbps/152).toFixed(0):"?"}% on the table.`; cls="no"; }
132
+
133
+ out.innerHTML=`
134
+ <div class="card"><span class="k" style="color:var(--dim)">Test matrix</span> &nbsp; ${N.toLocaleString()} × ${K} · ${(wBytes/1073741824).toFixed(2)} GB · pure-stream roofline 152 GB/s (220 tok/s)</div>
135
+ <div class="card"><table>
136
+ <tr><th>variant</th><th class="n">GB/s</th><th class="n">tok/s-equiv</th><th></th></tr>
137
+ ${rows.map(r=>r.err
138
+ ? `<tr><td>${r.V.name}</td><td colspan=3 class="no">failed: ${r.err.slice(0,54)}</td></tr>`
139
+ : `<tr><td>${r.V.name}</td><td class="n">${r.gbps.toFixed(0)}</td><td class="n">${r.tok.toFixed(0)}</td><td><span class="bar" style="width:${Math.round(140*r.gbps/maxG)}px"></span></td></tr>`).join("")}
140
+ </table></div>
141
+ <div class="verdict ${cls}">${verdict}</div>`;
142
+ }catch(e){ say("✗ "+(e&&(e.message||e)), "no"); }
143
+ })();
144
+ </script></body></html>