Spaces:
Running
Running
NakliTechie commited on
Commit ·
f260d7d
1
Parent(s): f3ea17b
Sync from GitHub 2026-06-12T13:28:48Z
Browse files- index.html +45 -9
- 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
|
| 97 |
-
|
| 98 |
-
const
|
| 99 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 148 |
-
lm = await DiffusionLM.from_pretrained({ model: modelUrl, tokenizer:
|
| 149 |
window.kohra = lm; // handy for the console / preview probing
|
| 150 |
-
status(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 172 |
-
|
| 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];
|