NakliTechie commited on
Commit
f260d7d
·
1 Parent(s): f3ea17b

Sync from GitHub 2026-06-12T13:28:48Z

Browse files
Files changed (2) hide show
  1. index.html +45 -9
  2. kohra.js +42 -6
index.html CHANGED
@@ -32,6 +32,7 @@
32
  .row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; margin:10px 0; }
33
  label { color:var(--dim); font-size:13px; }
34
  input[type=number] { width:72px; background:#1b1e25; color:var(--fg); border:1px solid #2a2e37; border-radius:6px; padding:4px 6px; font:inherit; }
 
35
  button { background:var(--acc); color:#10131a; border:0; border-radius:8px; padding:8px 18px; font:inherit; font-weight:600; cursor:pointer; }
36
  button:disabled { opacity:.4; cursor:default; }
37
  #canvas { background:#1b1e25; border:1px solid #2a2e37; border-radius:8px; padding:14px; margin-top:14px; min-height:120px; white-space:pre-wrap; word-break:break-word; }
@@ -57,6 +58,12 @@
57
  <p><span id="status" class="status-badge loading"><span class="spinner"></span><span id="statusText">Loading…</span></span></p>
58
  <textarea id="prompt">Lily runs 12 km/h for 4 hours. How far in 8 hours?</textarea>
59
  <div class="row">
 
 
 
 
 
 
60
  <label>new tokens <input id="maxNew" type="number" value="128" step="32" min="16"></label>
61
  <label>steps <input id="steps" type="number" value="128" step="16" min="4"></label>
62
  <label>block <input id="blockSize" type="number" value="32" step="8" min="8"></label>
@@ -83,8 +90,6 @@ import { DiffusionLM } from './kohra.js';
83
 
84
  // fused fp16 (G1d): RMSNorm fused to SimplifiedLayerNormalization so WebGPU's native
85
  // fp16 kernels don't overflow the decomposed Pow(x,2) -> all-zero. ~1.4GB, ~9.8 tok/s.
86
- const HF = 'https://huggingface.co/naklitechie/Qwen3-0.6B-diffusion-mdlm-ONNX/resolve/main';
87
- const TOKENIZER_ID = 'naklitechie/Qwen3-0.6B-diffusion-mdlm-ONNX';
88
  const $ = (id) => document.getElementById(id);
89
  // state: 'loading' (spinner) | 'ready' | 'error'
90
  const status = (t, state = 'loading') => {
@@ -92,11 +97,37 @@ const status = (t, state = 'loading') => {
92
  $('status').className = 'status-badge ' + state;
93
  };
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  const params = new URLSearchParams(location.search);
96
- const modelUrl = params.get('model') ?? `${HF}/onnx/model_fp16_fused.onnx`;
97
- // optional overrides for q4/ORT experiments; defaults keep the shipped demo unchanged
98
- const ortVersion = params.get('ort') || undefined;
99
- const graphOptimizationLevel = params.get('opt') || 'disabled';
 
 
 
 
 
100
 
101
  let lm;
102
 
@@ -123,6 +154,7 @@ async function generate() {
123
  blockSize: +$('blockSize').value,
124
  temperature: +$('temp').value,
125
  threshold: +$('threshold').value || null, // Fast-dLLM confidence-threshold decoding
 
126
  stripThink: true,
127
  onStep: (s) => {
128
  renderCanvas(s);
@@ -144,10 +176,10 @@ async function generate() {
144
  async function init() {
145
  try {
146
  if (!navigator.gpu) { status('This demo needs WebGPU — try Chrome/Edge 121+.', 'error'); return; }
147
- status('Loading model… first run downloads ~1.4GB + compiles WebGPU shaders (cached after).');
148
- lm = await DiffusionLM.from_pretrained({ model: modelUrl, tokenizer: TOKENIZER_ID, ortVersion, graphOptimizationLevel });
149
  window.kohra = lm; // handy for the console / preview probing
150
- status('Ready — fused fp16 Qwen3-0.6B-MDLM on WebGPU. Press Generate.', 'ready');
151
  $('go').disabled = false;
152
  } catch (e) {
153
  console.error(e);
@@ -158,6 +190,10 @@ async function init() {
158
  addEventListener('error', e => status('Uncaught: ' + (e.error?.message ?? e.message), 'error'));
159
  addEventListener('unhandledrejection', e => status('Rejection: ' + (e.reason?.message ?? e.reason), 'error'));
160
  $('go').addEventListener('click', generate);
 
 
 
 
161
  init();
162
  </script>
163
  </body>
 
32
  .row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; margin:10px 0; }
33
  label { color:var(--dim); font-size:13px; }
34
  input[type=number] { width:72px; background:#1b1e25; color:var(--fg); border:1px solid #2a2e37; border-radius:6px; padding:4px 6px; font:inherit; }
35
+ select { background:#1b1e25; color:var(--fg); border:1px solid #2a2e37; border-radius:6px; padding:4px 6px; font:inherit; cursor:pointer; }
36
  button { background:var(--acc); color:#10131a; border:0; border-radius:8px; padding:8px 18px; font:inherit; font-weight:600; cursor:pointer; }
37
  button:disabled { opacity:.4; cursor:default; }
38
  #canvas { background:#1b1e25; border:1px solid #2a2e37; border-radius:8px; padding:14px; margin-top:14px; min-height:120px; white-space:pre-wrap; word-break:break-word; }
 
58
  <p><span id="status" class="status-badge loading"><span class="spinner"></span><span id="statusText">Loading…</span></span></p>
59
  <textarea id="prompt">Lily runs 12 km/h for 4 hours. How far in 8 hours?</textarea>
60
  <div class="row">
61
+ <label>model <select id="modelSel">
62
+ <option value="mdlm">MDLM · fp16</option>
63
+ <option value="mdlm-q4">MDLM · q4</option>
64
+ <option value="bd3lm">BD3LM · fp16 · block diffusion</option>
65
+ <option value="bd3lm-q4">BD3LM · q4 · block diffusion</option>
66
+ </select></label>
67
  <label>new tokens <input id="maxNew" type="number" value="128" step="32" min="16"></label>
68
  <label>steps <input id="steps" type="number" value="128" step="16" min="4"></label>
69
  <label>block <input id="blockSize" type="number" value="32" step="8" min="8"></label>
 
90
 
91
  // fused fp16 (G1d): RMSNorm fused to SimplifiedLayerNormalization so WebGPU's native
92
  // fp16 kernels don't overflow the decomposed Pow(x,2) -> all-zero. ~1.4GB, ~9.8 tok/s.
 
 
93
  const $ = (id) => document.getElementById(id);
94
  // state: 'loading' (spinner) | 'ready' | 'error'
95
  const status = (t, state = 'loading') => {
 
97
  $('status').className = 'status-badge ' + state;
98
  };
99
 
100
+ // Model registry — each entry is a self-contained HF repo (graph + tokenizer + bundled
101
+ // kohra.js). bd3lm (block diffusion) drives the block-causal sampler; q4 variants are a
102
+ // smaller download. The dropdown picks one (reloads with ?arch=<key>); ?model= (+ optional
103
+ // ?arch=, ?tokenizer=, ?ort=, ?opt=) overrides for local/dev. MDLM-fp16 stays the default.
104
+ const REPO = {
105
+ mdlm: 'https://huggingface.co/naklitechie/Qwen3-0.6B-diffusion-mdlm-ONNX/resolve/main',
106
+ bd3lm: 'https://huggingface.co/naklitechie/Qwen3-0.6B-diffusion-bd3lm-ONNX/resolve/main',
107
+ };
108
+ const TOKENIZER = {
109
+ mdlm: 'naklitechie/Qwen3-0.6B-diffusion-mdlm-ONNX',
110
+ bd3lm: 'naklitechie/Qwen3-0.6B-diffusion-bd3lm-ONNX',
111
+ };
112
+ // q4 (RTN MatMulNBits) needs the ORT-web dev build with the WebGPU q4 fix + full graph opt.
113
+ const ORT_Q4 = '1.26.0-dev.20260416-b7804b056c';
114
+ const REGISTRY = {
115
+ 'mdlm': { label: 'MDLM · fp16', repo: 'mdlm', file: 'model_fp16_fused.onnx', blockCausal: false, q4: false },
116
+ 'mdlm-q4': { label: 'MDLM · q4 (smaller)', repo: 'mdlm', file: 'model_q4f16_rtn_sym.onnx', blockCausal: false, q4: true },
117
+ 'bd3lm': { label: 'BD3LM · fp16 · block diff', repo: 'bd3lm', file: 'model_fp16_fused.onnx', blockCausal: true, q4: false },
118
+ 'bd3lm-q4': { label: 'BD3LM · q4 · block diff', repo: 'bd3lm', file: 'model_q4f16_rtn_sym.onnx', blockCausal: true, q4: true },
119
+ };
120
+
121
  const params = new URLSearchParams(location.search);
122
+ const modelOverride = params.get('model');
123
+ const reqArch = params.get('arch') || (modelOverride?.includes('bd3lm') ? 'bd3lm' : 'mdlm');
124
+ const arch = REGISTRY[reqArch] ? reqArch : 'mdlm'; // unknown key → safe default
125
+ const preset = REGISTRY[arch];
126
+ const modelUrl = modelOverride ?? `${REPO[preset.repo]}/onnx/${preset.file}`;
127
+ const tokenizerId = params.get('tokenizer') ?? TOKENIZER[preset.repo];
128
+ const blockCausal = preset.blockCausal;
129
+ const ortVersion = params.get('ort') || (preset.q4 ? ORT_Q4 : undefined);
130
+ const graphOptimizationLevel = params.get('opt') || (preset.q4 ? 'all' : 'disabled');
131
 
132
  let lm;
133
 
 
154
  blockSize: +$('blockSize').value,
155
  temperature: +$('temp').value,
156
  threshold: +$('threshold').value || null, // Fast-dLLM confidence-threshold decoding
157
+ blockCausal, // bd3lm: static block-causal attention mask
158
  stripThink: true,
159
  onStep: (s) => {
160
  renderCanvas(s);
 
176
  async function init() {
177
  try {
178
  if (!navigator.gpu) { status('This demo needs WebGPU — try Chrome/Edge 121+.', 'error'); return; }
179
+ status(`Loading model… first run downloads ${preset.q4 ? '~0.7GB' : '~1.5GB'} + compiles WebGPU shaders (cached after).`);
180
+ lm = await DiffusionLM.from_pretrained({ model: modelUrl, tokenizer: tokenizerId, ortVersion, graphOptimizationLevel });
181
  window.kohra = lm; // handy for the console / preview probing
182
+ status(`Ready — ${preset.q4 ? 'q4f16' : 'fused fp16'} Qwen3-0.6B-${blockCausal ? 'BD3LM (block diffusion)' : 'MDLM'} on WebGPU. Press Generate.`, 'ready');
183
  $('go').disabled = false;
184
  } catch (e) {
185
  console.error(e);
 
190
  addEventListener('error', e => status('Uncaught: ' + (e.error?.message ?? e.message), 'error'));
191
  addEventListener('unhandledrejection', e => status('Rejection: ' + (e.reason?.message ?? e.reason), 'error'));
192
  $('go').addEventListener('click', generate);
193
+ // Model picker: reflect the active arch, and reload with the chosen preset on change
194
+ // (drop any ?model override so the registry URL is used).
195
+ $('modelSel').value = arch;
196
+ $('modelSel').addEventListener('change', () => { location.search = '?arch=' + $('modelSel').value; });
197
  init();
198
  </script>
199
  </body>
kohra.js CHANGED
@@ -35,6 +35,9 @@ const GEN_DEFAULTS = {
35
  threshold: null, // Fast-dLLM: if set (e.g. 0.9), unmask ALL block positions with
36
  // confidence >= threshold per step (min 1) instead of a fixed
37
  // count → fewer forwards when the model is confident. null = use `steps`.
 
 
 
38
  chatTemplate: true, // wrap prompt in Qwen3 ChatML + generation prompt
39
  stripThink: false, // drop a leading empty <think>…</think> block from the result
40
  onStep: null, // ({ x, P, fresh, block, numBlocks, forward, elapsedMs }) => void
@@ -145,9 +148,43 @@ export class DiffusionLM {
145
  promptIds.forEach((t, i) => { x[i] = BigInt(t); });
146
  for (let i = P; i < T; i++) x[i] = MASK;
147
 
148
- const numBlocks = Math.ceil(cfg.maxNewTokens / cfg.blockSize);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  const stepsPerBlock = Math.ceil(cfg.steps / numBlocks);
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  let forward = 0;
152
  const t0 = performance.now();
153
 
@@ -156,8 +193,7 @@ export class DiffusionLM {
156
  };
157
 
158
  for (let b = 0; b < numBlocks; b++) {
159
- const start = P + b * cfg.blockSize;
160
- const end = Math.min(start + cfg.blockSize, T);
161
  const nMasked = inBlockMasked(start, end);
162
  if (nMasked === 0) continue;
163
 
@@ -168,9 +204,9 @@ export class DiffusionLM {
168
 
169
  while (inBlockMasked(start, end) > 0) {
170
  if (schedule && step >= schedule.length) break;
171
- const out = await this.session.run({
172
- input_ids: new ort.Tensor('int64', x.slice(), [1, T]),
173
- });
174
  forward++;
175
  const logits = out.logits.data; // Float32Array, [T * V] (ORT upcasts fp16)
176
  const V = out.logits.dims[2];
 
35
  threshold: null, // Fast-dLLM: if set (e.g. 0.9), unmask ALL block positions with
36
  // confidence >= threshold per step (min 1) instead of a fixed
37
  // count → fewer forwards when the model is confident. null = use `steps`.
38
+ blockCausal: false, // bd3lm (block diffusion): pass a static [1,1,T,T] additive
39
+ // block-causal attention_mask each forward + denoise on the
40
+ // physical pos//blockSize grid. false = MDLM (single-input, no mask).
41
  chatTemplate: true, // wrap prompt in Qwen3 ChatML + generation prompt
42
  stripThink: false, // drop a leading empty <think>…</think> block from the result
43
  onStep: null, // ({ x, P, fresh, block, numBlocks, forward, elapsedMs }) => void
 
148
  promptIds.forEach((t, i) => { x[i] = BigInt(t); });
149
  for (let i = P; i < T; i++) x[i] = MASK;
150
 
151
+ // Block ranges to denoise, left-to-right. MDLM: blocks start at the prompt end
152
+ // (P + b·blockSize). bd3lm: blocks live on the absolute pos//blockSize grid, so we
153
+ // denoise from the physical block containing P — its prompt prefix is fixed, its
154
+ // masked tail is the first generated tokens (bidirectional within that block).
155
+ const blocks = [];
156
+ if (cfg.blockCausal) {
157
+ for (let blk = Math.floor(P / cfg.blockSize); blk * cfg.blockSize < T; blk++) {
158
+ blocks.push([blk * cfg.blockSize, Math.min((blk + 1) * cfg.blockSize, T)]);
159
+ }
160
+ } else {
161
+ const numBlocks = Math.ceil(cfg.maxNewTokens / cfg.blockSize);
162
+ for (let b = 0; b < numBlocks; b++) {
163
+ const start = P + b * cfg.blockSize;
164
+ blocks.push([start, Math.min(start + cfg.blockSize, T)]);
165
+ }
166
+ }
167
+ const numBlocks = blocks.length;
168
  const stepsPerBlock = Math.ceil(cfg.steps / numBlocks);
169
 
170
+ // bd3lm's static block-causal additive mask: 0 where block(key) ≤ block(query),
171
+ // else -1e9. Built once and fed every forward (the graph's 2nd input, fp32). The
172
+ // mask never changes during sampling; block-causality is what makes the cache-free
173
+ // full-canvas forward give correct current-block logits despite still-masked future
174
+ // blocks (a future block can't influence an earlier one).
175
+ let attnTensor = null;
176
+ if (cfg.blockCausal) {
177
+ const m = new Float32Array(T * T);
178
+ for (let q = 0; q < T; q++) {
179
+ const bq = Math.floor(q / cfg.blockSize);
180
+ const row = q * T;
181
+ for (let k = 0; k < T; k++) {
182
+ m[row + k] = Math.floor(k / cfg.blockSize) <= bq ? 0 : -1e9;
183
+ }
184
+ }
185
+ attnTensor = new ort.Tensor('float32', m, [1, 1, T, T]);
186
+ }
187
+
188
  let forward = 0;
189
  const t0 = performance.now();
190
 
 
193
  };
194
 
195
  for (let b = 0; b < numBlocks; b++) {
196
+ const [start, end] = blocks[b];
 
197
  const nMasked = inBlockMasked(start, end);
198
  if (nMasked === 0) continue;
199
 
 
204
 
205
  while (inBlockMasked(start, end) > 0) {
206
  if (schedule && step >= schedule.length) break;
207
+ const feeds = { input_ids: new ort.Tensor('int64', x.slice(), [1, T]) };
208
+ if (attnTensor) feeds.attention_mask = attnTensor;
209
+ const out = await this.session.run(feeds);
210
  forward++;
211
  const logits = out.logits.data; // Float32Array, [T * V] (ORT upcasts fp16)
212
  const V = out.logits.dims[2];