Spaces:
Running
Running
Download src/expert-stream.js from naklitechie/diskformer-chat: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/naklitechie/diskformer-chat/resolve/main/src/expert-stream.js
- Command line
-
hf download hf://spaces/naklitechie/diskformer-chat/src/expert-stream.js
-
curl -L -o expert-stream.js https://huggingface.co/spaces/naklitechie/diskformer-chat/resolve/main/src/expert-stream.js
12 kB
| /* moe-expert-stream.js — keep a mixture-of-experts model's routed experts on SSD (OPFS) and | |
| * page them into a fixed GPU slot pool on demand. Model-agnostic: an engine describes one | |
| * expert record (its byte layout in the OPFS file and which GPU pool buffer each part goes | |
| * to); this module owns the slot LRU, the reads, the uploads, the pins and the counters. | |
| * | |
| * const xs = new ExpertStreamer({ device, reader, recordBytes, recordOffset, parts, slots, | |
| * numLayers, numExperts }); | |
| * const slots = await xs.ensure(layer, expertIds); // resident + pinned, in id order | |
| * …write `slots` into the kernel's slot table, encode, queue.submit()… | |
| * xs.release(slots); // after the submit | |
| * xs.prefetch(layer + 1, predictedIds); // optional, never blocks | |
| * | |
| * `parts`: [{ buffer: GPUBuffer, srcOffset, bytes }]. Slot s of a part lives at s × bytes in | |
| * that part's buffer, so a kernel finds expert row r of slot s at s × rowsPerExpert + r. | |
| * | |
| * Safety of reuse: an upload (a staging-ring copy, or queue.writeBuffer) is ordered after every | |
| * earlier queue.submit, so a slot may be overwritten as soon as the work that reads it has been | |
| * submitted, even if the GPU has not run it yet. Until then the slot is pinned and never chosen | |
| * for eviction. | |
| */ | |
| export class ExpertStreamer { | |
| constructor({ device, reader, recordBytes, recordOffset, parts, slots, numLayers, numExperts, maxStaging = 32, uploadRing = 16, evict = 'lru', hotHalfLife = 8, maxPrefetchInflight = 8 }) { | |
| Object.assign(this, { device, reader, recordBytes, recordOffset, parts, slots, numLayers, numExperts, maxStaging, evict, maxPrefetchInflight }); | |
| // Prefetches wait in pfQueue (nearest layer first) and at most maxPrefetchInflight of them | |
| // read at once, so a demand read never queues behind a long run of guesses in the workers. | |
| this.pfQueue = []; | |
| this.pfInflight = 0; | |
| // evict 'hot': drop the unpinned expert with the lowest decaying route count, oldest first | |
| // on ties (llama.cpp PR #25294's policy). One tick per ensure() call, so the half-life is in | |
| // tokens × layers. 'lru' (default): plain least recently used. | |
| this.heat = new Float32Array(numLayers * numExperts); | |
| this.heatTick = new Float64Array(numLayers * numExperts); | |
| this.tick = 0; | |
| this.decayPerTick = Math.pow(0.5, 1 / Math.max(1, hotHalfLife * numLayers)); | |
| // Uploads go through a ring of mapped MAP_WRITE buffers + copyBufferToBuffer: measured | |
| // 2026-10-04 on an M4 Pro (Chrome 154) at 27.5 GB/s for 5 MB records with depth 4, against | |
| // 1.88 GB/s for queue.writeBuffer. uploadRing = 0 keeps writeBuffer (A/B measurement only). | |
| this.ring = []; | |
| for (let i = 0; i < uploadRing; i++) { | |
| this.ring.push({ buf: device.createBuffer({ size: Math.ceil(recordBytes / 4) * 4, usage: 0x02 | 0x04, mappedAtCreation: true }), ready: null }); | |
| } | |
| this.ringNext = 0; | |
| this.slotKey = new Int32Array(slots).fill(-1); | |
| this.pinCount = new Int32Array(slots); | |
| this.free = []; | |
| for (let s = slots - 1; s >= 0; s--) this.free.push(s); | |
| this.lru = new Map(); // key → slot, least recently used first | |
| this.inflight = new Map(); // key → { p: Promise<slot>, pins } | |
| this.staging = []; | |
| this.resetStats(); | |
| } | |
| resetStats() { | |
| const L = this.numLayers; | |
| this.stats = { | |
| hits: 0, misses: 0, lateHits: 0, prefetchIssued: 0, prefetchUsed: 0, evictions: 0, | |
| bytesRead: 0, readMs: 0, uploadMs: 0, waitMs: 0, | |
| hitsByLayer: new Uint32Array(L), missesByLayer: new Uint32Array(L), | |
| }; | |
| this.prefetched = new Set(); // keys loaded by prefetch() and not yet demanded | |
| } | |
| key(layer, expert) { return layer * this.numExperts + expert; } | |
| resident() { return this.lru.size; } | |
| pin(s) { this.pinCount[s]++; } | |
| release(slots) { for (const s of slots) if (this.pinCount[s] > 0) this.pinCount[s]--; } | |
| takeStaging() { return this.staging.pop() || new ArrayBuffer(this.recordBytes); } | |
| giveStaging(buf) { if (this.staging.length < this.maxStaging && buf.byteLength >= this.recordBytes) this.staging.push(buf); } | |
| heatOf(key) { return this.heat[key] * Math.pow(this.decayPerTick, this.tick - this.heatTick[key]); } | |
| touch(key) { this.heat[key] = this.heatOf(key) + 1; this.heatTick[key] = this.tick; } | |
| allocSlot() { | |
| if (this.free.length) return this.free.pop(); | |
| if (this.evict === 'hot') { | |
| let bestK = -1, bestS = -1, bestH = Infinity; | |
| for (const [k, s] of this.lru) { // LRU order, so the first minimum is the oldest | |
| if (this.pinCount[s] !== 0) continue; | |
| const h = this.heatOf(k); | |
| if (h < bestH) { bestH = h; bestK = k; bestS = s; } | |
| } | |
| if (bestS >= 0) { | |
| this.lru.delete(bestK); this.prefetched.delete(bestK); this.slotKey[bestS] = -1; this.stats.evictions++; | |
| return bestS; | |
| } | |
| throw new Error(`expert pool exhausted: all ${this.slots} slots pinned — raise the pool size`); | |
| } | |
| for (const [k, s] of this.lru) { | |
| if (this.pinCount[s] === 0) { | |
| this.lru.delete(k); | |
| this.prefetched.delete(k); | |
| this.slotKey[s] = -1; | |
| this.stats.evictions++; | |
| return s; | |
| } | |
| } | |
| throw new Error(`expert pool exhausted: all ${this.slots} slots pinned — raise the pool size`); | |
| } | |
| // Starts the read + upload of one expert into a newly allocated slot. The slot stays pinned | |
| // while the load is in flight; `entry.pins` demands registered meanwhile are converted into | |
| // pins in the same synchronous step that publishes the slot, so no eviction can slip | |
| // between "loaded" and "pinned by its user". Returns the in-flight entry { p, pins }. | |
| load(layer, expert, pins = 0) { | |
| const key = this.key(layer, expert); | |
| const s = this.allocSlot(); | |
| this.pin(s); | |
| const entry = { pins, p: null }; | |
| entry.p = (async () => { | |
| let ok = false; | |
| try { | |
| const r = await this.reader.read(this.recordOffset(layer, expert), this.recordBytes, this.takeStaging()); | |
| if (r.got !== this.recordBytes) throw new Error(`short expert read L${layer} E${expert}: ${r.got} of ${this.recordBytes} bytes`); | |
| this.stats.readMs += r.ms; | |
| this.stats.bytesRead += r.got; | |
| const t0 = performance.now(); | |
| await this.upload(r.buf, s); | |
| this.stats.uploadMs += performance.now() - t0; | |
| this.giveStaging(r.buf); | |
| this.slotKey[s] = key; | |
| this.lru.set(key, s); | |
| this.pinCount[s] += entry.pins; | |
| ok = true; | |
| return s; | |
| } catch (err) { | |
| if (err && err.buf) this.giveStaging(err.buf); | |
| throw err; | |
| } finally { | |
| this.inflight.delete(key); | |
| this.pinCount[s]--; | |
| if (!ok) { this.pinCount[s] = 0; this.free.push(s); } | |
| } | |
| })(); | |
| this.inflight.set(key, entry); | |
| return entry; | |
| } | |
| // Copies one record into slot `s` of every part. Queue order makes this as safe as | |
| // writeBuffer: the copy is submitted after the work that last read the slot and before the | |
| // work that will read the new expert. | |
| async upload(buf, s) { | |
| if (!this.ring.length) { | |
| for (const part of this.parts) this.device.queue.writeBuffer(part.buffer, s * part.bytes, buf, part.srcOffset, part.bytes); | |
| return; | |
| } | |
| const r = this.ring[this.ringNext]; | |
| this.ringNext = (this.ringNext + 1) % this.ring.length; | |
| while (r.ready) { const p = r.ready; await p; if (r.ready === p) r.ready = null; } | |
| new Uint8Array(r.buf.getMappedRange(0, r.buf.size)).set(new Uint8Array(buf, 0, this.recordBytes)); | |
| r.buf.unmap(); | |
| const enc = this.device.createCommandEncoder(); | |
| for (const part of this.parts) enc.copyBufferToBuffer(r.buf, part.srcOffset, part.buffer, s * part.bytes, part.bytes); | |
| this.device.queue.submit([enc.finish()]); | |
| r.ready = r.buf.mapAsync(2); | |
| } | |
| // Makes every expert in `ids` resident for `layer` and pins its slot. Returns the slots in | |
| // the same order as `ids`. The caller must release() them after submitting the GPU work. | |
| async ensure(layer, ids) { | |
| this.tick++; | |
| if (this.pfQueue.length) this.pfQueue = this.pfQueue.filter((q) => q.layer > layer); // not sorted by layer once lookahead >= 3 | |
| const out = new Uint32Array(ids.length); | |
| const waits = []; | |
| const st = this.stats; | |
| for (let i = 0; i < ids.length; i++) { | |
| const key = this.key(layer, ids[i]); | |
| this.touch(key); | |
| if (this.prefetched.delete(key)) st.prefetchUsed++; | |
| const s = this.lru.get(key); | |
| if (s !== undefined) { | |
| this.lru.delete(key); this.lru.set(key, s); // most recently used | |
| this.pin(s); | |
| out[i] = s; | |
| st.hits++; st.hitsByLayer[layer]++; | |
| continue; | |
| } | |
| const flying = this.inflight.get(key); | |
| if (flying) { | |
| st.lateHits++; st.hitsByLayer[layer]++; | |
| flying.pins++; | |
| waits.push(flying.p.then((slot) => { out[i] = slot; })); | |
| continue; | |
| } | |
| st.misses++; st.missesByLayer[layer]++; | |
| waits.push(this.load(layer, ids[i], 1).p.then((slot) => { out[i] = slot; })); | |
| } | |
| if (waits.length) { | |
| const t0 = performance.now(); | |
| await Promise.all(waits); | |
| st.waitMs += performance.now() - t0; | |
| } | |
| return out; | |
| } | |
| // For an engine that routes on the GPU against a residency map: `ids` of `layer` were found | |
| // resident and used without an ensure(). Refreshes their recency and counts them as hits; an | |
| // expert evicted since (its slot was reused after that work was submitted) is skipped. | |
| touchUsed(layer, ids) { | |
| this.tick++; | |
| const st = this.stats; | |
| for (const e of ids) { | |
| const key = this.key(layer, e); | |
| const s = this.lru.get(key); | |
| if (s === undefined) continue; | |
| this.touch(key); | |
| if (this.prefetched.delete(key)) st.prefetchUsed++; | |
| this.lru.delete(key); this.lru.set(key, s); | |
| st.hits++; st.hitsByLayer[layer]++; | |
| } | |
| } | |
| // Queues loads for experts that are neither resident, in flight nor queued; never awaits. | |
| // Call with the nearest layer first: the queue is served in order, and entries for a layer | |
| // are dropped once that layer's demand (ensure) arrives. Returns how many loads it started. | |
| prefetch(layer, ids) { | |
| const before = this.stats.prefetchIssued; | |
| for (const e of ids) { | |
| const key = this.key(layer, e); | |
| if (this.lru.has(key) || this.inflight.has(key) || this.pfQueue.some((q) => q.key === key)) continue; | |
| this.pfQueue.push({ layer, e, key }); | |
| } | |
| this.pump(); | |
| return this.stats.prefetchIssued - before; | |
| } | |
| // Starts queued prefetches while fewer than maxPrefetchInflight are reading. Stops when the | |
| // pool has nothing evictable left. | |
| pump() { | |
| while (this.pfInflight < this.maxPrefetchInflight && this.pfQueue.length) { | |
| const { layer, e, key } = this.pfQueue.shift(); | |
| if (this.lru.has(key) || this.inflight.has(key)) continue; | |
| let entry; | |
| try { entry = this.load(layer, e); } catch (_) { this.pfQueue.length = 0; break; } | |
| this.prefetched.add(key); | |
| this.stats.prefetchIssued++; | |
| this.pfInflight++; | |
| entry.p.catch(() => {}).finally(() => { this.pfInflight--; this.pump(); }); | |
| } | |
| } | |
| // Waits for every in-flight load and staging-ring map (before teardown and between runs). | |
| async drain() { | |
| await Promise.allSettled([...this.inflight.values()].map((f) => f.p)); | |
| await Promise.allSettled(this.ring.map((r) => r.ready).filter(Boolean)); | |
| } | |
| ringBytes() { return this.ring.reduce((a, r) => a + r.buf.size, 0); } | |
| // Empties the pool. Waits for loads still reading first: a load that finished after the | |
| // reset would claim a slot that is back on the free list. | |
| async clear() { | |
| this.pfQueue = []; | |
| await this.drain(); | |
| this.lru.clear(); | |
| this.inflight.clear(); | |
| this.prefetched.clear(); | |
| this.slotKey.fill(-1); | |
| this.pinCount.fill(0); | |
| this.free = []; | |
| for (let s = this.slots - 1; s >= 0; s--) this.free.push(s); | |
| } | |
| } | |