Spaces:
Running
Running
File size: 11,546 Bytes
a24d0c6 bcdc5b6 a24d0c6 bcdc5b6 a24d0c6 bcdc5b6 a24d0c6 bcdc5b6 a24d0c6 bcdc5b6 a24d0c6 bcdc5b6 a24d0c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | <!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1">
<title>Ternary GEMV limiter profile — your GPU</title>
<style>
:root{--bg:#0a0d13;--panel:#111825;--ink:#e8ebf1;--dim:#8b95a7;--ac:#7c5cff;--ok:#48c26c;--no:#f0616d;--warn:#e0a94a;--line:#1e2836}
*{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}
h1{font-size:20px;margin:0 0 4px}.sub{color:var(--dim);font-size:13px;margin:0 0 18px}
.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}
table{width:100%;border-collapse:collapse;font-family:ui-monospace,monospace;font-size:13px}
td,th{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left}th{color:var(--dim);font-weight:600}
td.n{text-align:right;font-variant-numeric:tabular-nums}
.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}
</style></head><body>
<h1>Ternary GEMV <span style="color:var(--ac)">limiter profile</span> — your GPU</h1>
<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>
<div id="status" class="card">starting…</div>
<div id="out"></div>
<script type="module">
const $=s=>document.querySelector(s), st=$("#status"), out=$("#out");
const MODEL_GB=0.69;
const say=(t,cls)=>{ st.textContent=t; st.className="card"+(cls?" "+cls:""); };
window.addEventListener("unhandledrejection",e=>say("✗ unhandled: "+(e.reason&&(e.reason.message||e.reason)),"no"));
const DOT16=`
fn dot16(word:u32, v:u32) -> f32 {
var s4=vec4<f32>(0.0);
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));
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));
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));
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));
return s4.x+s4.y+s4.z+s4.w;
}`;
// mode: 'read' (xor raw, no unpack) | 'dot' (R× dot16, same reads). T threads/row, ROWS rows/wg (T*ROWS=256).
const kern=(mode,R,T,ROWS)=>`
@group(0) @binding(0) var<storage,read> x: array<vec4<f32>>;
@group(0) @binding(1) var<storage,read> qw: array<u32>;
@group(0) @binding(2) var<storage,read_write> o: array<f32>;
@group(0) @binding(3) var<uniform> P: vec4<u32>; // K, N
var<workgroup> red: array<f32, 256>;
${mode==='dot'?DOT16:''}
@compute @workgroup_size(256)
fn main(@builtin(workgroup_id) wg:vec3<u32>, @builtin(local_invocation_id) lid:vec3<u32>){
let K=P.x; let nw=K>>4u;
let rr=lid.x/${T}u; let t=lid.x%${T}u;
let n0=(wg.y*65535u+wg.x)*${ROWS}u+rr; let n=min(n0, P.y-1u);
let rowW=n*nw;
${mode==='read'?'var accu=0u;':'var acc=0.0;'}
var w=t;
loop{ if(w>=nw){break;}
let word=qw[rowW+w]; let v=w<<2u;
${mode==='read'?'accu=accu^word;':Array.from({length:R},(_,r)=>`acc=acc+dot16(word^${r}u, v);`).join(' ')}
w=w+${T}u; }
${mode==='read'?'var acc=f32(accu&1u); if(accu==0xffffffffu){ acc=acc+x[0].x; }':''}
red[lid.x]=acc; workgroupBarrier();
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; }
if(t==0u && n0<P.y){ o[n0]=red[rr*${T}u]; }
}`;
const FILL=`
@group(0) @binding(0) var<storage,read_write> d: array<u32>;
@group(0) @binding(1) var<uniform> P: vec4<u32>;
@compute @workgroup_size(256)
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; } }`;
const VARIANTS=[
{id:"read", name:"read-only (no ALU) — memory ceiling", mode:"read", R:0, T:64, ROWS:4, grp:"alu"},
{id:"dot1", name:"dot ×1 (the real kernel)", mode:"dot", R:1, T:64, ROWS:4, grp:"alu"},
{id:"dot2", name:"dot ×2 (2× ALU, same reads)", mode:"dot", R:2, T:64, ROWS:4, grp:"alu"},
{id:"dot4", name:"dot ×4 (4× ALU, same reads)", mode:"dot", R:4, T:64, ROWS:4, grp:"alu"},
{id:"dot8", name:"dot ×8 (KX=8 batched-verify proxy)", mode:"dot", R:8, T:64, ROWS:4, grp:"alu"},
{id:"occ32", name:"dot ×1 · 32 thr/row · 8 rows/wg", mode:"dot", R:1, T:32, ROWS:8, grp:"occ"},
{id:"occ128",name:"dot ×1 · 128 thr/row · 2 rows/wg", mode:"dot", R:1, T:128,ROWS:2, grp:"occ"},
];
(async()=>{
try{
if(!navigator.gpu){ say("✗ No WebGPU. Open in Chrome/Edge.","no"); return; }
say("requesting adapter…");
let ad=await navigator.gpu.requestAdapter({powerPreference:"high-performance"}); if(!ad) ad=await navigator.gpu.requestAdapter();
if(!ad){ say("✗ no GPU adapter.","no"); return; }
const L=ad.limits, info=ad.info||{};
const dev=await ad.requestDevice({requiredLimits:{maxStorageBufferBindingSize:L.maxStorageBufferBindingSize, maxBufferSize:L.maxBufferSize, maxComputeWorkgroupsPerDimension:L.maxComputeWorkgroupsPerDimension}});
dev.lost.then(i=>say("✗ device lost: "+(i&&i.message||i.reason||""),"no"));
const K=4096, nw=K/16;
const wantBytes=Math.min(L.maxStorageBufferBindingSize, L.maxBufferSize, Math.round(MODEL_GB*1e9));
const N=Math.floor((wantBytes/4)/nw), qU32=N*nw, wBytes=qU32*4;
say("allocating "+(wBytes/1048576).toFixed(0)+" MB…");
const qw=dev.createBuffer({size:wBytes, usage:GPUBufferUsage.STORAGE});
const xbuf=dev.createBuffer({size:K*4, usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});
const obuf=dev.createBuffer({size:N*4, usage:GPUBufferUsage.STORAGE});
const P=dev.createBuffer({size:16, usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});
const xa=new Float32Array(K); for(let i=0;i<K;i++) xa[i]=(Math.sin(i*12.9898)*43758.5453)%1;
dev.queue.writeBuffer(xbuf,0,xa); dev.queue.writeBuffer(P,0,new Uint32Array([K,N,0,0]));
say("filling weights…");
{ const fmod=dev.createShaderModule({code:FILL}); const fpipe=dev.createComputePipeline({layout:"auto",compute:{module:fmod,entryPoint:"main"}});
const fp=dev.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}); const wgF=Math.min(L.maxComputeWorkgroupsPerDimension,65535);
dev.queue.writeBuffer(fp,0,new Uint32Array([qU32, wgF*256, 0, 0]));
const fbg=dev.createBindGroup({layout:fpipe.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:qw}},{binding:1,resource:{buffer:fp}}]});
const e=dev.createCommandEncoder(); const p=e.beginComputePass(); p.setPipeline(fpipe); p.setBindGroup(0,fbg); p.dispatchWorkgroups(wgF); p.end();
dev.queue.submit([e.finish()]); await dev.queue.onSubmittedWorkDone(); }
const PASSES=10, ITERS=5;
const rows=[];
for(const V of VARIANTS){
say("compiling "+V.id+"…");
dev.pushErrorScope("validation");
const mod=dev.createShaderModule({code:kern(V.mode,V.R,V.T,V.ROWS)});
const ci=await mod.getCompilationInfo(); const er=ci.messages.filter(m=>m.type==="error");
if(er.length){ await dev.popErrorScope(); rows.push({V,err:er[0].message}); continue; }
const pipe=dev.createComputePipeline({layout:"auto",compute:{module:mod,entryPoint:"main"}});
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}}]});
const se=await dev.popErrorScope(); if(se){ rows.push({V,err:se.message}); continue; }
const groups=Math.ceil(N/V.ROWS), wgx=Math.min(groups,65535), wgy=Math.ceil(groups/65535);
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; }
say("timing "+V.id+"…");
await run(2); let best=1e9; for(let k=0;k<ITERS;k++){ best=Math.min(best, await run(PASSES)); }
const gbps=(wBytes/1073741824)/((best/PASSES)/1000);
rows.push({V, gbps, tok:gbps/MODEL_GB});
}
say("done · adapter: "+((info.vendor||"?")+" "+(info.architecture||"")+" "+(info.device||"")).trim(),"ok");
const g=id=>rows.find(r=>r.V.id===id&&r.gbps); const rd=g("read"), d1=g("dot1"), d4=g("dot4"), d8=g("dot8"), o32=g("occ32"), o128=g("occ128");
const maxG=Math.max(...rows.filter(r=>r.gbps).map(r=>r.gbps));
const readVsDot = (rd&&d1)? rd.gbps/d1.gbps : 0; // >~1.5 ⇒ reads themselves fly, ALU is the wall
const aluSlope = (d1&&d4)? d1.gbps/d4.gbps : 0; // >~1.5 ⇒ time scales with ALU ⇒ ALU-throughput bound
const occGain = d1? Math.max(o32?o32.gbps/d1.gbps:0, o128?o128.gbps/d1.gbps:0) : 0;
const batchFree = (d1&&d8)? d8.gbps/d1.gbps : 0; // ~1 ⇒ 8-wide batched verify costs ~same as 1 ⇒ spec-decode is ~free
const readCeil = rd? rd.gbps : 0;
let verdict, cls;
const specNote = d8 ? ` Batched verify (dot ×8, the KX=8 spec proxy) runs at ${d8.gbps.toFixed(0)} GB/s = ${batchFree.toFixed(2)}× the single kernel — so verifying 8 tokens costs ≈ 1 token. Spec-decode IS ~free here; its earlier 1.0× was per-window JS/fence OVERHEAD, not GPU cost. Killing that overhead unlocks the ~4× the 93%-acceptance implies.` : "";
if(aluSlope>=1.6 && readVsDot>=1.6){ verdict = `ALU-BOUND. reads fly (${readCeil.toFixed(0)} GB/s) but dot ×1 only ${d1.gbps.toFixed(0)}, and ×1→×4 scales ${aluSlope.toFixed(1)}×. Fewer instructions per weight (int8 dot4I8Packed) is the lever.` + specNote; cls="warn"; }
else{ verdict = `MEMORY-LATENCY-BOUND — NOT ALU-bound. Quadrupling the ALU (×1→×4) costs only ${aluSlope.toFixed(1)}× → the f32 unpack is hidden; int8 will NOT help. read-only tops ${readCeil.toFixed(0)} GB/s at this pattern vs 152 pure-stream, and dot ×1 sits at ${d1?d1.gbps.toFixed(0):"?"} (${readCeil?(100*d1.gbps/readCeil).toFixed(0):"?"}% of even the read ceiling). Lever = more memory-level parallelism (register-block several output rows per thread so more loads are in flight before the barrier), NOT int8/occupancy.` + specNote; cls=(occGain>=1.15?"warn":"no"); }
out.innerHTML=`
<div class="card"><span class="k" style="color:var(--dim)">Test matrix</span> ${N.toLocaleString()} × ${K} · ${(wBytes/1073741824).toFixed(2)} GB · pure-stream roofline 152 GB/s (220 tok/s)</div>
<div class="card"><table>
<tr><th>variant</th><th class="n">GB/s</th><th class="n">tok/s-equiv</th><th></th></tr>
${rows.map(r=>r.err
? `<tr><td>${r.V.name}</td><td colspan=3 class="no">failed: ${r.err.slice(0,54)}</td></tr>`
: `<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("")}
</table></div>
<div class="verdict ${cls}">${verdict}</div>`;
}catch(e){ say("✗ "+(e&&(e.message||e)), "no"); }
})();
</script></body></html>
|