| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createArena } from './arena.js'; |
| import { deviceSupportsImmediates } from './device.js'; |
| import { |
| dispatchGemm, dispatchAttention, dispatchAddLn, dispatchEmbed, |
| dispatchArgmaxPenalty, dispatchArgmaxReduce, dispatchGemmRowLn, |
| dispatchGemmReduce, dispatchDecoderMega, decodeMegaSharedBytes, splitKParts, |
| dispatchCompactGather, purgeBindGroupsForBuffers, |
| } from './pipelines.js'; |
| import { |
| D_MODEL, HEADS, HEAD_DIM, FFN, VOCAB, DECODE_CAP, BITMASK_WORDS, DEC_LAYERS, |
| DECODER_START, assertModelActive, |
| } from './constants.js'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function normalizeKvCapacity(value, maxSteps) { |
| if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > DECODE_CAP) { |
| throw new Error(`decode maxSteps must be an integer in [1, ${DECODE_CAP}], got ${maxSteps}`); |
| } |
| |
| |
| if (value === null || value === undefined) return DECODE_CAP; |
| if (!Number.isInteger(value) || value < 1) { |
| throw new Error(`kvCapacity must be a positive integer, got ${value}`); |
| } |
| return Math.min(value, maxSteps, DECODE_CAP); |
| } |
|
|
| export function nextKvCapacity({ current, required, maxSteps, groupSteps = 8 }) { |
| for (const [name, value] of Object.entries({ current, required, maxSteps, groupSteps })) { |
| if (!Number.isInteger(value) || value < 1) { |
| throw new Error(`KV grow ${name} must be a positive integer, got ${value}`); |
| } |
| } |
| if (current > DECODE_CAP || maxSteps > DECODE_CAP) { |
| throw new Error(`KV grow capacity exceeds decode cap ${DECODE_CAP}`); |
| } |
| if (required > maxSteps) { |
| throw new Error(`KV grow requires ${required} steps but run maxSteps=${maxSteps}`); |
| } |
| if (required <= current) return current; |
| const wanted = Math.max(required, current * 2); |
| const aligned = Math.ceil(wanted / groupSteps) * groupSteps; |
| return Math.min(maxSteps, DECODE_CAP, aligned); |
| } |
|
|
| function allocateKvGeneration(device, { B, capacity, HD, eb, usage }) { |
| const arena = createArena(device); |
| const kvCacheK = []; |
| const kvCacheV = []; |
| try { |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| kvCacheK.push(arena.buf(B * capacity * HD * eb, usage, `dec kvK.${l} cap${capacity}`)); |
| kvCacheV.push(arena.buf(B * capacity * HD * eb, usage, `dec kvV.${l} cap${capacity}`)); |
| } |
| } catch (err) { |
| arena.destroy(); |
| throw err; |
| } |
| return { arena, kvCacheK, kvCacheV }; |
| } |
|
|
| export function createDecodeState(ctx, weights, { B, S, maxSteps, kvCapacity = null, lmHead = 'auto', lmHeadFlags = null, lmHeadFuse = 'auto', tiledProj = 'auto', ffn = 'auto', ffnFlags = null, fuseLn = 'auto', proj = 'auto', ffnSplitK = 'auto', projSplitK = 'auto', decodeMega = 'auto', sg = 'auto', immediates = 'auto', inPlaceCompact = false }) { |
| assertModelActive(weights.model, 'createDecodeState weights'); |
| const HD = HEADS * HEAD_DIM; |
| const QKV_N = 3 * HD; |
| const { device } = ctx; |
| const eb = weights.dtype === 'f16' ? 2 : 4; |
| const normalizedKvCapacity = normalizeKvCapacity(kvCapacity, maxSteps); |
|
|
| |
| |
| |
| |
| |
| const bindingLimit = ctx?.limits?.maxStorageBufferBindingSize ?? 134217728; |
| const biggest = [ |
| [`kv cache max [B, ${DECODE_CAP}, ${HD}]`, B * DECODE_CAP * HD * eb], |
| ['f32 logits [B, 24000]', B * VOCAB * 4], |
| ['token ring [maxSteps, B]', maxSteps * B * 4], |
| ]; |
| for (const [what, bytes] of biggest) { |
| if (bytes > bindingLimit) { |
| throw new Error( |
| `createDecodeState: ${what} = ${bytes} bytes exceeds ` + |
| `maxStorageBufferBindingSize=${bindingLimit} at B=${B} — reduce batch`, |
| ); |
| } |
| } |
|
|
| const arena = createArena(device); |
| const act = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC; |
| const rw = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; |
|
|
| |
| |
| const kvUsage = act | GPUBufferUsage.COPY_DST; |
| const kv = allocateKvGeneration(device, { |
| B, capacity: normalizedKvCapacity, HD, eb, usage: kvUsage, |
| }); |
| try { |
| const { kvCacheK, kvCacheV } = kv; |
|
|
| |
| const tokenRing = arena.buf(maxSteps * B * 4, rw, 'dec token ring'); |
| const done = arena.buf(B * 4, rw, 'dec done'); |
|
|
| |
| |
| const bitmask = arena.buf(B * BITMASK_WORDS * 4, rw, 'dec bitmask'); |
| const maskInit = new Uint32Array(B * BITMASK_WORDS); |
| for (let b = 0; b < B; b++) { |
| maskInit[b * BITMASK_WORDS + (DECODER_START >> 5)] = 1 << (DECODER_START & 31); |
| } |
| device.queue.writeBuffer(bitmask, 0, maskInit); |
|
|
| |
| const hidden = [ |
| arena.buf(B * D_MODEL * eb, act, 'dec hidden a'), |
| arena.buf(B * D_MODEL * eb, act, 'dec hidden b'), |
| ]; |
| const y = arena.buf(B * D_MODEL * eb, act, 'dec sublayer y'); |
| const attnOut = arena.buf(B * HD * eb, act, 'dec attn out'); |
| const qkvOut = arena.buf(B * QKV_N * eb, act, 'dec qkv out'); |
| const ffnTmp = arena.buf(B * FFN * eb, act, 'dec ffn tmp'); |
| const crossQOut = arena.buf(B * HD * eb, act, 'dec cross q'); |
|
|
| |
| |
| |
| |
| |
| |
| const lmHeadQ8 = lmHead === 'q8' |
| || (lmHead === 'auto' && B >= LM_HEAD_TILED_MIN_B && weights.tensors.has('lm_head.q8')); |
| if (lmHeadQ8 && !weights.tensors.has('lm_head.q8')) { |
| throw new Error("lmHead 'q8' needs weights loaded with lmHeadQ8: true"); |
| } |
| const lmHeadTiled = |
| lmHeadQ8 || lmHead === 'tiled' || (lmHead === 'auto' && B >= LM_HEAD_TILED_MIN_B); |
| |
| |
| |
| |
| |
| |
| |
| |
| const ffnMode = (() => { |
| if (ffn === 'q8') { |
| if (!weights.tensors.has('dec.0.fc1.q8')) throw new Error("ffn 'q8' needs weights loaded with ffnQ8: true"); |
| return 'q8'; |
| } |
| if (ffn === 'wt') { |
| if (!weights.tensors.has('dec.0.fc1.wt')) throw new Error("ffn 'wt' needs weights loaded with ffnWT: true"); |
| return 'wt'; |
| } |
| if (ffn === 'f16') return 'nwt'; |
| |
| |
| |
| return (B < PROJ_TILED_MIN_B && weights.tensors.has('dec.0.fc1.wt')) ? 'wt' : 'nwt'; |
| })(); |
|
|
| |
| |
| |
| |
| |
| |
| const wantSK = ffnSplitK === 'auto' |
| ? { fc1: FFN_SPLITK_AUTO.fc1, fc2: FFN_SPLITK_AUTO.fc2 } |
| : typeof ffnSplitK === 'number' || !ffnSplitK |
| ? { fc1: ffnSplitK || 0, fc2: ffnSplitK || 0 } |
| : { fc1: ffnSplitK.fc1 || 0, fc2: ffnSplitK.fc2 || 0 }; |
| const skKinds = ffnMode === 'q8' ? [] : PROJ_TILED_KINDS.filter((k) => wantSK[k] > 0); |
| const tiledProjKinds = new Set( |
| tiledProj === 'auto' |
| ? (B >= PROJ_TILED_MIN_B ? PROJ_TILED_KINDS |
| : B >= FFN_SPLITK_MIN_B ? skKinds : []) |
| : tiledProj, |
| ); |
|
|
| |
| |
| |
| if (lmHeadFuse === 'on' && !lmHeadTiled) { |
| throw new Error("lmHeadFuse 'on' needs a tiled lm_head (B >= 16 or lmHead 'tiled'/'q8')"); |
| } |
| const lmHeadFused = lmHeadFuse !== 'off' && lmHeadTiled && (lmHeadFlags?.tiledV ?? 2) === 2; |
| |
| |
| |
| |
| const lmShortMeta = weights.lmHeadShort ?? null; |
| const lmShort = !!(lmShortMeta && lmHeadQ8); |
| const lmN = lmShort ? lmShortMeta.ids.length : VOCAB; |
| const lmMaskWords = Math.ceil(lmN / 32); |
| |
| |
| const lmNT = Math.ceil(lmN / (lmHeadFlags?.bn ?? 64)); |
| const logits = lmHeadFused ? null : arena.buf(B * lmN * 4, act, 'dec logits'); |
| const argmaxPartials = lmHeadFused ? arena.buf(B * lmNT * 8, act, 'dec argmax partials') : null; |
|
|
| |
| |
| |
| |
| |
| |
| let bitmaskL = null; |
| if (lmShort) { |
| const startL = lmShortMeta.ids.indexOf(DECODER_START); |
| if (startL < 0) throw new Error('lm_head shortlist must contain decoderStart'); |
| bitmaskL = arena.buf(B * lmMaskWords * 4, rw, 'dec bitmaskL'); |
| const initL = new Uint32Array(B * lmMaskWords); |
| for (let b = 0; b < B; b++) { |
| initL[b * lmMaskWords + (startL >> 5)] = 1 << (startL & 31); |
| } |
| device.queue.writeBuffer(bitmaskL, 0, initL); |
| } |
|
|
| const lnFused = fuseLn === 'on' || (fuseLn === 'auto' && B <= FUSE_LN_MAX_B); |
|
|
| |
| |
| |
| const eligibleSK = (kind) => tiledProjKinds.has(kind) && ffnMode !== 'q8'; |
| const ffnSK = { |
| fc1: eligibleSK('fc1') ? wantSK.fc1 : 0, |
| fc2: eligibleSK('fc2') ? wantSK.fc2 : 0, |
| }; |
|
|
| const projMode = (() => { |
| if (proj === 'wt') { |
| if (!weights.tensors.has('dec.0.self_qkv.wt')) throw new Error("proj 'wt' needs weights loaded with projWT: true"); |
| return 'wt'; |
| } |
| if (proj === 'f16') return 'nwt'; |
| return weights.tensors.has('dec.0.self_qkv.wt') ? 'wt' : 'nwt'; |
| })(); |
|
|
| |
| |
| |
| const wantPSK = projSplitK === 'auto' |
| ? { |
| qkv: B >= PROJ_SPLITK_MIN_B.qkv ? PROJ_SPLITK_AUTO.qkv : 0, |
| out: B >= PROJ_SPLITK_MIN_B.out ? PROJ_SPLITK_AUTO.out : 0, |
| } |
| : typeof projSplitK === 'number' || !projSplitK |
| ? { qkv: projSplitK || 0, out: projSplitK || 0 } |
| : { qkv: projSplitK.qkv || 0, out: projSplitK.out || 0 }; |
| const projSK = projMode === 'wt' ? wantPSK : { qkv: 0, out: 0 }; |
|
|
| |
| |
| |
| const skBK = ffnFlags?.bkk ?? 16; |
| const partsBytes = Math.max( |
| ffnSK.fc1 ? splitKParts(D_MODEL, ffnSK.fc1, skBK).nz * B * FFN * 4 : 0, |
| ffnSK.fc2 ? splitKParts(FFN, ffnSK.fc2, skBK).nz * B * D_MODEL * 4 : 0, |
| projSK.qkv ? splitKParts(D_MODEL, projSK.qkv).nz * B * QKV_N * 4 : 0, |
| projSK.out ? splitKParts(HD, projSK.out).nz * B * D_MODEL * 4 : 0, |
| ); |
| const skParts = partsBytes ? arena.buf(partsBytes, act, 'dec splitK parts') : null; |
|
|
| |
| |
| |
| |
| |
| |
| const megaOk = weights.dtype === 'f16' && ffnMode !== 'q8' |
| && decodeMegaSharedBytes() <= 16384; |
| if (decodeMega === 'on' && !megaOk) { |
| throw new Error( |
| "decodeMega 'on' needs f16 weights, ffn != 'q8', and dims inside the 16KB shared budget"); |
| } |
| const mega = decodeMega === 'on' |
| || (decodeMega === 'auto' && megaOk && B <= DECODE_MEGA_MAX_B); |
|
|
| |
| |
| |
| |
| const compactMap = inPlaceCompact |
| ? arena.buf(B * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 'dec compact map') |
| : null; |
| const compactParams = inPlaceCompact |
| ? arena.buf( |
| (DEC_LAYERS * 3 + 2) * COMPACT_PARAM_STRIDE, |
| GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, |
| 'dec compact params', |
| ) |
| : null; |
|
|
| |
| |
| |
| |
| const sgOk = !!ctx.hasSubgroups && (ctx.subgroupMinSize ?? 0) >= 16; |
| if (sg === 'on' && !sgOk) { |
| throw new Error("sg 'on' needs the subgroups feature and subgroupMinSize ≥ 16"); |
| } |
| const sgOn = sg === 'on'; |
|
|
| if (!['auto', 'on', 'off'].includes(immediates)) { |
| throw new Error(`immediates must be 'auto', 'on', or 'off', got ${immediates}`); |
| } |
| const immediateAvailable = !!ctx.hasImmediates || deviceSupportsImmediates(device); |
| if (immediates === 'on' && !immediateAvailable) { |
| throw new Error("immediates 'on' needs WGSL immediate_address_space support"); |
| } |
| const immediateOn = immediates === 'on' || (immediates === 'auto' && immediateAvailable); |
|
|
| const state = { |
| B, S, maxSteps, kvCapacity: normalizedKvCapacity, arena, kvArena: kv.arena, |
| lmHeadTiled, lmHeadQ8, lmHeadFused, lmHeadFlags, lmNT, |
| lmShort, lmN, lmMaskWords, bitmaskL, |
| tiledProjKinds, ffnMode, ffnFlags, lnFused, projMode, ffnSK, projSK, skParts, |
| mega, sg: sgOn, immediate: immediateOn, inPlaceCompact, |
| kvCacheK, kvCacheV, tokenRing, done, bitmask, logits, argmaxPartials, |
| hidden, y, attnOut, qkvOut, ffnTmp, crossQOut, compactMap, compactParams, |
| destroy() { |
| state.kvArena?.destroy(); |
| state.kvArena = null; |
| arena.destroy(); |
| }, |
| }; |
| return state; |
| } catch (err) { |
| |
| |
| kv.arena.destroy(); |
| arena.destroy(); |
| throw err; |
| } |
| } |
|
|
| |
| |
| |
| export function growDecodeKV( |
| ctx, weights, state, |
| { requiredCapacity, submittedSteps, groupSteps = 8 } = {}, |
| ) { |
| assertModelActive(weights.model, 'growDecodeKV weights'); |
| if (!state?.kvArena || !Array.isArray(state.kvCacheK) || !Array.isArray(state.kvCacheV)) { |
| throw new Error('growDecodeKV: state has no owned KV generation'); |
| } |
| if (!Number.isInteger(submittedSteps) || submittedSteps < 0 |
| || submittedSteps > state.kvCapacity) { |
| throw new Error( |
| `growDecodeKV: submittedSteps=${submittedSteps} outside [0, ${state.kvCapacity}]`, |
| ); |
| } |
| const nextCapacity = nextKvCapacity({ |
| current: state.kvCapacity, |
| required: requiredCapacity, |
| maxSteps: state.maxSteps, |
| groupSteps, |
| }); |
| if (nextCapacity === state.kvCapacity) { |
| return { |
| grown: false, |
| oldCapacity: state.kvCapacity, |
| newCapacity: state.kvCapacity, |
| copiedSteps: submittedSteps, |
| bindGroupsPurged: 0, |
| }; |
| } |
|
|
| const { device } = ctx; |
| const HD = HEADS * HEAD_DIM; |
| const eb = weights.dtype === 'f16' ? 2 : 4; |
| const kvUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; |
| const next = allocateKvGeneration(device, { |
| B: state.B, capacity: nextCapacity, HD, eb, usage: kvUsage, |
| }); |
| const oldCapacity = state.kvCapacity; |
| const oldK = state.kvCacheK; |
| const oldV = state.kvCacheV; |
| const oldArena = state.kvArena; |
| const prefixBytes = submittedSteps * HD * eb; |
| let submitted = false; |
| try { |
| if (prefixBytes > 0) { |
| const encoder = device.createCommandEncoder({ |
| label: `grow decode KV ${oldCapacity} -> ${nextCapacity}`, |
| }); |
| const oldRowBytes = oldCapacity * HD * eb; |
| const newRowBytes = nextCapacity * HD * eb; |
| for (let b = 0; b < state.B; b++) { |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| encoder.copyBufferToBuffer( |
| oldK[l], b * oldRowBytes, next.kvCacheK[l], b * newRowBytes, prefixBytes, |
| ); |
| encoder.copyBufferToBuffer( |
| oldV[l], b * oldRowBytes, next.kvCacheV[l], b * newRowBytes, prefixBytes, |
| ); |
| } |
| } |
| device.queue.submit([encoder.finish()]); |
| submitted = true; |
| } |
| } catch (err) { |
| next.arena.destroy(); |
| throw err; |
| } |
|
|
| let drained = Promise.resolve(); |
| if (submitted && typeof device.queue.onSubmittedWorkDone === 'function') { |
| try { drained = device.queue.onSubmittedWorkDone(); } catch { } |
| } |
|
|
| |
| |
| const bindGroupsPurged = purgeBindGroupsForBuffers(device, [...oldK, ...oldV]); |
| state.kvCacheK = next.kvCacheK; |
| state.kvCacheV = next.kvCacheV; |
| state.kvArena = next.arena; |
| state.kvCapacity = nextCapacity; |
| oldArena.destroyDeferred(drained); |
|
|
| return { |
| grown: true, |
| oldCapacity, |
| newCapacity: nextCapacity, |
| copiedSteps: submittedSteps, |
| bindGroupsPurged, |
| }; |
| } |
|
|
| |
| |
| const COMPACT_PARAM_STRIDE = 256; |
|
|
| |
| |
| |
| |
| |
| const LM_HEAD_TILED_MIN_B = 16; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const PROJ_TILED_KINDS = ['fc1', 'fc2']; |
| const PROJ_TILED_MIN_B = 128; |
|
|
| |
| |
| |
| |
| |
| |
| const FFN_SPLITK_AUTO = { fc1: 8, fc2: 8 }; |
| const FFN_SPLITK_MIN_B = 32; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const PROJ_SPLITK_AUTO = { qkv: 8, out: 8 }; |
| const PROJ_SPLITK_MIN_B = { qkv: 64, out: 128 }; |
|
|
| |
| |
| |
| |
| |
| const DECODE_MEGA_MAX_B = 4; |
|
|
| |
| |
| |
| |
| |
| const FUSE_LN_MAX_B = 4; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function compactDecodeState(ctx, weights, prev, { liveIdx, t0, cap, lens, lastTok }) { |
| const HD = HEADS * HEAD_DIM; |
| const CROSS_KV_STRIDE = 2 * HD; |
| const { device } = ctx; |
| const { state, crossKV, S } = prev; |
| const eb = weights.dtype === 'f16' ? 2 : 4; |
| const newB = liveIdx.length; |
|
|
| const next = createDecodeState(ctx, weights, { |
| B: newB, S, maxSteps: cap, kvCapacity: state.kvCapacity, |
| lmHead: state.lmHeadQ8 ? 'q8' : state.lmHeadTiled ? 'tiled' : 'gemv', |
| lmHeadFlags: state.lmHeadFlags, |
| lmHeadFuse: state.lmHeadFused ? 'on' : 'off', |
| tiledProj: [...state.tiledProjKinds], |
| ffn: { q8: 'q8', wt: 'wt', nwt: 'f16' }[state.ffnMode], |
| ffnFlags: state.ffnFlags, |
| fuseLn: state.lnFused ? 'on' : 'off', |
| proj: state.projMode === 'wt' ? 'wt' : 'f16', |
| ffnSplitK: { ...state.ffnSK }, |
| projSplitK: { ...state.projSK }, |
| decodeMega: state.mega ? 'on' : 'off', |
| sg: state.sg ? 'on' : 'off', |
| immediates: state.immediate ? 'on' : 'off', |
| }); |
| const arena = createArena(device); |
| const cross = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; |
| const newCrossKV = [ |
| arena.buf(newB * S * CROSS_KV_STRIDE * eb, cross, 'compact crossKV dec.0'), |
| arena.buf(newB * S * CROSS_KV_STRIDE * eb, cross, 'compact crossKV dec.1'), |
| ]; |
| const newLens = arena.buf(newB * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 'compact lens'); |
| device.queue.writeBuffer(newLens, 0, lens); |
| |
| device.queue.writeBuffer(next.tokenRing, (t0 - 1) * newB * 4, lastTok); |
|
|
| const enc = device.createCommandEncoder({ label: `compact B ${state.B} -> ${newB}` }); |
| const oldKvRow = state.kvCapacity * HD * eb; |
| const newKvRow = next.kvCapacity * HD * eb; |
| const kvPrefix = Math.min(t0, state.kvCapacity, next.kvCapacity) * HD * eb; |
| const crossRow = S * CROSS_KV_STRIDE * eb; |
| const maskRow = BITMASK_WORDS * 4; |
| for (let i = 0; i < newB; i++) { |
| const o = liveIdx[i]; |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| enc.copyBufferToBuffer( |
| state.kvCacheK[l], o * oldKvRow, next.kvCacheK[l], i * newKvRow, kvPrefix, |
| ); |
| enc.copyBufferToBuffer( |
| state.kvCacheV[l], o * oldKvRow, next.kvCacheV[l], i * newKvRow, kvPrefix, |
| ); |
| enc.copyBufferToBuffer(crossKV[l], o * crossRow, newCrossKV[l], i * crossRow, crossRow); |
| } |
| enc.copyBufferToBuffer(state.bitmask, o * maskRow, next.bitmask, i * maskRow, maskRow); |
| if (state.bitmaskL && next.bitmaskL) { |
| const maskRowL = state.lmMaskWords * 4; |
| enc.copyBufferToBuffer(state.bitmaskL, o * maskRowL, next.bitmaskL, i * maskRowL, maskRowL); |
| } |
| } |
| device.queue.submit([enc.finish()]); |
|
|
| return { state: next, crossKV: newCrossKV, lensBuf: newLens, S, arena }; |
| } |
|
|
| |
| |
| |
| export function validateLiveIdx(liveIdx, oldB) { |
| if (!Array.isArray(liveIdx)) throw new Error('compact in place: liveIdx must be an array'); |
| const newB = liveIdx.length; |
| if (newB < 1 || newB > oldB) { |
| throw new Error(`compact in place: bad live count ${newB} (B=${oldB})`); |
| } |
| for (let i = 0; i < newB; i++) { |
| const value = liveIdx[i]; |
| if (!Number.isInteger(value) || value < 0 || value >= oldB |
| || (i > 0 && value <= liveIdx[i - 1])) { |
| throw new Error( |
| `compact in place: liveIdx must ascend within [0, ${oldB}) — [${liveIdx}]`, |
| ); |
| } |
| } |
| } |
|
|
| |
| |
| export function inPlaceGatherPlan({ |
| t0, S, eb, kvCapacity = DECODE_CAP, lmMaskWords = 0, lmShort = false, |
| }) { |
| const HD = HEADS * HEAD_DIM; |
| const kvStride = (kvCapacity * HD * eb) / 4; |
| const kvCopy = (Math.min(t0, kvCapacity) * HD * eb) / 4; |
| const crossStride = (S * 2 * HD * eb) / 4; |
| const plan = []; |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| plan.push({ key: `kvK.${l}`, strideU32: kvStride, copyU32: kvCopy }); |
| plan.push({ key: `kvV.${l}`, strideU32: kvStride, copyU32: kvCopy }); |
| plan.push({ key: `crossKV.${l}`, strideU32: crossStride, copyU32: crossStride }); |
| } |
| plan.push({ key: 'bitmask', strideU32: BITMASK_WORDS, copyU32: BITMASK_WORDS }); |
| if (lmShort) { |
| plan.push({ key: 'bitmaskL', strideU32: lmMaskWords, copyU32: lmMaskWords }); |
| } |
| for (const item of plan) { |
| if (!Number.isInteger(item.strideU32) || item.strideU32 < 1 |
| || !Number.isInteger(item.copyU32) || item.copyU32 < 1 |
| || item.copyU32 > item.strideU32) { |
| throw new Error(`compact in place: invalid u32 row shape at ${item.key}`); |
| } |
| } |
| return plan; |
| } |
|
|
| |
| |
| |
| |
| export function compactDecodeStateInPlace( |
| ctx, weights, prev, { liveIdx, t0, lens, lastTok }, |
| ) { |
| const { device } = ctx; |
| const { state, crossKV, lensBuf, S } = prev; |
| const oldB = state.B; |
| const newB = liveIdx.length; |
| validateLiveIdx(liveIdx, oldB); |
| if (!state.inPlaceCompact || !state.compactMap || !state.compactParams) { |
| throw new Error('compact in place: state was not created for in-place compaction'); |
| } |
| if (!Number.isInteger(t0) || t0 < 1 || t0 > state.maxSteps) { |
| throw new Error(`compact in place: bad t0=${t0} for maxSteps=${state.maxSteps}`); |
| } |
| if (lens?.length !== newB || lastTok?.length !== newB) { |
| throw new Error( |
| `compact in place: CPU row data mismatch live=${newB} lens=${lens?.length} token=${lastTok?.length}`, |
| ); |
| } |
|
|
| const eb = weights.dtype === 'f16' ? 2 : 4; |
| const plan = inPlaceGatherPlan({ |
| t0, S, eb, kvCapacity: state.kvCapacity, |
| lmMaskWords: state.lmMaskWords, lmShort: !!state.bitmaskL, |
| }); |
| const targets = { bitmask: state.bitmask }; |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| targets[`kvK.${l}`] = state.kvCacheK[l]; |
| targets[`kvV.${l}`] = state.kvCacheV[l]; |
| targets[`crossKV.${l}`] = crossKV[l]; |
| } |
| if (state.bitmaskL) targets.bitmaskL = state.bitmaskL; |
|
|
| |
| |
| if (liveIdx.some((source, i) => source !== i)) { |
| device.queue.writeBuffer(state.compactMap, 0, Uint32Array.from(liveIdx)); |
| const slotU32 = COMPACT_PARAM_STRIDE / 4; |
| const slab = new Uint32Array(slotU32 * plan.length); |
| plan.forEach((item, index) => { |
| slab.set([newB, item.strideU32, item.copyU32, 0], index * slotU32); |
| }); |
| device.queue.writeBuffer(state.compactParams, 0, slab); |
| const encoder = device.createCommandEncoder({ |
| label: `compact in place B ${oldB} -> ${newB}`, |
| }); |
| const pass = encoder.beginComputePass({ label: 'compact gather' }); |
| plan.forEach((item, index) => { |
| dispatchCompactGather(device, pass, { |
| data: targets[item.key], |
| map: state.compactMap, |
| params: { |
| buffer: state.compactParams, |
| offset: index * COMPACT_PARAM_STRIDE, |
| size: 16, |
| }, |
| rowStrideU32: item.strideU32, |
| copyLenU32: item.copyU32, |
| }); |
| }); |
| pass.end(); |
| device.queue.submit([encoder.finish()]); |
| } |
|
|
| device.queue.writeBuffer(lensBuf, 0, lens); |
| device.queue.writeBuffer(state.done, 0, new Uint32Array(newB)); |
| |
| |
| device.queue.writeBuffer(state.tokenRing, (t0 - 1) * newB * 4, lastTok); |
| state.B = newB; |
| return prev; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function encodeDecodeStep(ctx, weights, encRun, state, t, pass) { |
| const HD = HEADS * HEAD_DIM; |
| const QKV_N = 3 * HD; |
| const CROSS_KV_STRIDE = 2 * HD; |
| const { device } = ctx; |
| const { B } = state; |
| const flags = { t: weights.dtype, immediate: state.immediate }; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const sgFlag = state.sg ? { sg: true } : {}; |
| const gemvFlags = { ...flags, gemv: true, ...sgFlag }; |
| const tiledFlags = { ...flags, tiled: true }; |
| const projFlags = (kind) => (state.tiledProjKinds.has(kind) ? tiledFlags : gemvFlags); |
| const W = (name) => weights.bindingFor(name); |
|
|
| const scratch = []; |
| let dispatches = 0; |
| const rec = ({ scratch: s }) => { scratch.push(...s); dispatches++; }; |
|
|
| |
| |
| let cur = 0; |
| const addLn = (prefix) => { |
| rec(dispatchAddLn(device, pass, { |
| x: state.y, r: state.hidden[cur], |
| gamma: W(`${prefix}.weight`), beta: W(`${prefix}.bias`), |
| y: state.hidden[1 - cur], rows: B, flags: { ...flags, ...sgFlag }, |
| })); |
| cur = 1 - cur; |
| }; |
| |
| |
| const projLnFused = (x, wPrefix, lnPrefix, K) => { |
| rec(dispatchGemmRowLn(device, pass, { |
| x, w: W(`${wPrefix}.weight`), b: W(`${wPrefix}.bias`), |
| r: state.hidden[cur], |
| gamma: W(`${lnPrefix}.weight`), beta: W(`${lnPrefix}.bias`), |
| y: state.hidden[1 - cur], M: B, K, N: D_MODEL, flags: { ...flags, ...sgFlag }, |
| })); |
| cur = 1 - cur; |
| }; |
|
|
| |
| |
| |
| if (state.mega) { |
| for (let l = 0; l < DEC_LAYERS; l++) { |
| rec(dispatchDecoderMega(device, pass, { |
| weights, layer: l, embed: l === 0, |
| ring: state.tokenRing, kCache: state.kvCacheK[l], vCache: state.kvCacheV[l], |
| crossKV: encRun.crossKV[l], lens: encRun.lensBuf, x: state.hidden[0], |
| B, t, S: encRun.S, kvCapacity: state.kvCapacity, |
| flags: { ...flags, ...sgFlag }, |
| })); |
| } |
| } else { |
| |
| rec(dispatchEmbed(device, pass, { |
| ids: state.tokenRing, table: W('shared.weight'), posEmbed: W('pos_embed'), |
| y: state.hidden[cur], mode: 'decode', nRows: B, step: t, batch: B, flags, |
| })); |
|
|
| for (let l = 0; l < DEC_LAYERS; l++) { |
| const p = (name) => `dec.${l}.${name}`; |
| |
| |
| |
| |
| |
| |
| const projDispatch = (kind, { x, y, b, K, N, storeKV = null }) => { |
| const sk = kind === 'self_qkv' ? state.projSK.qkv : state.projSK.out; |
| if (sk) { |
| rec(dispatchGemm(device, pass, { |
| x, w: W(p(`${kind}.wt`)), y: null, M: B, K, N, |
| splitK: { parts: state.skParts, sk }, |
| flags: { ...flags, tiled: true, tm8: true, wt: true }, |
| })); |
| rec(dispatchGemmReduce(device, pass, { |
| parts: state.skParts, b, y, M: B, N, nz: splitKParts(K, sk).nz, |
| storeKV, flags, |
| })); |
| return; |
| } |
| const args = state.projMode === 'wt' |
| ? { w: W(p(`${kind}.wt`)), flags: { ...flags, gemv: true, wt: true, ...sgFlag } } |
| : { w: W(p(`${kind}.weight`)), flags: projFlags(kind) }; |
| rec(dispatchGemm(device, pass, { x, w: args.w, b, y, M: B, K, N, storeKV, flags: args.flags })); |
| }; |
|
|
| |
| projDispatch('self_qkv', { |
| x: state.hidden[cur], b: W(p('self_qkv.bias')), y: state.qkvOut, |
| K: D_MODEL, N: QKV_N, |
| storeKV: { |
| kCache: state.kvCacheK[l], vCache: state.kvCacheV[l], |
| t, Lmax: state.kvCapacity, |
| }, |
| }); |
| rec(dispatchAttention(device, pass, { |
| q: state.qkvOut, k: state.kvCacheK[l], v: state.kvCacheV[l], y: state.attnOut, |
| B, M: 1, L: state.kvCapacity, lenMode: 0, step: t, |
| qStride: QKV_N, flags: { ...flags, ...sgFlag }, |
| })); |
| if (state.lnFused) { |
| projLnFused(state.attnOut, p('self_out'), p('ln1'), HD); |
| } else { |
| projDispatch('self_out', { |
| x: state.attnOut, b: W(p('self_out.bias')), y: state.y, |
| K: HD, N: D_MODEL, |
| }); |
| addLn(p('ln1')); |
| } |
|
|
| |
| |
| projDispatch('cross_q', { |
| x: state.hidden[cur], b: W(p('cross_q.bias')), y: state.crossQOut, |
| K: D_MODEL, N: HD, |
| }); |
| rec(dispatchAttention(device, pass, { |
| q: state.crossQOut, k: encRun.crossKV[l], v: encRun.crossKV[l], |
| lens: encRun.lensBuf, y: state.attnOut, |
| B, M: 1, L: encRun.S, lenMode: 1, |
| kvStride: CROSS_KV_STRIDE, kOff: 0, vOff: HD, flags: { ...flags, ...sgFlag }, |
| })); |
| if (state.lnFused) { |
| projLnFused(state.attnOut, p('cross_out'), p('ln2'), HD); |
| } else { |
| projDispatch('cross_out', { |
| x: state.attnOut, b: W(p('cross_out.bias')), y: state.y, |
| K: HD, N: D_MODEL, |
| }); |
| addLn(p('ln2')); |
| } |
|
|
| |
| |
| |
| |
| const wtKernel = (kind) => (state.tiledProjKinds.has(kind) |
| ? { tiled: true, tm8: true } : { gemv: true, ...sgFlag }); |
| const ffnArgs = (kind) => { |
| if (state.ffnMode === 'q8') { |
| return { |
| w: W(p(`${kind}.q8`)), scales: W(p(`${kind}.scales`)), |
| flags: { ...flags, wt: true, wq8: true, ...wtKernel(kind), ...state.ffnFlags }, |
| }; |
| } |
| if (state.ffnMode === 'wt') { |
| return { w: W(p(`${kind}.wt`)), flags: { ...flags, wt: true, ...wtKernel(kind), ...state.ffnFlags } }; |
| } |
| return { w: W(p(`${kind}.weight`)), flags: projFlags(kind) }; |
| }; |
| |
| |
| |
| const fcSplit = (kind, { x, y, b, K, N, silu = false }) => { |
| const sk = state.ffnSK[kind]; |
| const args = ffnArgs(kind); |
| if (!sk) { |
| rec(dispatchGemm(device, pass, { |
| x, w: args.w, scales: args.scales, b, y, M: B, K, N, |
| flags: silu ? { ...args.flags, silu: true } : args.flags, |
| })); |
| return; |
| } |
| rec(dispatchGemm(device, pass, { |
| x, w: args.w, y: null, M: B, K, N, |
| splitK: { parts: state.skParts, sk }, flags: args.flags, |
| })); |
| rec(dispatchGemmReduce(device, pass, { |
| parts: state.skParts, b, y, M: B, N, |
| nz: splitKParts(K, sk, args.flags.bkk ?? 16).nz, |
| flags: { ...flags, silu }, |
| })); |
| }; |
| fcSplit('fc1', { |
| x: state.hidden[cur], b: W(p('fc1.bias')), y: state.ffnTmp, |
| K: D_MODEL, N: FFN, silu: true, |
| }); |
| |
| if (state.lnFused && state.ffnMode !== 'q8') { |
| projLnFused(state.ffnTmp, p('fc2'), p('ln3'), FFN); |
| } else { |
| fcSplit('fc2', { |
| x: state.ffnTmp, b: W(p('fc2.bias')), y: state.y, |
| K: FFN, N: D_MODEL, |
| }); |
| addLn(p('ln3')); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const lmShortArgs = state.lmShort ? { |
| n: state.lmN, maskWords: state.lmMaskWords, |
| idmap: W('lm_head.idmap'), gmask: state.bitmask, |
| } : null; |
| rec(dispatchGemm(device, pass, { |
| x: state.hidden[cur], |
| w: state.lmHeadQ8 ? W('lm_head.q8') : W('shared.weight'), |
| ...(state.lmHeadQ8 ? { scales: W('lm_head.scales') } : {}), |
| y: state.logits, |
| ...(state.lmHeadFused ? { |
| fusedArgmax: state.lmShort ? { |
| partials: state.argmaxPartials, lbias: W('lm_head.sbias'), |
| seen: state.bitmaskL, maskWords: state.lmMaskWords, |
| } : { |
| partials: state.argmaxPartials, lbias: W('final_logits_bias'), seen: state.bitmask, |
| }, |
| } : {}), |
| M: B, K: D_MODEL, N: state.lmN, |
| flags: { |
| ...flags, outT: 'f32', wt: true, |
| ...(state.lmHeadTiled |
| |
| ? { tiled: true, ...(state.lmHeadQ8 ? { wq8: true, tm8: true } : {}), ...state.lmHeadFlags } |
| : { gemv: true, ...sgFlag }), |
| }, |
| })); |
|
|
| if (state.lmHeadFused) { |
| rec(dispatchArgmaxReduce(device, pass, { |
| partials: state.argmaxPartials, |
| bitmask: state.lmShort ? state.bitmaskL : state.bitmask, |
| done: state.done, tokens: state.tokenRing, B, t, NT: state.lmNT, |
| short: lmShortArgs, |
| flags: { immediate: state.immediate }, |
| })); |
| } else { |
| rec(dispatchArgmaxPenalty(device, pass, { |
| logits: state.logits, |
| bias: W(state.lmShort ? 'lm_head.sbias' : 'final_logits_bias'), |
| bitmask: state.lmShort ? state.bitmaskL : state.bitmask, |
| done: state.done, tokens: state.tokenRing, |
| B, t, |
| short: lmShortArgs, |
| flags: { immediate: state.immediate }, |
| })); |
| } |
|
|
| return { dispatches, scratch }; |
| } |
|
|