@tryhamster/gerbil 1.11.4 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +104 -0
  2. package/dist/{architectures-DmZMEFsA.mjs → architectures-DHwj9AQD.mjs} +50 -38
  3. package/dist/architectures-DHwj9AQD.mjs.map +1 -0
  4. package/dist/browser/index.d.ts.map +1 -1
  5. package/dist/browser/index.js +11 -0
  6. package/dist/browser/index.js.map +1 -1
  7. package/dist/cli.mjs +8 -8
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{defaults-DfGx4d1m.mjs → defaults-B0aQZJTM.mjs} +3 -2
  10. package/dist/defaults-B0aQZJTM.mjs.map +1 -0
  11. package/dist/frameworks/express.mjs +1 -1
  12. package/dist/frameworks/fastify.mjs +1 -1
  13. package/dist/frameworks/hono.mjs +1 -1
  14. package/dist/frameworks/next.d.mts +2 -2
  15. package/dist/frameworks/next.mjs +1 -1
  16. package/dist/frameworks/trpc.mjs +1 -1
  17. package/dist/gerbil-Bw6do78d.mjs +4 -0
  18. package/dist/{gerbil-5_80K0gA.d.mts → gerbil-CD_skiL3.d.mts} +2 -2
  19. package/dist/{gerbil-5_80K0gA.d.mts.map → gerbil-CD_skiL3.d.mts.map} +1 -1
  20. package/dist/{gerbil-CYVmU8sQ.mjs → gerbil-DAz_a4Jh.mjs} +33 -13
  21. package/dist/gerbil-DAz_a4Jh.mjs.map +1 -0
  22. package/dist/gpu/hooks.d.mts +1 -1
  23. package/dist/gpu/hooks.mjs +1 -1
  24. package/dist/gpu/index.d.mts +2 -2
  25. package/dist/gpu/index.mjs +5 -5
  26. package/dist/{gpu-CrzjQHv2.mjs → gpu-CU_Mldk0.mjs} +1959 -198
  27. package/dist/gpu-CU_Mldk0.mjs.map +1 -0
  28. package/dist/index-B3tjyDJI.d.mts.map +1 -1
  29. package/dist/{index-h8TDu1qm.d.mts → index-Fj2XkP-o.d.mts} +1946 -938
  30. package/dist/index-Fj2XkP-o.d.mts.map +1 -0
  31. package/dist/index.d.mts +3 -3
  32. package/dist/index.d.mts.map +1 -1
  33. package/dist/index.mjs +6 -6
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/integrations/ai-sdk.mjs +1 -1
  36. package/dist/integrations/langchain.mjs +1 -1
  37. package/dist/integrations/llamaindex.mjs +1 -1
  38. package/dist/integrations/mcp.d.mts +2 -2
  39. package/dist/integrations/mcp.mjs +4 -4
  40. package/dist/{mcp-CAsD7eCj.mjs → mcp-CdMvLQ_9.mjs} +3 -3
  41. package/dist/{mcp-CAsD7eCj.mjs.map → mcp-CdMvLQ_9.mjs.map} +1 -1
  42. package/dist/{moonshine-stt-BXoZaHJE.mjs → moonshine-stt-COJeK2Zb.mjs} +6348 -2000
  43. package/dist/moonshine-stt-COJeK2Zb.mjs.map +1 -0
  44. package/dist/moonshine-stt-Dp4j5JEB.mjs +4 -0
  45. package/dist/{one-liner-ppgw4jHH.mjs → one-liner-CMiGNWJ7.mjs} +2 -2
  46. package/dist/{one-liner-ppgw4jHH.mjs.map → one-liner-CMiGNWJ7.mjs.map} +1 -1
  47. package/dist/repl-BhaLCJFb.mjs +9 -0
  48. package/dist/skills/index.d.mts +4 -4
  49. package/dist/skills/index.mjs +3 -3
  50. package/dist/{skills-CQa1Gshd.mjs → skills-BXybFWlG.mjs} +2 -2
  51. package/dist/{skills-CQa1Gshd.mjs.map → skills-BXybFWlG.mjs.map} +1 -1
  52. package/dist/tune/index.mjs +1 -1
  53. package/package.json +1 -1
  54. package/dist/architectures-DmZMEFsA.mjs.map +0 -1
  55. package/dist/defaults-DfGx4d1m.mjs.map +0 -1
  56. package/dist/gerbil-CTefAwKp.mjs +0 -4
  57. package/dist/gerbil-CYVmU8sQ.mjs.map +0 -1
  58. package/dist/gpu-CrzjQHv2.mjs.map +0 -1
  59. package/dist/index-h8TDu1qm.d.mts.map +0 -1
  60. package/dist/moonshine-stt-BXoZaHJE.mjs.map +0 -1
  61. package/dist/moonshine-stt-DZVnKgPO.mjs +0 -4
  62. package/dist/repl-C0Ew7_Z-.mjs +0 -9
@@ -814,1038 +814,1947 @@ declare function quantizeKaniBackbone(graph: ModelGraph, weights: Map<string, {
814
814
  shape: number[];
815
815
  }>): void;
816
816
  //#endregion
817
- //#region src/gpu/sampler.d.ts
818
- /**
819
- * CPU-side token sampling from logits.
820
- *
821
- * Applies temperature, top-k, and top-p (nucleus) filtering,
822
- * then samples from the resulting probability distribution.
823
- *
824
- * Uses typed arrays and min-heap for zero-allocation top-K selection.
825
- * For vocab_size ~152K with topK=50, this avoids creating 152K JS tuples.
826
- */
827
- interface SamplingParams {
828
- temperature?: number;
829
- topK?: number;
830
- topP?: number;
831
- repetitionPenalty?: number;
832
- }
833
- //#endregion
834
- //#region src/gpu/vision-preprocess.d.ts
835
- /**
836
- * Host-side vision preprocessing for the Qwen3.5 ViT.
837
- *
838
- * The learned position embeddings (bilinear-interpolated over the patch grid)
839
- * and the 2D rotary cos/sin tables are functions of the image grid (t, h, w)
840
- * ONLY — not of the model weights or pixel values. They are cheap to compute on
841
- * the CPU and fed to the GPU graph as input activations, keeping the graph to
842
- * weight-dependent math while staying byte-identical to HF transformers.
843
- *
844
- * Ports (verified against transformers 5.12 vision_utils.py + modeling_qwen3_5):
845
- * - get_vision_bilinear_indices_and_weights → buildPosEmbeds()
846
- * - get_vision_position_ids → buildPositionIds()
847
- * - Qwen3_5VisionRotaryEmbedding → buildRotaryCosSin()
848
- *
849
- * Patch ordering: both the pos-embed gather and the position ids reorder patches
850
- * into spatial_merge_size×spatial_merge_size groups, matching the HF image
851
- * processor's output ordering, so the merger's [N,h]→[N/u,h*u] reshape lines up.
852
- */
853
- interface VisionGridConfig {
854
- hiddenSize: number;
855
- numHeads: number;
856
- numPositionEmbeddings: number;
857
- spatialMergeSize: number;
858
- ropeTheta?: number;
859
- }
860
- interface VisionPositionTensors {
861
- /** [N, hidden_size] bilinear-interpolated learned position embeddings. */
862
- posEmbeds: Float32Array;
863
- /** [N, head_dim] rotary cosines. */
864
- cos: Float32Array;
865
- /** [N, head_dim] rotary sines. */
866
- sin: Float32Array;
867
- numPatches: number;
868
- }
869
- /**
870
- * Build bilinear-interpolated learned position embeddings [N, hidden].
871
- * posEmbedTable is the raw pos_embed.weight [num_position_embeddings, hidden].
872
- */
873
- declare function buildPosEmbeds(gridTHW: [number, number, number], posEmbedTable: Float32Array, cfg: VisionGridConfig): Float32Array;
874
- /**
875
- * Build the reordered (row, col) position ids [N, 2] for rotary, matching
876
- * get_vision_position_ids.
877
- */
878
- declare function buildPositionIds(gridTHW: [number, number, number], merge: number): Int32Array;
879
- /**
880
- * Build rotary cos/sin tables [N, head_dim] from position ids, matching
881
- * Qwen3_5VisionRotaryEmbedding + the cat((rotary, rotary)) in VisionModel.forward.
882
- *
883
- * rotary_pos_emb(position_ids) = (position_ids[..,None] * inv_freq).flatten(1)
884
- * where inv_freq has length (head_dim/2)/2 = head_dim/4, computed over dim=head_dim/2.
885
- * For each token the two position components (h, w) each produce head_dim/4 freqs,
886
- * concatenated → head_dim/2, then duplicated → head_dim for cos/sin.
887
- */
888
- declare function buildRotaryCosSin(positionIds: Int32Array, headDim: number, theta?: number): {
889
- cos: Float32Array;
890
- sin: Float32Array;
891
- numPatches: number;
892
- };
893
- /**
894
- * Build all host position tensors for a single image grid in one call.
895
- */
896
- declare function buildVisionPositionTensors(gridTHW: [number, number, number], posEmbedTable: Float32Array, cfg: VisionGridConfig): VisionPositionTensors;
897
- interface Gemma4VisionGridConfig {
898
- hiddenSize: number;
899
- numHeads: number;
900
- headDim: number;
901
- ropeTheta: number;
902
- poolingKernelSize: number;
903
- }
904
- interface Gemma4VisionPositionTensors {
905
- /** [N, hidden] axial position embeddings (table[0][x] + table[1][y]). */
906
- posEmbeds: Float32Array;
907
- /** [N, headDim] axial rotary cosines. */
908
- cos: Float32Array;
909
- /** [N, headDim] axial rotary sines. */
910
- sin: Float32Array;
911
- /** [Np, N] average-pooling matrix (1/k² in-cell, 0 elsewhere). */
912
- poolMatrix: Float32Array;
913
- /** number of patches N (= gridH*gridW). */
914
- numPatches: number;
915
- /** number of pooled (soft) tokens Np (= ceil(gridH/k)*ceil(gridW/k)). */
916
- numPooled: number;
917
- }
918
- /**
919
- * Build axial learned position embeddings [N, hidden] from the [2, posSize, hidden]
920
- * table: pos[p] = table[0][x_p] + table[1][y_p]. Direct lookup, no interpolation
921
- * (HF F.embedding on clamped positions).
922
- */
923
- declare function buildGemma4PosEmbeds(gridH: number, gridW: number, posEmbedTable: Float32Array,
924
- // [2, posSize, hidden] flattened
925
- hidden: number, posSize: number): Float32Array;
926
- /**
927
- * Build the 2D axial rotary cos/sin tables [N, headDim].
928
- * spatial_dim = headDim / 2; inv_freq[j] = 1/theta^((2j)/spatial_dim), j in [0, spatial_dim/2)
929
- * per spatial dim: f = pos * inv_freq (spatial_dim/2 values); emb = cat(f, f) (spatial_dim values)
930
- * cos/sin = cat([emb_x, emb_y]) → headDim values, layout [fx,fx,fy,fy].
931
- * Applied with the global-half rotate_half kernel (ApplyRotaryEmb), which computes
932
- * out = x*cos + rotate_half(x)*sin element-wise — exact for this layout.
933
- */
934
- declare function buildGemma4RotaryCosSin(gridH: number, gridW: number, headDim: number, theta: number): {
935
- cos: Float32Array;
936
- sin: Float32Array;
937
- };
938
- /**
939
- * Build the [Np, N] average-pooling matrix for k×k spatial pooling over the real
940
- * (unpadded) grid, matching modeling_gemma4's kernel_idxs/one_hot pooling:
941
- * cell(p) = floor(x_p/k) + ceil(gridW/k) * floor(y_p/k)
942
- * poolMatrix[cell, p] = 1/k² (so pooled = poolMatrix @ hidden = mean over the k×k block)
943
- * Np = ceil(gridH/k) * ceil(gridW/k). Each pooled cell averages exactly the patches
944
- * that fall in it (edge cells with fewer than k² patches still divide by k², matching
945
- * HF's fixed 1/k² normalization).
946
- */
947
- declare function buildGemma4PoolMatrix(gridH: number, gridW: number, k: number): {
948
- poolMatrix: Float32Array;
949
- numPooled: number;
950
- };
817
+ //#region src/gpu/executor.d.ts
951
818
  /**
952
- * Build all Gemma 4 vision host tensors for one image grid in one call.
953
- * `posEmbedTable` is the raw [2, posSize, hidden] flattened table.
819
+ * Safari/Metal workaround: shader variant alternation.
820
+ * Metal caches argument buffers per compiled function. When consecutive dispatches
821
+ * use the same WGSL code (same Metal function), Metal reuses the previous dispatch's
822
+ * argument buffer, ignoring setBindGroup(). We alternate between variant 0/1 of each
823
+ * shader (prepending `const _MV: u32 = Xu;`) to force different Metal function
824
+ * specializations, preventing argument buffer reuse.
954
825
  */
955
- declare function buildGemma4VisionPositionTensors(gridH: number, gridW: number, posEmbedTable: Float32Array, posSize: number, cfg: Gemma4VisionGridConfig): Gemma4VisionPositionTensors;
956
- /** Gemma 4 image processor config (from processor_config.json). */
957
- declare const GEMMA4_IMAGE_PROCESSOR: ImageProcessorConfig;
958
- interface Gemma4PreprocessedImage {
959
- /** Flattened patches [N, 3·patch²] row-major (row-major patch grid). */
960
- patches: Float32Array;
961
- /** Patch grid (gridH, gridW). */
962
- gridHW: [number, number];
826
+ interface ExecutorOptions {
827
+ maxSeqLen: number;
828
+ /** KV cache kernel strategy. Defaults to "native-f16" when not specified. */
829
+ kvMode?: KvMode;
830
+ /**
831
+ * WebKit only: dispatches per command buffer, with at most one command
832
+ * buffer in flight (awaited). 1 (default) is the proven-correct floor on
833
+ * iPad; larger values are faster if this WebKit version keeps storage
834
+ * writes visible across dispatches within one submission. Sweepable via
835
+ * the ?group=N URL param.
836
+ */
837
+ webkitGroupSize?: number;
963
838
  }
964
- /**
965
- * Preprocess a decoded RGB image for the Gemma 4 ViT: aspect-preserving resize so
966
- * the patch grid is ≤ max_soft_tokens·k² patches and H,W divisible by k·patch,
967
- * rescale ×1/255 (no normalize), patchify row-major into [N, 3·16·16].
968
- *
969
- * @param pixels row-major HWC RGB (0..255), length width*height*3.
970
- */
971
- declare function preprocessImageGemma4(pixels: Float32Array | Uint8ClampedArray | Uint8Array, width: number, height: number, maxSoftTokens?: number, poolingKernelSize?: number, patchSize?: number): Gemma4PreprocessedImage;
972
- interface ImageProcessorConfig {
973
- patchSize: number;
974
- temporalPatchSize: number;
975
- mergeSize: number;
976
- imageMean: [number, number, number];
977
- imageStd: [number, number, number];
978
- /** rescale factor applied to raw 0..255 pixels before normalization (1/255). */
979
- rescaleFactor: number;
980
- /** min total pixels after resize (shortest_edge). */
981
- minPixels: number;
982
- /** max total pixels after resize (longest_edge). */
983
- maxPixels: number;
839
+ interface ForwardResult {
840
+ logits: Float32Array;
984
841
  }
985
- declare const QWEN3_5_IMAGE_PROCESSOR: ImageProcessorConfig;
986
- interface PreprocessedImage {
987
- /** Flattened patches [N, 1536] in the spatial-merge order encodeImage expects. */
988
- patches: Float32Array;
989
- /** (t, h, w) patch grid. t=1 for a single image, h/w in patch units. */
990
- gridTHW: [number, number, number];
842
+ /** One tensor reference of a decode-graph node, with its pool-buffer identity. */
843
+ interface DecodeGraphTensorRef {
844
+ tensor: string;
845
+ /** Declared shape (may contain symbolic dims "T" / "L_max"). */
846
+ shape: Array<number | string>;
847
+ /** Shape resolved at T=1, seqPos = dump position. */
848
+ resolvedShape: number[] | null;
849
+ /** Label of the GPUBuffer this tensor is bound to. Pooled activation
850
+ * buffers are shared by many tensors — equal labels here = aliasing. */
851
+ buffer: string | null;
852
+ bufferBytes: number | null;
853
+ }
854
+ /** One ORIGINAL (pre-fusion) graph node covered by a dispatch entry. */
855
+ interface DecodeGraphNode {
856
+ id: string;
857
+ opType: string;
858
+ inputs: DecodeGraphTensorRef[];
859
+ outputs: DecodeGraphTensorRef[];
860
+ attributes: Record<string, number | string | boolean>;
861
+ }
862
+ /** One entry of the post-fusion decode dispatch list (= one GPU dispatch). */
863
+ interface DecodeGraphEntry {
864
+ index: number;
865
+ nodeId: string;
866
+ /** Anchor node's opType (fused entries report their anchor constituent). */
867
+ opType: string;
868
+ fused: boolean;
869
+ /** Pre-fusion constituent node ids, execution order. Null when unfused. */
870
+ fusedNodeIds: string[] | null;
871
+ /** Storage (non-uniform) bindings the kernel declares. */
872
+ storageBindings: number;
873
+ totalBindings: number;
874
+ dispatchSize: [number, number, number] | null;
875
+ paramsBytes: number;
876
+ /** Uniform params as raw u32 words (f32 fields appear as their bit pattern). */
877
+ paramsU32: number[];
878
+ /** Buffer labels in bind-group order, when captured at entry creation. */
879
+ bindBufferLabels: string[] | null;
880
+ /** The original graph nodes this dispatch covers (1 for unfused entries). */
881
+ nodes: DecodeGraphNode[];
882
+ }
883
+ interface DecodeGraphDump {
884
+ dispatchesPerToken: number;
885
+ unfusedDispatchesPerToken: number;
886
+ maxStorageBuffersPerShaderStage: number;
887
+ /** seqPos the shapes/params were resolved at (T=1 decode step). */
888
+ dumpSeqPos: number;
889
+ /** Every GPU buffer referenced by the decode entries. */
890
+ buffers: Array<{
891
+ label: string;
892
+ bytes: number;
893
+ kind: string;
894
+ }>;
895
+ entries: DecodeGraphEntry[];
991
896
  }
992
- /**
993
- * Qwen2-VL smart-resize: round H and W to multiples of factor=patch*merge,
994
- * keeping aspect ratio and clamping the total pixel budget to [minPixels, maxPixels].
995
- * Matches transformers.models.qwen2_vl.image_processing.smart_resize.
996
- */
997
- declare function smartResize(height: number, width: number, factor: number, minPixels: number, maxPixels: number): [number, number];
998
- /**
999
- * Preprocess a decoded RGB image into the [N, 1536] patch tensor + grid_thw that
1000
- * `encodeImage()` expects, matching the HF Qwen2-VL image processor:
1001
- * smart_resize → rescale (×1/255) → normalize → temporal-pair (×temporal_patch_size)
1002
- * → patchify into spatial_merge×spatial_merge blocks flatten to [N, C·T·P·P].
1003
- *
1004
- * @param pixels row-major HWC RGB, length width*height*3. Values 0..255 (default)
1005
- * or already 0..1 if `rescaled` is true.
1006
- * @param width source pixel width
1007
- * @param height source pixel height
1008
- */
1009
- declare function preprocessImage(pixels: Float32Array | Uint8ClampedArray | Uint8Array, width: number, height: number, cfg?: ImageProcessorConfig, rescaled?: boolean): PreprocessedImage;
1010
- declare function buildMRoPEPositionIds(inputIds: Int32Array | Uint32Array | number[], imageGrids: Array<[number, number, number]>, imageTokenId: number, mergeSize: number): Int32Array;
1011
- /**
1012
- * Per-pair frequency→dimension assignment for interleaved M-RoPE, matching
1013
- * Qwen3_5TextRotaryEmbedding.apply_interleaved_mrope. For pair index i in
1014
- * [0, sum(section)) the position component is section-cyclic: T,H,W,T,H,W,...
1015
- * but each component capped at its section count. Returns an array of length
1016
- * (rope_dim/2) with values 0=T, 1=H, 2=W.
1017
- */
1018
- declare function mropeFreqDims(mropeSection: [number, number, number]): Int32Array;
1019
- /**
1020
- * Build the interleaved-M-RoPE cos/sin tables [seq, rope_dim] from 3D position
1021
- * ids, matching Qwen3_5TextRotaryEmbedding.forward:
1022
- * freqs[d][i] = pos[d] * inv_freq[i], inv_freq[i] = 1/theta^(2i/rope_dim)
1023
- * freq[i] picks component mropeFreqDims[i]; emb = cat(freqs, freqs).
1024
- * cos/sin have length seq*rope_dim. For text-only (all 3 pos rows equal) this
1025
- * reduces exactly to standard 1D partial RoPE.
1026
- *
1027
- * @param positionIds3 [3, seq] as produced by buildMRoPEPositionIds.
1028
- * @param ropeDim number of rotated dims per head (head_dim * partial_factor).
1029
- */
1030
- declare function buildMRoPECosSin(positionIds3: Int32Array, seq: number, ropeDim: number, theta: number, mropeSection: [number, number, number]): {
1031
- cos: Float32Array;
1032
- sin: Float32Array;
1033
- };
1034
- //#endregion
1035
- //#region src/gpu/defaults.d.ts
1036
- /**
1037
- * Default model per capability. Kept in its own tiny module (no heavy imports)
1038
- * so the React hooks can resolve defaults without statically pulling in the GPU
1039
- * engine they import the engine dynamically to stay light.
1040
- */
1041
- declare const DEFAULT_MODELS: {
1042
- /** Text generation (also the vision-capable checkpoint). */
1043
- readonly text: "mlx-community/Qwen3.5-0.8B-4bit";
1044
- /** Image understanding same checkpoint, vision tower built on demand. */
1045
- readonly vision: "mlx-community/Qwen3.5-0.8B-4bit";
1046
- /** Text embeddings. */
1047
- readonly embedding: "mlx-community/embeddinggemma-300m-4bit";
1048
- /** Text-to-speech (Apache-2.0; LFM2-350M backbone + NanoCodec). Small/fast default. */
1049
- readonly tts: "nineninesix/kani-tts-450m-0.2-ft";
1050
- /**
1051
- * Selectable many-voices TTS (OuteTTS-1.0-0.6B: Qwen3-0.6B codec-LM + DAC.speech).
1052
- * Pass this as the `repo`/`model` to engine.speak()/useTTS to route to OuteTTS.
1053
- */
1054
- readonly ttsOute: "OuteAI/OuteTTS-1.0-0.6B";
897
+ declare class Executor {
898
+ private ctx;
899
+ private graph;
900
+ /** Output tensor names of this executor's graph (read-only; used by non-LM
901
+ * codec executors that need to locate the decoded-audio output tensor). */
902
+ get graphOutputs(): string[];
903
+ private weightBuffers;
904
+ private activationBuffers;
905
+ private ssmStateBuffers;
906
+ private kvCacheBuffers;
907
+ /** Pre-allocated input_ids buffer (maxSeqLen * 4 bytes). */
908
+ private inputIdsBuffer;
909
+ /**
910
+ * CPU-resident Per-Layer-Embeddings (PLE) source for Gemma 4. The PLE table
911
+ * (`embed_tokens_per_layer`, [vocab, L*256]) is ~1.17GB at 4-bit and is kept
912
+ * OFF the GPU. Each forward step we gather + dequantize only the rows for the
913
+ * actual input tokens and upload a tiny [T, L*256] f32 buffer. See setPleSource.
914
+ */
915
+ private pleSource;
916
+ /** Reusable scratch for the dequantized PLE rows (resized on demand). */
917
+ private pleScratch;
918
+ /**
919
+ * One-time promise that materializes a cache-backed PLE table into heap. The
920
+ * table is read from CacheStorage on the FIRST forward i.e. AFTER the GPU
921
+ * weight upload has completed, so the ~1.17 GB does not stack on top of the
922
+ * upload's transient allocations at the load-time memory high-water mark.
923
+ */
924
+ private pleReady;
925
+ /**
926
+ * Dummy GPU buffer bound to an otherwise-aliasing storage-read-write slot.
927
+ * Used by RoPE-Q-only nodes (Gemma 4 KV-shared layers) whose node lists the
928
+ * same tensor as input and output: the RoPE kernel always declares two
929
+ * read_write bindings (q, k), but with num_kv_heads=0 the k slot is never
930
+ * touched. WebGPU still rejects two read_write bindings aliasing one buffer,
931
+ * so we bind this throwaway buffer to the unused k slot. Allocated lazily.
932
+ */
933
+ private bindingScratchBuffer;
934
+ /** Readback buffer for logits. */
935
+ private logitsReadback;
936
+ /** GPU buffer for argmax result (1 u32). */
937
+ private argmaxResultBuffer;
938
+ /** Readback buffer for argmax result (1 u32). */
939
+ private argmaxReadback;
940
+ /** Readback ring for pipelined greedy decode (created lazily). */
941
+ private decodeReadbacks;
942
+ /** Window size K (GERBIL_DECODE_WINDOW). 0 = off. */
943
+ readonly decodeWindowK: number;
944
+ /** Readback ring for decode windows (4·K bytes per slot; created lazily). */
945
+ private windowReadbacks;
946
+ /**
947
+ * Per-step uniform/bind-group variants for the few seq-pos-dependent decode
948
+ * entries (RoPE position, KV-append offset, attention S). Keyed by nodeId;
949
+ * index k holds the variant used at window step k (1..K-1 — step 0 uses the
950
+ * entry's base uniform). Created lazily, rewritten per window pre-submit.
951
+ */
952
+ private windowVariants;
953
+ /**
954
+ * Staging buffer for uniform param updates.
955
+ * Safari/Metal has weaker visibility guarantees for queue.writeBuffer() to
956
+ * UNIFORM buffers early writes get dropped when hundreds are queued.
957
+ * Instead, we pack all params into this STORAGE staging buffer (1 writeBuffer),
958
+ * then use encoder.copyBufferToBuffer to distribute to each uniform buffer.
959
+ * Copies are GPU-sequenced and guaranteed to complete before compute passes.
960
+ */
961
+ private uniformStagingBuffer;
962
+ private uniformStagingCapacity;
963
+ /** Dispatch entries for prefill (M>1): uses tiled matmul kernels. */
964
+ private dispatchEntries;
965
+ /** Dispatch entries for decode (M=1): uses K-parallel matvec kernels. */
966
+ private decodeEntries;
967
+ /**
968
+ * Column-concatenated INT4 weight buffers built by the Mamba-front fusion
969
+ * (qkv|a|b|z packed into one q/scales/zeros set per layer). Retained here so
970
+ * they outlive the fusion pass and are not garbage-collected while the fused
971
+ * bind groups still reference them.
972
+ */
973
+ private mambaFrontConcatBuffers;
974
+ /** Per-node decode entries captured BEFORE fusion (the overlay base). */
975
+ private decodeEntriesUnfused;
976
+ /** True while a runtime adapter is applied. */
977
+ private loraActive;
978
+ /** Decode entries (unfused base + spliced LoRA dispatches). */
979
+ private decodeEntriesLoRA;
980
+ /** Prefill entries (base + spliced LoRA dispatches). */
981
+ private prefillEntriesLoRA;
982
+ /** GPU buffers owned by the current adapter (A/B factors + tmp); freed on swap. */
983
+ private loraBuffers;
984
+ /** Argmax dispatch entry (created in initBindGroups). */
985
+ private argmaxEntry;
986
+ /** True when running on Safari/WebKit (needs multi-encoder submit). */
987
+ readonly needsMultiEncoder: boolean;
988
+ private maxSeqLen;
989
+ private kvMode;
990
+ private seqPos;
991
+ private webkitGroupSize;
992
+ private readonly pagedKV;
993
+ private kvBlockManager;
994
+ /** Lockstep batch width (GERBIL_BATCH). 0 = batching off. */
995
+ readonly batchSize: number;
996
+ /** Block-table row stride — set for any paged mode. */
997
+ private maxBlocksPerSeq;
998
+ /** Block-table row the single-sequence paths target (batch prefill slot). */
999
+ private activeSeqSlot;
1000
+ /** Batched dispatch entries (built once in initBindGroups when batch on). */
1001
+ private batchEntries;
1002
+ /** Per-slot SSM/conv state pools: slot b's state at offset b*slotBytes. */
1003
+ private batchSsmPools;
1004
+ /** Batch-widened tail buffers (SliceLastRow output + logits at [N, ...]). */
1005
+ private batchTailBuffers;
1006
+ private batchPositionsBuffer;
1007
+ private batchSeqLensBuffer;
1008
+ private batchSampledBuffer;
1009
+ private batchReadbacks;
1010
+ private batchArgmaxEntry;
1011
+ /** JS-side authoritative per-slot token counts (positions source). */
1012
+ private batchSeqLens;
1013
+ /** Scratch for per-step positions/seq_lens uploads. */
1014
+ private batchPositionsScratch;
1015
+ private batchSeqLensScratch;
1016
+ /** Cached batch-resolved shapes (static: T == batchSize every step). */
1017
+ private batchShapes;
1018
+ /** True once the batched-argmax uniform has been written this session. */
1019
+ private batchArgmaxParamsWritten;
1020
+ /** True when GERBIL_BATCH_LORA=1 rode in on an active batch path. */
1021
+ readonly batchLoraEnabled: boolean;
1022
+ /** Registered adapters (expanded deltas), indexed by adapter id. */
1023
+ private batchAdapterDeltas;
1024
+ /** Per-lane adapter ids (u32; 0xFFFFFFFF = bare base). */
1025
+ private batchAdapterIdsBuffer;
1026
+ /** GPU buffers owned by the batch-LoRA overlay (factors, maps, tmp, uniforms). */
1027
+ private batchLoraBuffers;
1028
+ /** Batch entries snapshotted BEFORE the batch fusion passes (overlay base). */
1029
+ private batchEntriesUnfused;
1030
+ /** Batch entries with the per-lane LoRA dispatches spliced in. */
1031
+ private batchEntriesLoRA;
1032
+ /** True while at least one lane carries an adapter (set by setBatchLaneAdapters). */
1033
+ private batchLoraActive;
1034
+ private profileEnabled;
1035
+ private readonly profileData;
1036
+ private querySet;
1037
+ private queryResolveBuf;
1038
+ private queryReadbackBuf;
1039
+ constructor(ctx: GPUContext, graph: ModelGraph, options: ExecutorOptions);
1040
+ /**
1041
+ * Register the CPU-resident Gemma 4 PLE table. The quantized table is kept in
1042
+ * JS memory (NOT uploaded to a GPU buffer), and {@link forward} gathers +
1043
+ * dequantizes only the rows for the current input tokens each step, uploading a
1044
+ * small [T, width] f32 buffer into `targetTensor`. This is what keeps Gemma 4
1045
+ * mobile-viable: resident GPU memory is just the active transformer weights.
1046
+ */
1047
+ setPleSource(src: {
1048
+ packed: Uint32Array;
1049
+ scales: Float32Array;
1050
+ zeros: Float32Array;
1051
+ width: number;
1052
+ groupSize: number;
1053
+ targetTensor: string;
1054
+ cache?: {
1055
+ cacheName: string;
1056
+ packedKey: string;
1057
+ scalesKey: string;
1058
+ zerosKey: string;
1059
+ packedLen: number;
1060
+ };
1061
+ }): void;
1062
+ /**
1063
+ * True when this model streams a CPU-resident Per-Layer-Embedding table
1064
+ * (Gemma 4): each decode step must gather+upload the PLE rows for the ACTUAL
1065
+ * input token id on the CPU before the forward runs. The GPU-chained pipelined
1066
+ * decode path and forwardArgmax both bypass CPU token knowledge, so they leave
1067
+ * `ple_embed_out` stale and must NOT be used for these models — only the
1068
+ * per-step forward() path (which calls streamPleRows) is correct. See generate().
1069
+ */
1070
+ hasPleSource(): boolean;
1071
+ /**
1072
+ * Materialize a cache-backed PLE table into heap exactly once. Deferred to the
1073
+ * first forward (after GPU upload) so the ~1.17 GB table is not co-resident
1074
+ * with the upload's transient allocations during the load-time memory peak.
1075
+ */
1076
+ private ensurePleLoaded;
1077
+ /**
1078
+ * Gather + dequantize the PLE rows for `inputIds` and upload them into the
1079
+ * target activation buffer. Touches only T rows (T*width floats) — the full
1080
+ * [vocab, width] quantized table never goes to the GPU.
1081
+ */
1082
+ private streamPleRows;
1083
+ /**
1084
+ * Stream weights to the GPU one tensor at a time, pulling each from the
1085
+ * `WeightSource` only when it is about to be uploaded and dropping the
1086
+ * reference immediately afterward.
1087
+ *
1088
+ * This is the property that bounds peak JS heap: with a cache-backed source
1089
+ * (browser/mobile), only ONE tensor's bytes are materialized in heap at a time
1090
+ * (read from CacheStorage in `source.get()`), uploaded to its GPU buffer, then
1091
+ * released before the next tensor is fetched. The whole model is never co-
1092
+ * resident in heap. With a heap-backed source (Node/desktop), behavior matches
1093
+ * the old Map path (the source deletes each entry as it is consumed).
1094
+ *
1095
+ * Accepts either a `WeightSource` (new, streamed/async) or a plain Map
1096
+ * (back-compat for callers that build a Map directly, e.g. Kani/Moonshine).
1097
+ */
1098
+ uploadWeights(source: WeightSource | Map<string, {
1099
+ data: ArrayBufferView;
1100
+ shape: number[];
1101
+ }>): Promise<void>;
1102
+ /**
1103
+ * Synchronous Map upload (Node/desktop and the Kani/Moonshine sub-executors,
1104
+ * which build small heap Maps). Deletes each entry as it is consumed to free
1105
+ * the JS-side bytes once they are GPU-resident. Safe to call from a constructor.
1106
+ */
1107
+ uploadWeightsMap(weights: Map<string, {
1108
+ data: ArrayBufferView;
1109
+ shape: number[];
1110
+ }>): void;
1111
+ /**
1112
+ * Build all pipelines and bind groups. Call ONCE after uploadWeights().
1113
+ *
1114
+ * Creates two dispatch entry arrays:
1115
+ * - dispatchEntries: tiled matmul for prefill (any M)
1116
+ * - decodeEntries: K-parallel matvec for decode (M=1)
1117
+ */
1118
+ initBindGroups(): void;
1119
+ /** True while a runtime LoRA overlay is applied. */
1120
+ get hasRuntimeAdapter(): boolean;
1121
+ /** Decode entries in effect for the current adapter state. */
1122
+ private get activeDecodeEntries();
1123
+ /**
1124
+ * Apply a runtime LoRA overlay on the STATIC base. For each delta whose target
1125
+ * maps to a base linear (MatMulInt4 / MatMul) with matching dims, uploads the
1126
+ * f32 A/B factors and splices two low-rank dispatches — `tmp = A·x`, then
1127
+ * `y += scaling·B·tmp` — right after that node in both the decode and prefill
1128
+ * paths, so the corrected output is live before any consumer reads it.
1129
+ *
1130
+ * Rebuilds the overlay from scratch each call (this is how adapters hot-swap);
1131
+ * the base weight buffers and bind groups are never touched or re-uploaded.
1132
+ * Pass an empty list (or call {@link clearRuntimeLoRA}) to drop the overlay.
1133
+ */
1134
+ /** Find the base linear (MatMulInt4 / MatMul) node whose weight is `key`. */
1135
+ private findBaseLinear;
1136
+ /**
1137
+ * Expand any fused query+gate delta into separate `q_proj` + `attn_gate`
1138
+ * deltas so it can splice onto a deinterleaved runtime graph.
1139
+ *
1140
+ * Qwen3.5-style gated attention ships `q_proj.weight` fused as
1141
+ * `[2·num_heads·head_dim, hidden]` (per-head: query rows then gate rows); the
1142
+ * model loader splits it into two runtime linears (`q_proj`, `attn_gate`). An
1143
+ * adapter trained on the fused checkpoint carries a single `q_proj` delta whose
1144
+ * out-dim is 2× either split node, so it matches neither. When BOTH split
1145
+ * linears exist and the delta is exactly their combined width, split it via the
1146
+ * same per-head row deinterleave the loader uses; otherwise the delta passes
1147
+ * through untouched (e.g. a base that never split the projection).
1148
+ */
1149
+ private expandFusedQGateDeltas;
1150
+ applyRuntimeLoRA(deltas: LoRADelta[]): {
1151
+ applied: number;
1152
+ skipped: number;
1153
+ };
1154
+ /** Drop the runtime LoRA overlay and free its GPU buffers. Base is untouched. */
1155
+ clearRuntimeLoRA(): void;
1156
+ /** Build the [down, up] low-rank dispatch pair for one adapted node. */
1157
+ private buildLoraPair;
1158
+ /** Copy a base entry list, splicing each node's LoRA pair in right after it. */
1159
+ private spliceLoraOverlay;
1160
+ /** Original constituent node ids a (possibly fused) decode entry covers. */
1161
+ private coveredNodeIds;
1162
+ /**
1163
+ * Build the decode entry list for an active adapter with SELECTIVE unfusing.
1164
+ * Every fused group that covers no adapted linear is kept exactly as the
1165
+ * no-adapter fast path runs it (full SwiGLU/dual/gated fusion, no numerics
1166
+ * change). Only the groups that cover an adapted node fall back to their
1167
+ * pre-fusion per-node dispatches (pulled from {@link decodeEntriesUnfused}),
1168
+ * with the low-rank `[down, up]` pair spliced right after each adapted node so
1169
+ * its corrected output is live before any consumer reads it. Replaying the
1170
+ * exact pre-fusion entries reproduces the validated unfused numerics for the
1171
+ * adapted groups while leaving all other groups on the fused path.
1172
+ */
1173
+ private buildSelectiveLoraDecode;
1174
+ /**
1175
+ * Run a forward pass. Uses matvec kernels for M=1 (decode), tiled for M>1 (prefill).
1176
+ *
1177
+ * `opts.readLogits: false` skips the logits copy + readback entirely (the
1178
+ * returned logits array is empty) and returns right after submission — used
1179
+ * by the scheduler's chunked prefill, where only the FINAL chunk's logits
1180
+ * are ever sampled. seqPos still advances by T.
1181
+ */
1182
+ forward(inputIds: Uint32Array, opts?: {
1183
+ readLogits?: boolean;
1184
+ }): Promise<ForwardResult>;
1185
+ /**
1186
+ * Profiling variant of the desktop dispatch path: one compute pass per dispatch,
1187
+ * each bracketed by timestamp queries, so we get per-op GPU time. Accumulates
1188
+ * into profileData by opType. Only runs under GERBIL_PROFILE with timestamp-query
1189
+ * support — slower than the batched path (it measures relative cost, not tok/s).
1190
+ */
1191
+ private runProfiledDispatches;
1192
+ /** Per-opType GPU time (ns) + dispatch count accumulated by GERBIL_PROFILE, hottest first. */
1193
+ getProfile(): Array<{
1194
+ opType: string;
1195
+ ns: number;
1196
+ count: number;
1197
+ }>;
1198
+ /** Clear accumulated profiler data (e.g. to drop warm-up tokens). */
1199
+ resetProfile(): void;
1200
+ /** GPU dispatches per decode token (post-fusion). On mobile this drives the
1201
+ * submit-group count = ceil(dispatchCount / webkitGroupSize). */
1202
+ get decodeDispatchCount(): number;
1203
+ /** Device limit that gates the INT4 projection fusions (they need ≥9). If a
1204
+ * device caps at 8 the dual/gated/swiglu-gated INT4 fusions silently fall back,
1205
+ * inflating the decode dispatch count. */
1206
+ get maxStorageBuffers(): number;
1207
+ /**
1208
+ * Build a machine-readable dump of the post-fusion decode entry list (the
1209
+ * exact per-token dispatch sequence), with per-tensor pool-buffer identities
1210
+ * so pooled-aliasing hazards are visible offline. Debug/analysis only — never
1211
+ * called on any hot path; reached via the GERBIL_DUMP_GRAPH hook at the end
1212
+ * of {@link initBindGroups} or directly by tooling.
1213
+ */
1214
+ dumpDecodeGraph(dumpSeqPos?: number): DecodeGraphDump;
1215
+ /**
1216
+ * Profile ONE real decode step: times the actual `decodeEntries` (the kernels
1217
+ * the pipelined greedy benchmark runs) with per-dispatch timestamps. Timing is
1218
+ * token-independent, so pass any valid id; runs un-pipelined with a synchronous
1219
+ * timestamp readback (measurement only, not for production decode). Argmax (one
1220
+ * tiny dispatch) is intentionally excluded — it is not a hotspot target.
1221
+ */
1222
+ profileDecodeStep(tokenId: number): Promise<void>;
1223
+ /**
1224
+ * Profile ONE batched decode step (GERBIL_BATCH): times each batchEntries
1225
+ * dispatch with timestamp queries, accumulating GPU ns per opType into the
1226
+ * same profile as profileDecodeStep. Measurement-only (un-pipelined, CPU
1227
+ * tokens). Call after prefilling the batch slots; advances every slot by
1228
+ * one position like a real step.
1229
+ */
1230
+ profileBatchDecodeStep(tokens: Uint32Array<ArrayBuffer>): Promise<void>;
1231
+ /**
1232
+ * Run a single forward pass over `inputIds` and read back the L2-normalized
1233
+ * embedding vector. Requires an embedding graph (one whose output tensor is
1234
+ * "embedding", produced by the last-token-pool + L2-norm tail).
1235
+ *
1236
+ * Always runs in a fresh-state single pass (caller should reset() first):
1237
+ * embeddings are non-autoregressive, so the whole sequence is one prefill.
1238
+ */
1239
+ embed(inputIds: Uint32Array): Promise<Float32Array>;
1240
+ /**
1241
+ * Generic one-shot dispatch for a non-LM graph: run every entry once over a
1242
+ * single forward and read back `elemCount` f32 elements of the named output
1243
+ * tensor. Used to execute the NanoCodec decoder graph (codes→PCM) — its ops use
1244
+ * concrete lengths and it has no "logits" output, so the normal forward()
1245
+ * logits-readback path does not apply. Caller writes any inputs (e.g. audio_codes)
1246
+ * via writeInput() and reset()s first.
1247
+ */
1248
+ runGraphOutput(outputName: string, elemCount: number): Promise<Float32Array>;
1055
1249
  /**
1056
- * Selectable describe-your-voice TTS (Parler-TTS-mini-v1: Flan-T5 encoder +
1057
- * cross-attending decoder LM + dac_44khz). Pass this as the `repo`/`model` to
1058
- * engine.speak()/useTTS with a `describeVoice` natural-language description.
1250
+ * Autoregressive step for a multi-output decoder graph (e.g. Parler-TTS's 9 codebook
1251
+ * lm_heads). Dispatches the whole graph ONCE at the current seqPos (so the per-layer
1252
+ * KVCacheAppend writes at the right cache row), reads back each named output, then
1253
+ * advances seqPos by `tokens` (default 1, for a prefilled prompt span pass tokens=T).
1254
+ * The caller writes inputs (dec_input_embed + frozen cross-attn K/V) and reset()s
1255
+ * before the first step. Unlike forward(), there is no single "logits" output —
1256
+ * the caller decodes each codebook row itself.
1059
1257
  */
1060
- readonly ttsParler: "parler-tts/parler-tts-mini-v1";
1061
- /** Speech-to-text. */
1062
- readonly stt: "UsefulSensors/moonshine-base";
1063
- };
1064
- declare const OUTETTS_ASSETS: {
1065
- /** Folded DAC.speech.v1.0 encoder+decoder weights (prep-dac-speech.py output). */
1066
- readonly codecWeightsURL: "https://pub-0522ecec25fc412bb35a523f3e3f63c6.r2.dev/tryhamster/gerbil-outetts-assets/dac-speech-folded.safetensors";
1067
- /** Base URL for the preset speaker JSONs (`{name}.json`). */
1068
- readonly speakersBaseURL: "https://pub-0522ecec25fc412bb35a523f3e3f63c6.r2.dev/tryhamster/gerbil-outetts-assets/speakers";
1069
- /** The default preset voice. */
1070
- readonly defaultVoice: "en-female-1-neutral";
1071
- };
1072
- /**
1073
- * Bundled OuteTTS preset voices.
1074
- *
1075
- * Provenance (honest labelling):
1076
- * - `en-female-1-neutral` is the ONE AUTHENTIC OuteAI voice — the OuteTTS v3 bundled
1077
- * default speaker (default_speakers/en-female-1-neutral), copied verbatim.
1078
- * - `gerbil-voice-2..6` are DERIVED voices: distinct timbres produced by running
1079
- * pitch/tempo-varied renderings of that authentic clip back through the REAL
1080
- * DAC.speech encode + prosody pipeline (scripts/engine/stage-outetts-assets.py).
1081
- * They are genuine DAC-encoded OuteSpeakers, but they are NOT OuteAI's own
1082
- * `en-male-*` voices, so they are not labelled as such. (Earlier builds mislabelled
1083
- * them as `en-male-2-neutral` etc.; renamed for honesty.)
1084
- */
1085
- declare const OUTETTS_PRESET_VOICES: readonly ["en-female-1-neutral", "gerbil-voice-2", "gerbil-voice-3", "gerbil-voice-4", "gerbil-voice-5", "gerbil-voice-6"];
1086
- /**
1087
- * True when `repo`/`architecture` identify a text-to-speech checkpoint that must
1088
- * run through a dedicated native TTS engine (KaniTTS, OuteTTS, or Parler-TTS)
1089
- * rather than the single-graph base text loader. Mirrors the routing used by
1090
- * `WebGPUEngine.speak()` / `Gerbil.ensureNativeTTSEngine`:
1091
- * - Parler → encoder-decoder (`ParlerTTSForConditionalGeneration`)
1092
- * - OuteTTS → Qwen3-0.6B codec-LM
1093
- * - Kani → conv-backbone codec-LM (`KaniTTS2ForCausalLM`, or the Apache
1094
- * default which is a plain `Lfm2ForCausalLM` — hence the repo-name
1095
- * gate; the architecture string alone does not identify it).
1096
- * At `create()` time only the repo is known (the architecture is discovered
1097
- * during load), so the repo-name signals below cover all three DEFAULT_MODELS
1098
- * and conventionally-named TTS repos; the architecture arg is honored when a
1099
- * caller already has it.
1100
- */
1101
- declare function isTTSRepo(repo?: string, architecture?: string): boolean;
1102
- /** Resolve the model repo for a set of options, falling back to the defaults. */
1103
- declare function resolveDefaultRepo(opts: {
1104
- repo?: string;
1105
- embedding?: boolean;
1106
- enableVision?: boolean;
1107
- }): string;
1108
- //#endregion
1109
- //#region src/gpu/architectures/gemma4_vision.d.ts
1110
- interface Gemma4VisionGraphInfo {
1111
- hiddenSize: number;
1112
- numHeads: number;
1113
- headDim: number;
1114
- depth: number;
1115
- intermediateSize: number;
1116
- textHidden: number;
1117
- patchSize: number;
1118
- patchDim: number;
1119
- poolingKernelSize: number;
1120
- ropeTheta: number;
1121
- rmsNormEps: number;
1122
- }
1123
- /**
1124
- * Resolve the Gemma 4 vision dims from a raw HF config. Accepts either the
1125
- * top-level config (reads `.vision_config` + `.text_config.hidden_size`) or a
1126
- * bare vision_config (then `textHidden` falls back to the projector row count if
1127
- * present, else hidden). Family-general — no E2B constants.
1128
- */
1129
- declare function resolveGemma4VisionInfo(rawConfig: Record<string, unknown>): Gemma4VisionGraphInfo;
1130
- /**
1131
- * Dequantize an MLX affine-int4 weight to a plain f32 [rows, cols] matrix.
1132
- * MLX packs 8 int4 values per u32 (low-nibble first); each group of `groupSize`
1133
- * columns shares one scale + bias: w[r,c] = scale[r, c/gs] * q + bias[r, c/gs].
1134
- * Used for the Gemma 4 multimodal projector (`embed_vision.embedding_projection`)
1135
- * in MLX-4bit checkpoints, where (unlike the BF16 ViT body) the projector is int4.
1136
- */
1137
- declare function dequantizeMLXProjection(packed: Uint32Array, scales: Float32Array, biases: Float32Array, rows: number, cols: number, groupSize: number): Float32Array;
1138
- /**
1139
- * If the Gemma 4 multimodal projector arrived as an MLX affine-int4 triplet
1140
- * (`embed_vision.embedding_projection.{weight(U32), scales, biases}`), dequantize
1141
- * it in-place to a plain f32 `embed_vision.embedding_projection.weight` and drop
1142
- * the scales/biases, so the vision graph's plain MatMul on the projector works for
1143
- * MLX-4bit checkpoints too. No-op for BF16 (HF) checkpoints (weight already f32).
1144
- */
1145
- declare function dequantizeGemma4VisionProjection(weights: Map<string, {
1146
- data: ArrayBufferView;
1147
- shape: number[];
1148
- }>, groupSize: number, rows: number, cols: number): void;
1149
- /**
1150
- * Patch the ClippedMatMul nodes of a Gemma 4 vision graph with the calibrated clip
1151
- * scalars from the checkpoint (Gemma4ClippableLinear's per-tensor input/output
1152
- * min/max buffers), then drop those scalar tensors from the weights map so the
1153
- * vision executor doesn't try to upload them as GPU buffers. Call BEFORE
1154
- * VisionExecutor.uploadWeights(). Missing scalars default to ±inf (clip = identity),
1155
- * so a checkpoint without calibration still loads.
1156
- */
1157
- declare function patchGemma4VisionClips(graph: ModelGraph, weights: Map<string, {
1158
- data: ArrayBufferView;
1159
- shape: number[];
1160
- }>): void;
1161
- /**
1162
- * Build the Gemma 4 ViT graph. Shaped by symbolic "N" (number of patches, runtime)
1163
- * and "Np" (number of pooled tokens = ceil(grid_h/k)·ceil(grid_w/k), runtime),
1164
- * resolved from input tensor dims — like the Qwen ViT's "N"/"Nm".
1165
- */
1166
- declare function generateGemma4VisionGraph(rawConfig: Record<string, unknown>): ModelGraph;
1167
- //#endregion
1168
- //#region src/gpu/architectures/kani_tts.d.ts
1169
- /** Parsed KaniTTS2 backbone config (the LFM2 dims + the TTS-specific fields). */
1170
- interface KaniConfig {
1171
- textVocabSize: number;
1172
- vocabSize: number;
1173
- tokensPerFrame: number;
1174
- audioStep: number;
1175
- useLearnableRope: boolean;
1176
- alphaMin: number;
1177
- alphaMax: number;
1178
- speakerEmbDim: number;
1179
- audioTokensStart: number;
1180
- startOfSpeech: number;
1181
- endOfSpeech: number;
1182
- codebookSize: number;
1258
+ runGraphStep(outputs: Array<{
1259
+ name: string;
1260
+ elemCount: number;
1261
+ }>, tokens?: number): Promise<Float32Array[]>;
1183
1262
  /**
1184
- * Whether the backbone uses KaniTTS2's frame-level position IDs (the 4 audio
1185
- * tokens of a frame share one logical position) + learnable per-layer RoPE.
1186
- * The `KaniTTS2ForCausalLM` checkpoints (e.g. kani-tts-2-en) do; the plain
1187
- * `Lfm2ForCausalLM` family (e.g. the Apache kani-tts-450m-0.2-ft / kani-tts-370m)
1188
- * does NOT — those run standard sequential positions with α=1 plain RoPE.
1189
- * Detected from the presence of `use_learnable_rope`/`tokens_per_frame`/the
1190
- * `KaniTTS2ForCausalLM` architecture marker in config.json.
1263
+ * Greedy decode step: forward + GPU argmax. Returns token ID directly.
1264
+ * Always uses matvec kernels (M=1). Reads back 4 bytes instead of vocab_size*4.
1191
1265
  */
1192
- frameLevelPositions: boolean;
1193
- }
1194
- declare function parseKaniConfig(rawConfig: Record<string, unknown>): KaniConfig;
1195
- /**
1196
- * Convert the model's flat audio-token stream (the IDs between start/end-of-speech)
1197
- * into NanoCodec codes [NUM_GROUPS, T]. Mirrors NemoAudioPlayer.get_nano_codes:
1198
- * reshape [-1, 4]; codes[:,c] -= codebook_size*c; codes -= audio_tokens_start;
1199
- * transpose → [4, T]. Returns a Uint32Array laid out group-major ([g*T + t]).
1200
- */
1201
- declare function audioTokensToCodes(audioTokenIds: number[], cfg?: KaniConfig): {
1202
- codes: Uint32Array;
1203
- numFrames: number;
1204
- };
1205
- interface NanoCodecGraphOptions {
1206
- /** Number of audio frames T (the code grid width). PCM length = T * 1764. */
1207
- numFrames: number;
1208
- }
1209
- declare function generateNanoCodecDecoderGraph(opts: NanoCodecGraphOptions): ModelGraph;
1210
- /**
1211
- * Generate the KaniTTS codec-LM backbone graph (LFM2-350M body, full-vocab logits,
1212
- * per-layer learnable MRoPE). Mirrors generateLfm2Graph block-for-block; the only
1213
- * difference is that each attention layer rotates Q/K with the MRoPE op fed a
1214
- * per-layer host cos/sin table instead of the position-counter RoPE op.
1215
- *
1216
- * dtype:
1217
- * • undefined / "f32" — full precision (the checkpoint is bf16/f32 → f32).
1218
- * • "q4" — weight-only group-wise INT4 (W4A16) for the LFM2 linear projections
1219
- * (q/k/v/o, gate/up/down, short-conv in/out_proj) via MatMulInt4. The tied
1220
- * embedding + LM head stay at f32 on purpose: decoder-LM heads are the most
1221
- * quantization-sensitive part of a codec-LM and the embed table is the single
1222
- * buffer shared by both, so quantizing it would degrade audio quality for no
1223
- * load-size win that the int4 projections do not already capture. Backbone
1224
- * drops from ~1.5 GB (f32) to ~0.37 GB. The NanoCodec decoder is a separate
1225
- * graph and always runs f32.
1226
- */
1227
- declare function generateKaniTtsGraph(rawConfig: Record<string, unknown>, dtype?: GraphDType, groupSize?: number, kvDtype?: KVDType): ModelGraph;
1228
- //#endregion
1229
- //#region src/gpu/architectures/moonshine.d.ts
1230
- /**
1231
- * Historical record of the executor-side work the keystone left open; all items
1232
- * are now implemented (see the STATUS note above). Retained as exported
1233
- * documentation of the dependency surface.
1234
- */
1235
- declare const MOONSHINE_REMAINING_WORK: readonly ["DONE: Transpose op kernel (conv output [C,L] → encoder input [L,C]).", "DONE: GroupNorm(num_groups=1) over conv channels (weight+bias).", "DONE: Tanh elementwise op (conv1 activation).", "DONE: Interleaved-RoPE variant (Moonshine rotates adjacent dim pairs 2p/2p+1).", "DONE: No-bias LayerNorm variant (Moonshine norms are weight-only).", "DONE: Per-utterance conv graph regeneration from the concrete sample count.", "DONE: Dual-graph executor — encoder once → frozen cross-attn K/V → AR decode.", "DONE: engine.transcribe(pcm) host path (MoonshineSTT): conv → encode → AR decode → detokenize."];
1236
- interface MoonshineDims {
1237
- hidden_size: number;
1238
- enc_layers: number;
1239
- dec_layers: number;
1240
- num_heads: number;
1241
- num_kv_heads: number;
1242
- head_dim: number;
1243
- rotary_dim: number;
1244
- intermediate_size: number;
1245
- vocab_size: number;
1246
- rope_base: number;
1247
- context_length: number;
1248
- ln_eps: number;
1249
- }
1250
- /** Pull and derive Moonshine dimensions from the raw HF config. */
1251
- declare function parseMoonshineConfig(raw: Record<string, unknown>): MoonshineDims;
1252
- /** Number of encoder frames produced by the conv frontend for n_samples PCM. */
1253
- declare function moonshineEncoderFrames(nSamples: number): number;
1254
- /**
1255
- * Encoder graph: raw-waveform conv frontend + bidirectional transformer.
1256
- * @param nSamples concrete PCM sample count (the conv frontend is length-static).
1257
- * The output tensor "encoder_out" is [T_frames, hidden] and is consumed (after
1258
- * per-layer K/V projection) as the frozen K/V for the decoder's cross-attention.
1259
- */
1260
- declare function generateMoonshineEncoderGraph(raw: Record<string, unknown>, nSamples: number): ModelGraph;
1261
- /**
1262
- * Decoder graph: AR transformer with causal self-attn (KV-cache) + cross-attn to
1263
- * the frozen encoder output. Built for a single decode step (T=1). The encoder K/V
1264
- * are supplied as graph inputs "enc_k_layer{i}" / "enc_v_layer{i}" — the host
1265
- * pre-projects the encoder output through each layer's encoder_attn.k_proj/v_proj
1266
- * ONCE and binds them frozen for the whole decode (the CrossAttention contract).
1267
- */
1268
- declare function generateMoonshineDecoderGraph(raw: Record<string, unknown>, sEnc: number): ModelGraph;
1269
- //#endregion
1270
- //#region src/gpu/architectures/outetts.d.ts
1271
- /** Parsed OuteTTS backbone config (the Qwen3 dims). Reuses the generic Qwen path. */
1272
- interface OuteTtsConfig {
1273
- hidden_size: number;
1274
- num_layers: number;
1275
- num_heads: number;
1276
- num_kv_heads: number;
1277
- head_dim: number;
1278
- intermediate_size: number;
1279
- vocab_size: number;
1280
- rope_base: number;
1281
- rms_norm_eps: number;
1282
- max_position_embeddings: number;
1283
- }
1284
- declare function parseOuteTtsConfig(rawConfig: Record<string, unknown>): OuteTtsConfig;
1285
- /**
1286
- * Convert the flat generated token stream (the run BETWEEN <|audio_start|> and
1287
- * <|audio_end|>) into a DAC code grid [NUM_CODEBOOKS, T] (codebook-major [c*T + t]).
1288
- *
1289
- * Mirrors outetts v3 `extract_audio_from_tokens`: filter the stream into two ordered
1290
- * lists by membership of the c1 / c2 ranges, recover the raw code
1291
- * (id − base), drop the reserved index 1024, and truncate to the shorter list. The
1292
- * model emits c1,c2 interleaved per frame, but the parse is membership-based (it does
1293
- * not assume strict pairing) so a stray token cannot desync the grid.
1294
- */
1295
- declare function audioTokensToDacCodes(tokenIds: number[]): {
1296
- codes: Uint32Array;
1297
- numFrames: number;
1298
- };
1299
- interface DacSpeechGraphOptions {
1300
- /** Number of audio frames T (the code-grid width). */
1301
- numFrames: number;
1266
+ forwardArgmax(inputIds: Uint32Array): Promise<number>;
1267
+ /** KV-cache positions still available for decode steps. */
1268
+ decodeCapacityRemaining(): number;
1269
+ /** Number of decode steps that may be in flight in the pipelined path. */
1270
+ static readonly PIPELINE_DEPTH = 2;
1302
1271
  /**
1303
- * Number of RVQ codebooks. Default 2 (DAC.speech.v1.0). Parler-TTS's
1304
- * dac_44khz_8kbps uses 9 — the RVQDequant kernel loops over NCB so the only
1305
- * change is the codebook/out_proj tensor first-dim and the codes grid height.
1272
+ * Pipelined greedy decode step (Dawn only — WebKit uses forwardArgmax).
1273
+ *
1274
+ * Encodes one full decode forward + argmax and submits WITHOUT awaiting
1275
+ * completion. The input token is taken from `tokenId` for the first step
1276
+ * after prefill; for subsequent steps (tokenId === null) the previous step's
1277
+ * argmax result is copied into input_ids ON THE GPU, so the decode loop
1278
+ * never blocks on a readback before submitting the next step. The argmax
1279
+ * result is copied to a per-slot readback buffer, read later (one step
1280
+ * behind) via readDecodeToken(slot).
1281
+ *
1282
+ * queue.writeBuffer is queue-ordered: uniform updates land after the
1283
+ * previously submitted step's command buffer and before this one's, so
1284
+ * shared uniform buffers are safe with multiple steps in flight.
1306
1285
  */
1307
- numCodebooks?: number;
1286
+ submitGreedyDecodeStep(tokenId: number | null, slot: number): void;
1287
+ /** Read back the token produced by the pipelined step that used `slot`. */
1288
+ readDecodeToken(slot: number): Promise<number>;
1308
1289
  /**
1309
- * Decoder upsampling rates (the DecoderBlock strides). Default [8,5,4,2]
1310
- * (DAC.speech.v1.0). Parler's dac_44khz uses [8,8,4,2] (hop 512). Channels
1311
- * still halve per stage from `decoderDim`.
1290
+ * K-step greedy decode window (GERBIL_DECODE_WINDOW=K dispatch-collapse
1291
+ * campaign Lever 1; Dawn only).
1292
+ *
1293
+ * Records `steps` full decode forwards + argmax into ONE command buffer and
1294
+ * submits once. Between recorded steps the previous step's argmax result is
1295
+ * copied into input_ids ON THE GPU (the same 4-byte chain the pipelined path
1296
+ * uses across submits), so no CPU sync exists inside the window. Each step's
1297
+ * winning token id is copied into the slot's window readback at offset 4·k;
1298
+ * {@link readDecodeWindow} maps it once per window.
1299
+ *
1300
+ * Seq-pos-dependent entries (RoPE position, KV-append offset, attention S —
1301
+ * ~3 per full-attention layer; Mamba layers carry state and have none) get
1302
+ * lazily-built per-step uniform/bind-group variants, ALL written with
1303
+ * queue.writeBuffer BEFORE the submit (queue-ordered, so safe with a prior
1304
+ * window still in flight). Params are built against per-step resolved shapes
1305
+ * (L_max tensors resolve to seqPos+T — attention S comes from the KV shape).
1306
+ * Dispatch grids are encoded ONCE per window at the worst case (last step's
1307
+ * shapes, pos+K-1): decode grids are step-invariant in practice, and any
1308
+ * L_max-derived grid over-dispatches for early steps, which is safe because
1309
+ * kernels bound their work from uniforms.
1310
+ *
1311
+ * EOS is only observable at window granularity: up to K-1 tokens beyond EOS
1312
+ * are computed and discarded by the caller. KV overshoot is harmless (seqPos
1313
+ * governs reads) but SSM/conv state is NOT rewindable — continuing the same
1314
+ * context past an intra-window EOS requires a re-prefill from that point.
1315
+ *
1316
+ * Evidence base: measured FLAT on M4 Max/Metal (results.jsonl r13-E2 /
1317
+ * r14-E2v2 — depth-2 pipelining already hides submit+readback there);
1318
+ * targeted at Dawn/Vulkan where the submit roundtrip is 52µs and decode is
1319
+ * dispatch/driver-bound (A5000 gate, arXiv:2604.02344). See
1320
+ * docs/design/dispatch-collapse-campaign.md §3.
1321
+ */
1322
+ submitGreedyDecodeWindow(firstToken: number | null, steps: number, slot: number): void;
1323
+ /** Get (or lazily create) entry's per-step uniform/bind-group variant. */
1324
+ private getWindowVariant;
1325
+ /** Read back the `steps` token ids produced by the window that used `slot`. */
1326
+ readDecodeWindow(slot: number, steps: number): Promise<Uint32Array>;
1327
+ reset(): void;
1328
+ /**
1329
+ * Diagnostic: dispatch ONLY the first kernel (EmbeddingInt4) using the
1330
+ * production bind group, pipeline, and buffers — but in isolation (1 dispatch,
1331
+ * no staging, fresh encoder). Compares against full forward pass to isolate
1332
+ * whether the issue is the bind group/pipeline or the multi-dispatch context.
1312
1333
  */
1313
- decoderRates?: readonly number[];
1314
- /** First-conv output channels (decoder_dim). Default 1536. */
1315
- decoderDim?: number;
1316
- /** Quantizer out_proj output / first-conv input dim (latent_dim). Default 1024. */
1317
- latentDim?: number;
1318
- }
1319
- declare function generateDacSpeechDecoderGraph(opts: DacSpeechGraphOptions): ModelGraph;
1320
- /**
1321
- * Final decoded PCM length (samples) for a T-frame code grid, via the DAC stage chain.
1322
- * `rates` defaults to DAC.speech.v1.0 ([8,5,4,2]); pass [8,8,4,2] for Parler's dac_44khz.
1323
- */
1324
- declare function dacOutputLength(numFrames: number, rates?: readonly number[]): number;
1325
- declare function generateOuteTtsBackboneGraph(rawConfig: Record<string, unknown>, dtype?: GraphDType, groupSize?: number, kvDtype?: KVDType): ModelGraph;
1326
- //#endregion
1327
- //#region src/gpu/architectures/qwen3_5_vision.d.ts
1328
- /**
1329
- * Build the ViT graph. The graph is shaped by symbolic "N" (number of patches),
1330
- * resolved at run time from the input tensor's first dim — exactly like the LM's
1331
- * symbolic "T".
1332
- */
1333
- declare function generateQwen3_5VisionGraph(rawConfig: Record<string, unknown>): ModelGraph;
1334
- //#endregion
1335
- //#region src/gpu/executor.d.ts
1336
- /**
1337
- * Safari/Metal workaround: shader variant alternation.
1338
- * Metal caches argument buffers per compiled function. When consecutive dispatches
1339
- * use the same WGSL code (same Metal function), Metal reuses the previous dispatch's
1340
- * argument buffer, ignoring setBindGroup(). We alternate between variant 0/1 of each
1341
- * shader (prepending `const _MV: u32 = Xu;`) to force different Metal function
1342
- * specializations, preventing argument buffer reuse.
1343
- */
1344
- interface ExecutorOptions {
1345
- maxSeqLen: number;
1346
- /** KV cache kernel strategy. Defaults to "native-f16" when not specified. */
1347
- kvMode?: KvMode;
1334
+ debugFirstDispatch(inputIds: Uint32Array): Promise<{
1335
+ nodeId: string;
1336
+ opType: string;
1337
+ dispatchSize: [number, number, number];
1338
+ output: Float32Array;
1339
+ }>;
1348
1340
  /**
1349
- * WebKit only: dispatches per command buffer, with at most one command
1350
- * buffer in flight (awaited). 1 (default) is the proven-correct floor on
1351
- * iPad; larger values are faster if this WebKit version keeps storage
1352
- * writes visible across dispatches within one submission. Sweepable via
1353
- * the ?group=N URL param.
1341
+ * Diagnostic: run a single dispatch entry by index, in isolation.
1342
+ * Call after debugFirstDispatch() to test whether entry[1] (RMSNorm)
1343
+ * can read embed_out written by entry[0] (EmbeddingInt4).
1354
1344
  */
1355
- webkitGroupSize?: number;
1356
- }
1357
- interface ForwardResult {
1358
- logits: Float32Array;
1359
- }
1360
- declare class Executor {
1361
- private ctx;
1362
- private graph;
1363
- private weightBuffers;
1364
- private activationBuffers;
1365
- private ssmStateBuffers;
1366
- private kvCacheBuffers;
1367
- /** Pre-allocated input_ids buffer (maxSeqLen * 4 bytes). */
1368
- private inputIdsBuffer;
1345
+ debugDispatchEntry(entryIndex: number, T: number): Promise<{
1346
+ nodeId: string;
1347
+ opType: string;
1348
+ output: Float32Array;
1349
+ }>;
1369
1350
  /**
1370
- * CPU-resident Per-Layer-Embeddings (PLE) source for Gemma 4. The PLE table
1371
- * (`embed_tokens_per_layer`, [vocab, L*256]) is ~1.17GB at 4-bit and is kept
1372
- * OFF the GPU. Each forward step we gather + dequantize only the rows for the
1373
- * actual input tokens and upload a tiny [T, L*256] f32 buffer. See setPleSource.
1351
+ * Diagnostic: compute the JS-side params for the first N decode entries
1352
+ * WITHOUT dispatching. Shows what buildParams produces.
1374
1353
  */
1375
- private pleSource;
1376
- /** Reusable scratch for the dequantized PLE rows (resized on demand). */
1377
- private pleScratch;
1354
+ debugComputeParams(T: number, count?: number): Array<{
1355
+ idx: number;
1356
+ nodeId: string;
1357
+ opType: string;
1358
+ paramsU32: number[];
1359
+ dispatchSize: [number, number, number];
1360
+ }>;
1378
1361
  /**
1379
- * One-time promise that materializes a cache-backed PLE table into heap. The
1380
- * table is read from CacheStorage on the FIRST forward — i.e. AFTER the GPU
1381
- * weight upload has completed, so the ~1.17 GB does not stack on top of the
1382
- * upload's transient allocations at the load-time memory high-water mark.
1362
+ * Diagnostic: after a forward pass, read back output tensors at several points
1363
+ * in the pipeline to find where data drops to zero.
1383
1364
  */
1384
- private pleReady;
1365
+ debugPipelineProbe(T: number): Promise<Array<{
1366
+ idx: number;
1367
+ nodeId: string;
1368
+ opType: string;
1369
+ tensor: string;
1370
+ sum: number;
1371
+ first4: number[];
1372
+ uniformParams?: number[];
1373
+ }>>;
1374
+ debugWriteBuffer(tensorName: string, data: ArrayBufferView): void;
1385
1375
  /**
1386
- * Dummy GPU buffer bound to an otherwise-aliasing storage-read-write slot.
1387
- * Used by RoPE-Q-only nodes (Gemma 4 KV-shared layers) whose node lists the
1388
- * same tensor as input and output: the RoPE kernel always declares two
1389
- * read_write bindings (q, k), but with num_kv_heads=0 the k slot is never
1390
- * touched. WebGPU still rejects two read_write bindings aliasing one buffer,
1391
- * so we bind this throwaway buffer to the unused k slot. Allocated lazily.
1376
+ * Write a host-supplied activation input buffer (e.g. multimodal M-RoPE
1377
+ * cos/sin, spliced vision embeddings, image row-map). The buffer must be a
1378
+ * persistent activation tensor in the graph. Call before forward().
1392
1379
  */
1393
- private bindingScratchBuffer;
1394
- /** Readback buffer for logits. */
1395
- private logitsReadback;
1396
- /** GPU buffer for argmax result (1 u32). */
1397
- private argmaxResultBuffer;
1398
- /** Readback buffer for argmax result (1 u32). */
1399
- private argmaxReadback;
1400
- /** Readback ring for pipelined greedy decode (created lazily). */
1401
- private decodeReadbacks;
1380
+ writeInput(tensorName: string, data: ArrayBufferView): void;
1381
+ /** Write a host activation buffer at a byte offset (for per-row decode updates). */
1382
+ writeInputAt(tensorName: string, data: ArrayBufferView, byteOffset: number): void;
1383
+ /** True if the graph has a buffer with this name (multimodal-capability probe). */
1384
+ hasBuffer(tensorName: string): boolean;
1385
+ /** Current sequence position (number of tokens processed since reset). */
1386
+ get currentSeqPos(): number;
1387
+ debugReadBuffer(tensorName: string, maxElements?: number, byteOffset?: number): Promise<Float32Array>;
1388
+ destroy(): void;
1402
1389
  /**
1403
- * Staging buffer for uniform param updates.
1404
- * Safari/Metal has weaker visibility guarantees for queue.writeBuffer() to
1405
- * UNIFORM buffers early writes get dropped when hundreds are queued.
1406
- * Instead, we pack all params into this STORAGE staging buffer (1 writeBuffer),
1407
- * then use encoder.copyBufferToBuffer to distribute to each uniform buffer.
1408
- * Copies are GPU-sequenced and guaranteed to complete before compute passes.
1390
+ * Allocate activation buffers with liveness-based reuse.
1391
+ *
1392
+ * One dedicated buffer per activation tensor at full maxSeqLen is ~2.3GB for
1393
+ * Qwen3.5-0.8B at T=512 over the iOS jetsam budget on its own. Instead,
1394
+ * a buffer returns to a size-keyed pool once its tensor's last reader has
1395
+ * executed, so concurrently-live tensors share a small working set.
1396
+ *
1397
+ * Graph outputs and tensors read before they are written (cross-forward
1398
+ * state) keep dedicated buffers. Within a forward, dispatches execute in
1399
+ * executionOrder on every path (single-pass Dawn, per-dispatch WebKit), and
1400
+ * WebGPU synchronizes hazards between dispatches, so reuse is safe.
1401
+ *
1402
+ * Caveat: debugReadBuffer() on an intermediate tensor is only meaningful
1403
+ * before a later op reuses its buffer (probes that stop mid-graph are fine).
1409
1404
  */
1410
- private uniformStagingBuffer;
1411
- private uniformStagingCapacity;
1412
- /** Dispatch entries for prefill (M>1): uses tiled matmul kernels. */
1413
- private dispatchEntries;
1414
- /** Dispatch entries for decode (M=1): uses K-parallel matvec kernels. */
1415
- private decodeEntries;
1416
- /** Per-node decode entries captured BEFORE fusion (the overlay base). */
1417
- private decodeEntriesUnfused;
1418
- /** True while a runtime adapter is applied. */
1419
- private loraActive;
1420
- /** Decode entries (unfused base + spliced LoRA dispatches). */
1421
- private decodeEntriesLoRA;
1422
- /** Prefill entries (base + spliced LoRA dispatches). */
1423
- private prefillEntriesLoRA;
1424
- /** GPU buffers owned by the current adapter (A/B factors + tmp); freed on swap. */
1425
- private loraBuffers;
1426
- /** Argmax dispatch entry (created in initBindGroups). */
1427
- private argmaxEntry;
1428
- /** True when running on Safari/WebKit (needs multi-encoder submit). */
1429
- readonly needsMultiEncoder: boolean;
1430
- private maxSeqLen;
1431
- private kvMode;
1432
- private seqPos;
1433
- private webkitGroupSize;
1434
- private profileEnabled;
1435
- private readonly profileData;
1436
- private querySet;
1437
- private queryResolveBuf;
1438
- private queryReadbackBuf;
1439
- constructor(ctx: GPUContext, graph: ModelGraph, options: ExecutorOptions);
1405
+ private allocateActivationBuffers;
1406
+ private allocateSSMStateBuffers;
1407
+ private allocateKVCacheBuffers;
1408
+ /** f16 SSM-state kernels are selected on Dawn with f16 support (not WebKit). */
1409
+ private get usesF16Ssm();
1440
1410
  /**
1441
- * Register the CPU-resident Gemma 4 PLE table. The quantized table is kept in
1442
- * JS memory (NOT uploaded to a GPU buffer), and {@link forward} gathers +
1443
- * dequantizes only the rows for the current input tokens each step, uploading a
1444
- * small [T, width] f32 buffer into `targetTensor`. This is what keeps Gemma 4
1445
- * mobile-viable: resident GPU memory is just the active transformer weights.
1411
+ * Batched-decode state (GERBIL_BATCH=N): per-slot SSM/conv state pools, the
1412
+ * per-row positions/seq_lens buffers, the sampled-token buffer + readback
1413
+ * ring, and batch-widened buffers for the lm-head tail (SliceLastRow output
1414
+ * and logits become [N, ...]).
1415
+ *
1416
+ * SSM pool slot stride: the f16 MambaSSM kernel indexes its state buffer in
1417
+ * f16 ELEMENTS (the buffer is f32-sized with the upper half unused), so its
1418
+ * pool slot stride is elems*2 bytes; conv state (f32 kernels) uses the full
1419
+ * f32 byte size. The batched kernels compute `slot * elems` in their own
1420
+ * element space, so the pool stride must match each kernel's element width.
1446
1421
  */
1447
- setPleSource(src: {
1448
- packed: Uint32Array;
1449
- scales: Float32Array;
1450
- zeros: Float32Array;
1451
- width: number;
1452
- groupSize: number;
1453
- targetTensor: string;
1454
- cache?: {
1455
- cacheName: string;
1456
- packedKey: string;
1457
- scalesKey: string;
1458
- zerosKey: string;
1459
- packedLen: number;
1460
- };
1461
- }): void;
1422
+ private allocateBatchState;
1423
+ private resolveShapes;
1424
+ private getBuffer;
1425
+ /**
1426
+ * Batch-path buffer resolution: lm-head tail tensors use their [B, ...]
1427
+ * widened buffers, ssm_state tensors bind the per-slot pool, everything
1428
+ * else falls through to the regular buffer maps.
1429
+ */
1430
+ private getBatchBuffer;
1431
+ /**
1432
+ * True when this node's KV traffic goes through the paged path: the flag is
1433
+ * on AND the node actually touches a kv_cache tensor (KVCacheAppend writes
1434
+ * one; self-attention reads one as its K input). SSM/conv state and
1435
+ * ViT/cross-attention over plain activations never match — on hybrid models
1436
+ * only the attention layers page, exactly as the design requires.
1437
+ */
1438
+ private usesPagedKV;
1439
+ /**
1440
+ * Paged KV only: make sure blocks are mapped for logical positions
1441
+ * [0, seqPos + T) BEFORE this forward's dispatches are submitted.
1442
+ * queue.writeBuffer is queue-ordered, so the (rare — once per 16 tokens)
1443
+ * table upload always lands ahead of the submits that read it, including in
1444
+ * the pipelined decode path with PIPELINE_DEPTH steps in flight.
1445
+ */
1446
+ private preparePagedKV;
1447
+ /** Runtime context for batched dispatches (positions live in buffers). */
1448
+ private batchRuntimeCtx;
1449
+ /**
1450
+ * Shapes for the batched step: every "T" dimension is the batch width, and
1451
+ * the lm-head tail tensors (statically [1, ...] in the graph) widen to
1452
+ * [B, ...] — every row IS a last row on the batch path.
1453
+ */
1454
+ private resolveBatchShapes;
1455
+ /**
1456
+ * Build the batched dispatch-entry list: one lockstep decode step for all
1457
+ * batchSize rows. Row-independent ops (embedding, norms, elementwise) keep
1458
+ * their prefill kernels at T=B; linear layers run the batched INT4 GEMM;
1459
+ * every position/state-dependent op swaps to its batch-indexed variant
1460
+ * (paged attention + append via per-row block-table rows, RoPE via the
1461
+ * positions buffer, conv/SSM via the per-slot state pool).
1462
+ */
1463
+ private buildBatchEntries;
1464
+ /**
1465
+ * Batched-path fusion driver (Phase 4). Ports the single-sequence decode
1466
+ * fusions onto `batchEntries`: identical math (the fused kernels reproduce
1467
+ * the unfused chain operation-for-operation — exact-tier GEMM reductions
1468
+ * keep the V2 order, elementwise prologues/epilogues keep the registry
1469
+ * kernels' formulas), strictly fewer dispatches. `GERBIL_BATCH_FUSE`
1470
+ * selects a subset for A/B measurement: unset/"all" = every pass,
1471
+ * "none" = none, or a comma list of pass names.
1472
+ */
1473
+ private fuseBatchDecodeEntries;
1474
+ /**
1475
+ * Batch port of fuseSwiGLUDecodeEntries: gate MatMulInt4 + up MatMulInt4 +
1476
+ * elementwise SwiGLU → ONE fused batched GEMM (saves 2 dispatches per MLP
1477
+ * block and the gate/up activation round-trips).
1478
+ */
1479
+ private fuseBatchSwiGLUMLPEntries;
1480
+ /**
1481
+ * Batch port of fuseDualMatVecDecodeEntries: two adjacent MatMulInt4s
1482
+ * sharing one input and identical K/N/group_size (q+gate, k+v) → ONE dual
1483
+ * batched GEMM dispatch (wid.y selects the projection — per-projection
1484
+ * math unchanged).
1485
+ */
1486
+ private fuseBatchDualMatVecEntries;
1462
1487
  /**
1463
- * True when this model streams a CPU-resident Per-Layer-Embedding table
1464
- * (Gemma 4): each decode step must gather+upload the PLE rows for the ACTUAL
1465
- * input token id on the CPU before the forward runs. The GPU-chained pipelined
1466
- * decode path and forwardArgmax both bypass CPU token knowledge, so they leave
1467
- * `ple_embed_out` stale and must NOT be used for these models — only the
1468
- * per-step forward() path (which calls streamPleRows) is correct. See generate().
1488
+ * Batch port of fuseDualKVCacheAppendEntries: adjacent K+V paged appends
1489
+ * ONE BATCHED_PAGED_DUAL_KV_APPEND dispatch (pure memcpys identical
1490
+ * writes, one dispatch per attention layer saved).
1469
1491
  */
1470
- hasPleSource(): boolean;
1492
+ private fuseBatchDualKVAppendEntries;
1471
1493
  /**
1472
- * Materialize a cache-backed PLE table into heap exactly once. Deferred to the
1473
- * first forward (after GPU upload) so the ~1.17 GB table is not co-resident
1474
- * with the upload's transient allocations during the load-time memory peak.
1494
+ * Batch port of fuseGatedOProjDecodeEntries ("sigmoid": SigmoidGate
1495
+ * o_proj) and fuseSwiGLUGatedProjDecodeEntries ("swiglu": Mamba SwiGLU
1496
+ * out_proj): the elementwise gate is recomputed inside the GEMM's input
1497
+ * load with the registry kernel's exact formula — one dispatch per layer
1498
+ * saved plus the gated-activation round-trip.
1475
1499
  */
1476
- private ensurePleLoaded;
1500
+ private fuseBatchGatedProjEntries;
1477
1501
  /**
1478
- * Gather + dequantize the PLE rows for `inputIds` and upload them into the
1479
- * target activation buffer. Touches only T rows (T*width floats) the full
1480
- * [vocab, width] quantized table never goes to the GPU.
1502
+ * Batch port of fuseDualRMSNormRoPEDecodeEntries: [q_norm, k_norm, RoPE]
1503
+ * ONE BATCHED_DUAL_RMSNORM_ROPE dispatch (per-row rotation position from
1504
+ * the positions buffer). Saves 2 dispatches per full-attention layer.
1481
1505
  */
1482
- private streamPleRows;
1506
+ private fuseBatchDualRMSNormRoPEEntries;
1483
1507
  /**
1484
- * Stream weights to the GPU one tensor at a time, pulling each from the
1485
- * `WeightSource` only when it is about to be uploaded and dropping the
1486
- * reference immediately afterward.
1508
+ * Reset ALL batch slots for a new lockstep generation: frees every
1509
+ * sequence's KV blocks, zeroes the single-sequence SSM state (the prefill
1510
+ * staging area), and clears per-slot lengths. State-pool slots need no
1511
+ * zeroing — finishBatchSlot overwrites a slot in full before it is read.
1512
+ */
1513
+ resetBatch(): void;
1514
+ /**
1515
+ * Route the single-sequence forward() path at batch slot `slot`: paged KV
1516
+ * writes/reads use the slot's block-table row (RuntimeContext.tableBase) and
1517
+ * SSM state accumulates in the singleton staging buffers (zeroed).
1518
+ * Call once per prompt, then forward(promptIds), then finishBatchSlot().
1519
+ */
1520
+ beginBatchSlot(slot: number): void;
1521
+ /**
1522
+ * Adopt the just-prefilled sequence into `slot`: records its length, copies
1523
+ * the singleton SSM/conv state into the slot's pool rows (KV already lives
1524
+ * in the slot's block-table row via tableBase), and zeroes the singleton
1525
+ * staging state for the next prefill.
1526
+ */
1527
+ finishBatchSlot(slot: number): void;
1528
+ /** Per-slot token counts (prompt + generated) on the batch path. */
1529
+ get batchSlotLengths(): Uint32Array;
1530
+ /**
1531
+ * Submit one lockstep batched decode step (pipelined — does not await).
1532
+ * `tokens` feeds input_ids on the first step; pass null afterwards to chain
1533
+ * the previous step's sampled ids into input_ids ON the GPU. Sampled ids
1534
+ * land in the `slot` readback (read one step behind via readBatchTokens).
1535
+ */
1536
+ submitBatchDecodeStep(tokens: Uint32Array<ArrayBuffer> | null, slot: number): void;
1537
+ /** Read the sampled ids produced by the batched step that used `slot`. */
1538
+ readBatchTokens(slot: number): Promise<Uint32Array>;
1539
+ /** Batch entries in effect for the current lane-adapter state. */
1540
+ private get activeBatchEntries();
1541
+ /** Number of adapters registered for per-lane batched decode. */
1542
+ get batchAdapterCount(): number;
1543
+ /**
1544
+ * Register a LoRA adapter for per-lane batched decode and return its integer
1545
+ * adapter id (assign lanes via {@link setBatchLaneAdapters}). Factors are
1546
+ * packed into per-adapted-node GPU buffers ONCE — every lane running this
1547
+ * adapter reads the same factors. Fused query+gate deltas are expanded with
1548
+ * the same deinterleave the single-sequence overlay uses.
1487
1549
  *
1488
- * This is the property that bounds peak JS heap: with a cache-backed source
1489
- * (browser/mobile), only ONE tensor's bytes are materialized in heap at a time
1490
- * (read from CacheStorage in `source.get()`), uploaded to its GPU buffer, then
1491
- * released before the next tensor is fetched. The whole model is never co-
1492
- * resident in heap. With a heap-backed source (Node/desktop), behavior matches
1493
- * the old Map path (the source deletes each entry as it is consumed).
1550
+ * Phase-1 constraint: every adapter adapting a given linear must share that
1551
+ * node's LoRA rank (the packed factor arena is fixed-stride; mixed ranks are
1552
+ * deferred).
1553
+ */
1554
+ registerBatchAdapter(deltas: LoRADelta[]): number;
1555
+ /** Drop every registered batch adapter and free the overlay's GPU buffers. */
1556
+ clearBatchAdapters(): void;
1557
+ /**
1558
+ * Assign each batch lane its adapter: `ids[b]` is a registered adapter id or
1559
+ * -1 for the bare base. Uploads the per-lane adapter_ids buffer (the
1560
+ * positions/seq_lens per-row pattern) and switches the step's entry list to
1561
+ * the LoRA overlay iff any lane carries an adapter — an all-base assignment
1562
+ * runs the untouched plain batch entries at zero extra cost.
1563
+ */
1564
+ setBatchLaneAdapters(ids: ArrayLike<number>): void;
1565
+ /** Free the batch-LoRA overlay's GPU buffers (factors, maps, tmp, uniforms). */
1566
+ private destroyBatchLoraBuffers;
1567
+ /**
1568
+ * (Re)build the per-lane LoRA overlay: pack every registered adapter's
1569
+ * factors per adapted node, and derive `batchEntriesLoRA` from the fused
1570
+ * batch entry list with SELECTIVE unfusing — every fused group that covers
1571
+ * no adapted linear stays exactly as the plain batch path runs it; groups
1572
+ * covering an adapted node fall back to their pre-fusion per-node entries
1573
+ * (token-exact by the Phase 2/4 derivation discipline) with the per-lane
1574
+ * `[down, up]` gather pair spliced right after each adapted node, so the
1575
+ * corrected output is live before any consumer reads it.
1576
+ */
1577
+ private rebuildBatchLoraOverlay;
1578
+ /** Total physical KV blocks in the paged pool (admission-control bound). */
1579
+ get kvTotalBlocks(): number;
1580
+ /** KV blocks currently unallocated. */
1581
+ get kvFreeBlocks(): number;
1582
+ /** Tokens per KV block (admission-control block-count math). */
1583
+ get kvBlockSizeTokens(): number;
1584
+ /** Blocks a single sequence may map (== ceil(maxSeqLen / blockSize)). */
1585
+ get kvMaxBlocksPerSeq(): number;
1586
+ /** Effective context length (scheduler admission math). */
1587
+ get maxSequenceLength(): number;
1588
+ /**
1589
+ * Retire a batch lane: free its KV blocks and zero its length so masked
1590
+ * steps treat the slot as empty (seq_lens[slot] = 0). The SSM pool row
1591
+ * needs no cleanup — re-admission overwrites it in full (finishBatchSlot).
1592
+ */
1593
+ retireBatchSlot(slot: number): void;
1594
+ /**
1595
+ * Seed `slot`'s next input token (the first token sampled from its prefill
1596
+ * logits). Written into the sampled-ids buffer so the next masked step's
1597
+ * on-GPU chain copy propagates it into input_ids. queue.writeBuffer is
1598
+ * queue-ordered: it lands AFTER any in-flight step's argmax write to the
1599
+ * same word and BEFORE the next submitted step's chain copy — correct even
1600
+ * with PIPELINE_DEPTH steps in flight.
1601
+ */
1602
+ injectBatchToken(slot: number, token: number): void;
1603
+ /**
1604
+ * Submit one MASKED batched decode step (the scheduler's step; pipelined —
1605
+ * does not await). Only lanes with `active[b] != 0` advance: inactive lanes
1606
+ * get positions=0 / seq_lens=0, which masks their KV appends (kernel guard)
1607
+ * and clamps their attention window to nothing; their other row-local ops
1608
+ * compute garbage that stays confined to their rows/state slots and is
1609
+ * fully overwritten on re-admission.
1494
1610
  *
1495
- * Accepts either a `WeightSource` (new, streamed/async) or a plain Map
1496
- * (back-compat for callers that build a Map directly, e.g. Kani/Moonshine).
1611
+ * The step's dispatch width is dynamic: kernels take their batch dimension
1612
+ * from the runtime context, so only rows [0, maxActiveSlot] are dispatched
1613
+ * (GEMM b-tiles may cover a few dead rows past the last active one —
1614
+ * in-bounds stale writes that are never read). Input tokens always chain
1615
+ * from the sampled-ids buffer on the GPU; newly admitted lanes are seeded
1616
+ * via injectBatchToken. Sampled ids land in the `slot` readback (read one
1617
+ * step behind via readBatchTokens).
1618
+ */
1619
+ submitBatchDecodeStepMasked(active: Uint8Array, slot: number): void;
1620
+ /** Write the batched-argmax uniform once per batch session (invariant). */
1621
+ private writeBatchArgmaxParams;
1622
+ private requireBatch;
1623
+ /**
1624
+ * Detect gate_proj + up_proj + SwiGLU patterns in decode entries and replace
1625
+ * with a single fused SwiGLUMatVec dispatch. Saves 2 dispatches per MLP block.
1497
1626
  */
1498
- uploadWeights(source: WeightSource | Map<string, {
1499
- data: ArrayBufferView;
1500
- shape: number[];
1501
- }>): Promise<void>;
1502
1627
  /**
1503
- * Synchronous Map upload (Node/desktop and the Kani/Moonshine sub-executors,
1504
- * which build small heap Maps). Deletes each entry as it is consumed to free
1505
- * the JS-side bytes once they are GPU-resident. Safe to call from a constructor.
1628
+ * Fuse [ResidualRMSNorm, gate MatMulInt4, up MatMulInt4, SwiGLU] → ONE
1629
+ * NormSwiGLUMatVec dispatch. The kernel computes the residual sum + RMS in a
1630
+ * prologue (workgroup 0 persists the residual sum for the downstream residual
1631
+ * chain) and normalizes the activation on the fly, so the norm dispatch AND
1632
+ * the normed tensor's write+read roundtrip disappear. INT4-only; falls back
1633
+ * when the device can't bind 11 storage buffers. Windows it skips fall
1634
+ * through to fuseSwiGLUDecodeEntries.
1506
1635
  */
1507
- uploadWeightsMap(weights: Map<string, {
1508
- data: ArrayBufferView;
1509
- shape: number[];
1510
- }>): void;
1511
1636
  /**
1512
- * Build all pipelines and bind groups. Call ONCE after uploadWeights().
1637
+ * Fuse [ResidualRMSNorm Mamba qkv MatVecInt4] ONE NormMatVec dispatch.
1638
+ * The kernel recomputes the residual sum + RMS in a prologue (workgroup 0
1639
+ * persists BOTH the residual sum and the full normed vector — the latter is
1640
+ * live for the sibling a_proj/b_proj/z_proj), then the GEMV normalizes the
1641
+ * activation on the fly. Saves the ResidualRMSNorm dispatch (−1 per Mamba
1642
+ * layer, ×17). 10 storage bindings — fits the Dawn/Metal 10-binding adapter.
1643
+ * INT4-only.
1513
1644
  *
1514
- * Creates two dispatch entry arrays:
1515
- * - dispatchEntries: tiled matmul for prefill (any M)
1516
- * - decodeEntries: K-parallel matvec for decode (M=1)
1645
+ * OPT-IN, DEFAULT OFF (GERBIL_FUSE_NORM_QKV=1). Round r28 (2026-07-25) measured
1646
+ * this a −4.6% REGRESSION on M4 Max/Metal: the qkv GEMV is the widest projection
1647
+ * (N=6144 384 workgroups), so recomputing the 1024-wide residual+RMS reduction
1648
+ * per workgroup + normalizing activations on the fly costs far more ALU than the
1649
+ * single saved ResidualRMSNorm dispatch returns on a bandwidth-plateaued backend.
1650
+ * The −17 dispatch/token reduction still targets Vulkan/WebKit (dispatch-count-
1651
+ * bound) — validate on the kernel-lab A5000 rig before enabling anywhere.
1652
+ */
1653
+ private fuseNormMatVecDecodeEntries;
1654
+ private fuseNormSwiGLUDecodeEntries;
1655
+ private fuseSwiGLUDecodeEntries;
1656
+ /**
1657
+ * Fuse the Mamba-front projections — qkv (MatVec), a (MatVec), b (MatVec),
1658
+ * z (MatVec) — into ONE QuadMatVecInt4 dispatch per Mamba layer. All four
1659
+ * read the SAME `input_layernorm` output and share K and group_size, so their
1660
+ * INT4 weight matrices (stored column-major, one column contiguous) are
1661
+ * column-concatenated into a single q/scales/zeros buffer by a plain
1662
+ * byte-append (a GPU buffer-to-buffer copy at init). The fused kernel runs a
1663
+ * single GEMV over the concatenated N and routes each output column back to
1664
+ * its ORIGINAL destination buffer, so downstream consumers (conv1d reads qkv,
1665
+ * MambaSSM reads a/b, the gated out_proj reads z) are untouched.
1666
+ *
1667
+ * Numerically identical to the three separate dispatches (same dequant, same
1668
+ * K-parallel reduction, same per-column sum — the concatenated weights are the
1669
+ * same bytes), so this is token-exact and WebKit-safe. It removes 2
1670
+ * dispatches per Mamba layer (3 front dispatches → 1) plus one input read and
1671
+ * two weight-read passes.
1672
+ *
1673
+ * Runs BEFORE fuseDualMatVecDecodeEntries so the a/b pair is still two
1674
+ * standalone MatVec entries here (not yet a DualMatVec). Restricted to the
1675
+ * Mamba front by weight-name (`.in_proj_qkv/a/b/z.`), so the attention front
1676
+ * (q/gate/k/v) is never matched. Bindings: A + q/s/z + 4 outs + params = 9
1677
+ * (fits Metal's 10-binding wall). Gate: default-on when the adapter offers ≥9
1678
+ * storage bindings; opt-out via GERBIL_FUSE_MAMBA_FRONT=0.
1517
1679
  */
1518
- initBindGroups(): void;
1519
- /** True while a runtime LoRA overlay is applied. */
1520
- get hasRuntimeAdapter(): boolean;
1521
- /** Decode entries in effect for the current adapter state. */
1522
- private get activeDecodeEntries();
1680
+ private fuseMambaFrontDecodeEntries;
1523
1681
  /**
1524
- * Apply a runtime LoRA overlay on the STATIC base. For each delta whose target
1525
- * maps to a base linear (MatMulInt4 / MatMul) with matching dims, uploads the
1526
- * f32 A/B factors and splices two low-rank dispatches `tmp = A·x`, then
1527
- * `y += scaling·B·tmp` right after that node in both the decode and prefill
1528
- * paths, so the corrected output is live before any consumer reads it.
1682
+ * Fuse two adjacent INT4 projections that share the same input activation and
1683
+ * the same K/N (e.g. q_proj+gate_proj and k_proj+v_proj in full-attention
1684
+ * decode) into a single DualMatVecInt4 dispatch. Reads the shared input vector
1685
+ * once and writes both projection outputs removing one GPU round-trip (one
1686
+ * submit+drain on Safari/iOS) per fused pair.
1529
1687
  *
1530
- * Rebuilds the overlay from scratch each call (this is how adapters hot-swap);
1531
- * the base weight buffers and bind groups are never touched or re-uploaded.
1532
- * Pass an empty list (or call {@link clearRuntimeLoRA}) to drop the overlay.
1688
+ * Numerics are identical to running the two MatVecInt4 kernels separately
1689
+ * (same dequant, same K-parallel reduction), so this is WebKit-safe: it reuses
1690
+ * the proven INT4 matvec math and only merges two writes into one dispatch.
1691
+ *
1692
+ * Must run AFTER fuseSwiGLUDecodeEntries so the MLP gate+up pair (which is
1693
+ * consumed by a SwiGLU node) is already collapsed and won't be matched here.
1533
1694
  */
1534
- /** Find the base linear (MatMulInt4 / MatMul) node whose weight is `key`. */
1535
- private findBaseLinear;
1695
+ private fuseDualMatVecDecodeEntries;
1536
1696
  /**
1537
- * Expand any fused query+gate delta into separate `q_proj` + `attn_gate`
1538
- * deltas so it can splice onto a deinterleaved runtime graph.
1697
+ * Fuse the adjacent K-cache and V-cache appends in each full-attention layer
1698
+ * into a single DualKVCacheAppend dispatch. Both are pure memcpys into f32
1699
+ * caches sharing the same width and dst_offset, so one dispatch with two
1700
+ * src/dst buffers writes both — removing one GPU round-trip per layer.
1539
1701
  *
1540
- * Qwen3.5-style gated attention ships `q_proj.weight` fused as
1541
- * `[2·num_heads·head_dim, hidden]` (per-head: query rows then gate rows); the
1542
- * model loader splits it into two runtime linears (`q_proj`, `attn_gate`). An
1543
- * adapter trained on the fused checkpoint carries a single `q_proj` delta whose
1544
- * out-dim is 2× either split node, so it matches neither. When BOTH split
1545
- * linears exist and the delta is exactly their combined width, split it via the
1546
- * same per-head row deinterleave the loader uses; otherwise the delta passes
1547
- * through untouched (e.g. a base that never split the projection).
1702
+ * Supports f32, native-f16, and packed-f16 caches (the dual kernel mirrors the
1703
+ * single-append kernel selected for the active kvMode). Numerically identical
1704
+ * to the separate appends WebKit-safe (the packed-f16 variant is the Safari
1705
+ * path and uses pack2x16float, no `enable f16`).
1548
1706
  */
1549
- private expandFusedQGateDeltas;
1550
- applyRuntimeLoRA(deltas: LoRADelta[]): {
1551
- applied: number;
1552
- skipped: number;
1553
- };
1554
- /** Drop the runtime LoRA overlay and free its GPU buffers. Base is untouched. */
1555
- clearRuntimeLoRA(): void;
1556
- /** Build the [down, up] low-rank dispatch pair for one adapted node. */
1557
- private buildLoraPair;
1558
- /** Copy a base entry list, splicing each node's LoRA pair in right after it. */
1559
- private spliceLoraOverlay;
1560
- /** Original constituent node ids a (possibly fused) decode entry covers. */
1561
- private coveredNodeIds;
1707
+ private fuseDualKVCacheAppendEntries;
1562
1708
  /**
1563
- * Build the decode entry list for an active adapter with SELECTIVE unfusing.
1564
- * Every fused group that covers no adapted linear is kept exactly as the
1565
- * no-adapter fast path runs it (full SwiGLU/dual/gated fusion, no numerics
1566
- * change). Only the groups that cover an adapted node fall back to their
1567
- * pre-fusion per-node dispatches (pulled from {@link decodeEntriesUnfused}),
1568
- * with the low-rank `[down, up]` pair spliced right after each adapted node so
1569
- * its corrected output is live before any consumer reads it. Replaying the
1570
- * exact pre-fusion entries reproduces the validated unfused numerics for the
1571
- * adapted groups while leaving all other groups on the fused path.
1709
+ * Fuse the attention SigmoidGate (attn_out * sigmoid(gate)) into the INT4
1710
+ * o_proj that consumes it: a GatedMatVecInt4 reads attn_out and gate directly,
1711
+ * applies the sigmoid gate to its input vector, and runs the projection in ONE
1712
+ * dispatch removing the standalone SigmoidGate (one round-trip per
1713
+ * full-attention layer).
1714
+ *
1715
+ * Numerically identical to SigmoidGate→MatVecInt4 (same gate formula, same INT4
1716
+ * dequant + reduction). Slight extra ALU: the gated input is recomputed per
1717
+ * output column, but A reads hit L1 and the saved submit+drain dominates on
1718
+ * mobile. WebKit risk: low — reuses the proven INT4 matvec, only the A vector
1719
+ * is built from two reads + a sigmoid (no new reduction/barrier pattern).
1720
+ *
1721
+ * Runs after the dual fusions so it only sees the post-attention SigmoidGate.
1572
1722
  */
1573
- private buildSelectiveLoraDecode;
1723
+ private fuseGatedOProjDecodeEntries;
1574
1724
  /**
1575
- * Run a forward pass. Uses matvec kernels for M=1 (decode), tiled for M>1 (prefill).
1725
+ * Fuse a standalone SwiGLU (silu(gate) * up) into the INT4 projection that
1726
+ * consumes its output: a SwiGLUGatedMatVecInt4 reads gate and up directly,
1727
+ * builds the gated input vector, and runs the projection in ONE dispatch.
1728
+ * Targets the Mamba block's mamba_swiglu (silu(z) * norm_out) feeding out_proj
1729
+ * — one round-trip saved per linear-attention layer.
1730
+ *
1731
+ * The MLP SwiGLU is already collapsed into a SwiGLUMatVec entry by
1732
+ * fuseSwiGLUDecodeEntries (it has no surviving standalone SwiGLU node), so only
1733
+ * the Mamba SwiGLU matches here. Numerically identical to SwiGLU→MatVecInt4;
1734
+ * WebKit risk low (reuses the proven INT4 matvec, only the A vector changes).
1576
1735
  */
1577
- forward(inputIds: Uint32Array): Promise<ForwardResult>;
1736
+ private fuseSwiGLUGatedProjDecodeEntries;
1578
1737
  /**
1579
- * Profiling variant of the desktop dispatch path: one compute pass per dispatch,
1580
- * each bracketed by timestamp queries, so we get per-op GPU time. Accumulates
1581
- * into profileData by opType. Only runs under GERBIL_PROFILE with timestamp-query
1582
- * support slower than the batched path (it measures relative cost, not tok/s).
1738
+ * Fuse two adjacent per-row RMSNorms sharing hidden_size + eps into a single
1739
+ * DualRMSNorm dispatch (e.g. the per-head q_norm and k_norm in full-attention
1740
+ * decode). One workgroup still handles one row; the fused grid just spans both
1741
+ * inputs' rows, so each row's reduction is unchanged numerically identical to
1742
+ * two separate RMSNorm dispatches. One round-trip saved per fused pair.
1743
+ *
1744
+ * WebKit risk: low — same single-workgroup reduction as the proven RMSNorm
1745
+ * kernel, only the row→input routing is added.
1583
1746
  */
1584
- private runProfiledDispatches;
1585
- /** Per-opType GPU time (ns) + dispatch count accumulated by GERBIL_PROFILE, hottest first. */
1586
- getProfile(): Array<{
1587
- opType: string;
1588
- ns: number;
1589
- count: number;
1590
- }>;
1591
- /** Clear accumulated profiler data (e.g. to drop warm-up tokens). */
1592
- resetProfile(): void;
1593
- /** GPU dispatches per decode token (post-fusion). On mobile this drives the
1594
- * submit-group count = ceil(dispatchCount / webkitGroupSize). */
1595
- get decodeDispatchCount(): number;
1596
- /** Device limit that gates the INT4 projection fusions (they need ≥9). If a
1597
- * device caps at 8 the dual/gated/swiglu-gated INT4 fusions silently fall back,
1598
- * inflating the decode dispatch count. */
1599
- get maxStorageBuffers(): number;
1600
1747
  /**
1601
- * Profile ONE real decode step: times the actual `decodeEntries` (the kernels
1602
- * the pipelined greedy benchmark runs) with per-dispatch timestamps. Timing is
1603
- * token-independent, so pass any valid id; runs un-pipelined with a synchronous
1604
- * timestamp readback (measurement only, not for production decode). Argmax (one
1605
- * tiny dispatch) is intentionally excluded — it is not a hotspot target.
1748
+ * Fuse [CausalConv1dSiLU|CausalConv1dGated ConvStateUpdate] into one
1749
+ * dispatch (decode entries only). At T=1 the conv read and the rolling-state
1750
+ * write for a channel live in the same thread, so the fused kernel is
1751
+ * race-free; prefill (T>1) keeps the two-dispatch path.
1606
1752
  */
1607
- profileDecodeStep(tokenId: number): Promise<void>;
1753
+ private fuseConvStateUpdateDecodeEntries;
1608
1754
  /**
1609
- * Run a single forward pass over `inputIds` and read back the L2-normalized
1610
- * embedding vector. Requires an embedding graph (one whose output tensor is
1611
- * "embedding", produced by the last-token-pool + L2-norm tail).
1612
- *
1613
- * Always runs in a fresh-state single pass (caller should reset() first):
1614
- * embeddings are non-autoregressive, so the whole sequence is one prefill.
1755
+ * Fuse [per-head q_norm RMSNorm, k_norm RMSNorm, in-place RoPE] into ONE
1756
+ * DualRMSNormRoPE dispatch. Numerically identical (norm then rotate, same
1757
+ * math); requires head_dim <= 256 and RoPE operating in-place on exactly the
1758
+ * two norm outputs. Saves 2 dispatches per full-attention layer — a mobile
1759
+ * (WebKit submit-per-dispatch) win, desktop-neutral.
1615
1760
  */
1616
- embed(inputIds: Uint32Array): Promise<Float32Array>;
1617
1761
  /**
1618
- * Generic one-shot dispatch for a non-LM graph: run every entry once over a
1619
- * single forward and read back `elemCount` f32 elements of the named output
1620
- * tensor. Used to execute the NanoCodec decoder graph (codes→PCM) its ops use
1621
- * concrete lengths and it has no "logits" output, so the normal forward()
1622
- * logits-readback path does not apply. Caller writes any inputs (e.g. audio_codes)
1623
- * via writeInput() and reset()s first.
1762
+ * Fuse [Add RMSNorm] into a single ResidualRMSNorm dispatch: the add's sum
1763
+ * is still materialized (sum_output downstream residual chain reads it) and
1764
+ * the norm result is written in the same dispatch. Numerically identical.
1624
1765
  */
1625
- runGraphOutput(outputName: string, elemCount: number): Promise<Float32Array>;
1766
+ private fuseAddRMSNormDecodeEntries;
1767
+ private fuseDualRMSNormRoPEDecodeEntries;
1768
+ private fuseDualRMSNormDecodeEntries;
1626
1769
  /**
1627
- * Autoregressive step for a multi-output decoder graph (e.g. Parler-TTS's 9 codebook
1628
- * lm_heads). Dispatches the whole graph ONCE at the current seqPos (so the per-layer
1629
- * KVCacheAppend writes at the right cache row), reads back each named output, then
1630
- * advances seqPos by `tokens` (default 1, for a prefilled prompt span pass tokens=T).
1631
- * The caller writes inputs (dec_input_embed + frozen cross-attn K/V) and reset()s
1632
- * before the first step. Unlike forward(), there is no single "logits" output —
1633
- * the caller decodes each codebook row itself.
1770
+ * Gather buffer entries for a bind group, matching the kernel spec's binding layout.
1771
+ * Uses the pre-allocated inputIdsBuffer for the "input_ids" tensor.
1634
1772
  */
1635
- runGraphStep(outputs: Array<{
1636
- name: string;
1637
- elemCount: number;
1638
- }>, tokens?: number): Promise<Float32Array[]>;
1773
+ private gatherBuffers;
1774
+ /** Lazily allocate a scratch storage buffer at least `minBytes` large. */
1775
+ private getBindingScratchBuffer;
1776
+ }
1777
+ //#endregion
1778
+ //#region src/gpu/batch/scheduler.d.ts
1779
+ /** Options for a single scheduled request. */
1780
+ interface ServeOptions {
1781
+ /** Max new tokens (clamped to context; default 128). */
1782
+ maxTokens?: number;
1783
+ stopSequences?: string[];
1784
+ systemPrompt?: string;
1785
+ /** Greedy-only in Phase 5: temperature must be 0 (or omitted). */
1786
+ sampling?: {
1787
+ temperature?: number;
1788
+ };
1789
+ /**
1790
+ * Registered adapter name this request decodes with (SGMV — requires
1791
+ * `GERBIL_BATCH_LORA=1` and a prior `registerAdapter` call on the engine);
1792
+ * undefined = bare base. Requests with different adapters (or none) decode
1793
+ * concurrently over the shared base, each token-exact vs a solo run with
1794
+ * that adapter loaded.
1795
+ */
1796
+ adapter?: string;
1797
+ /** Streamed per-token callback (decoded text piece). */
1798
+ onToken?: (token: string, meta: {
1799
+ requestId: number;
1800
+ tokenId: number;
1801
+ generated: number;
1802
+ }) => void;
1803
+ }
1804
+ /** Per-request result + serving timings (all performance.now() ms). */
1805
+ interface ServeResult {
1806
+ requestId: number;
1807
+ text: string;
1808
+ tokenIds: number[];
1809
+ tokensGenerated: number;
1810
+ finishReason: "eos" | "max_tokens" | "stop_sequence";
1811
+ /** The adapter name the request decoded with (echoed through), if any. */
1812
+ adapter?: string;
1813
+ /** submit() call time. */
1814
+ submittedAt: number;
1815
+ /** Admission into a batch slot (prefill start). */
1816
+ admittedAt: number;
1817
+ /** First token available (end of prefill). */
1818
+ firstTokenAt: number;
1819
+ finishedAt: number;
1820
+ /** Time-to-first-token from submission (queue wait + prefill). */
1821
+ ttftMs: number;
1822
+ e2eMs: number;
1823
+ }
1824
+ /** Wiring the engine hands the scheduler (see WebGPUEngine.createScheduler). */
1825
+ interface SchedulerHost {
1826
+ executor: Executor;
1827
+ encodeChat(messages: ChatMessage[]): number[];
1828
+ decodeToken(id: number): string;
1829
+ eosTokenId: number | null;
1830
+ eotTokenId: number | null;
1831
+ /** Vocab-prune remap for CPU-sampled tokens (identity when unpruned). */
1832
+ remapToken(id: number): number;
1833
+ /**
1834
+ * Resolve a registered batch-adapter name to its executor adapter id +
1835
+ * parsed deltas. Must throw a clear error for unregistered names (surfaced
1836
+ * at submit() time, before the request is queued).
1837
+ */
1838
+ resolveAdapter(name: string): {
1839
+ id: number;
1840
+ deltas: LoRADelta[];
1841
+ };
1842
+ /** Serialize against generate()/embed() on the same engine. */
1843
+ acquireLock(): Promise<() => void>;
1844
+ }
1845
+ interface SchedulerStats {
1846
+ submitted: number;
1847
+ completed: number;
1848
+ stepsSubmitted: number;
1849
+ prefillChunks: number;
1850
+ tokensGenerated: number;
1851
+ /** Wall-clock ms the serving loop was live (work outstanding). */
1852
+ busyMs: number;
1853
+ peakActiveLanes: number;
1854
+ }
1855
+ declare class RequestScheduler {
1856
+ private readonly host;
1857
+ private readonly batchSize;
1858
+ private readonly maxSeqLen;
1859
+ private readonly blockSize;
1860
+ private readonly prefillChunk;
1861
+ /** Free KV blocks kept unreserved as safety headroom. */
1862
+ private readonly blockHeadroom;
1863
+ private nextId;
1864
+ private queue;
1865
+ /** slot → occupying request (prefilling or decoding). */
1866
+ private slots;
1867
+ /** Sum of blocksReserved across live (admitted, unfinished) requests. */
1868
+ private reservedBlocks;
1869
+ private prefilling;
1870
+ private inFlight;
1871
+ /**
1872
+ * The slot→adapter-id assignment last uploaded to the executor (null =
1873
+ * nothing uploaded this batch session). Steps only re-upload when the
1874
+ * assignment changes (admissions/retirements), and the very first upload is
1875
+ * skipped entirely while every lane is bare base — an adapter-free serving
1876
+ * session never touches the LoRA machinery.
1877
+ */
1878
+ private lastLaneAdapterIds;
1879
+ private stepCounter;
1880
+ private loop;
1881
+ private closed;
1882
+ private stats;
1883
+ constructor(host: SchedulerHost, options?: {
1884
+ prefillChunkTokens?: number;
1885
+ });
1886
+ /**
1887
+ * Submit a request; resolves with the full result when it finishes.
1888
+ * Tokens stream through options.onToken as they are decoded.
1889
+ */
1890
+ submit(prompt: string | ChatMessage[], options?: ServeOptions): Promise<ServeResult>;
1891
+ /** Resolves when every submitted request has completed. */
1892
+ drain(): Promise<void>;
1893
+ /** Stop accepting requests; resolves when in-flight work drains. */
1894
+ close(): Promise<void>;
1895
+ getStats(): SchedulerStats;
1896
+ private pump;
1897
+ private hasWork;
1898
+ private activeMask;
1899
+ /** The serving loop. Holds the engine generation lock while live. */
1900
+ private run;
1901
+ /**
1902
+ * Upload the slot→adapter assignment for the step about to be submitted,
1903
+ * when it differs from the last uploaded one. Called immediately before
1904
+ * every decode-step submit: `queue.writeBuffer` is queue-ordered, so steps
1905
+ * already in flight keep the assignment they were submitted under, and a
1906
+ * retiring lane's slot can never leak its adapter into the slot's next
1907
+ * occupant — the next occupant's assignment is re-derived from `slots`
1908
+ * before its first decode step. Masked (free/prefilling) lanes are pinned
1909
+ * to bare base; their output is discarded regardless.
1910
+ */
1911
+ private syncLaneAdapters;
1912
+ /**
1913
+ * Admission: move the next queued request into a free slot when (a) a slot
1914
+ * is free, (b) no other prefill is staged (the single-sequence prefill path
1915
+ * stages SSM state in singleton buffers — one at a time), and (c) its
1916
+ * worst-case block need fits the unreserved KV pool with headroom.
1917
+ */
1918
+ private admit;
1919
+ /**
1920
+ * Run one bounded prefill chunk for the staged request on the
1921
+ * single-sequence path (into its slot's block-table row via tableBase).
1922
+ * Intermediate chunks are submit-only; the final chunk reads logits, adopts
1923
+ * the singleton SSM state into the slot's pool row, seeds the lane's first
1924
+ * token, and flips the lane to decoding.
1925
+ */
1926
+ private prefillChunkStep;
1927
+ /** Read the oldest in-flight step and deliver its tokens. */
1928
+ private readStep;
1929
+ /** Append one generated token; finish + retire the lane when terminal. */
1930
+ private consumeToken;
1931
+ /** Retire the lane immediately: free KV blocks, open the slot, resolve. */
1932
+ private finish;
1933
+ }
1934
+ //#endregion
1935
+ //#region src/gpu/batch/swarm.d.ts
1936
+ /** Per-request options a swarm sub-task may set (the member picks the adapter). */
1937
+ type SwarmTaskOptions = Omit<ServeOptions, "adapter">;
1938
+ /** One planned sub-task: which member decodes which prompt. */
1939
+ interface SwarmTask {
1940
+ /** Member name (a key of the swarm's `members`). */
1941
+ member: string;
1942
+ prompt: string | ChatMessage[];
1943
+ /** Per-task overrides merged over the run's options. */
1944
+ options?: SwarmTaskOptions;
1945
+ }
1946
+ /** One member's completed sub-task (plan order is preserved). */
1947
+ interface SwarmMemberResult {
1948
+ member: string;
1949
+ result: ServeResult;
1950
+ }
1951
+ /**
1952
+ * Swarm configuration.
1953
+ *
1954
+ * @template R The reduced result type (defaults to the member-keyed map the
1955
+ * default reduce produces).
1956
+ */
1957
+ interface SwarmOptions<R = Record<string, ServeResult>> {
1639
1958
  /**
1640
- * Greedy decode step: forward + GPU argmax. Returns token ID directly.
1641
- * Always uses matvec kernels (M=1). Reads back 4 bytes instead of vocab_size*4.
1959
+ * Member name adapter source (`hf:owner/repo`, URL, or `file:./dir`), or
1960
+ * null for a member that decodes on the bare base. Every non-null source is
1961
+ * registered on the engine (factors packed into shared GPU buffers once).
1642
1962
  */
1643
- forwardArgmax(inputIds: Uint32Array): Promise<number>;
1644
- /** KV-cache positions still available for decode steps. */
1645
- decodeCapacityRemaining(): number;
1646
- /** Number of decode steps that may be in flight in the pipelined path. */
1647
- static readonly PIPELINE_DEPTH = 2;
1963
+ members: Record<string, string | null>;
1648
1964
  /**
1649
- * Pipelined greedy decode step (Dawn only WebKit uses forwardArgmax).
1650
- *
1651
- * Encodes one full decode forward + argmax and submits WITHOUT awaiting
1652
- * completion. The input token is taken from `tokenId` for the first step
1653
- * after prefill; for subsequent steps (tokenId === null) the previous step's
1654
- * argmax result is copied into input_ids ON THE GPU, so the decode loop
1655
- * never blocks on a readback before submitting the next step. The argmax
1656
- * result is copied to a per-slot readback buffer, read later (one step
1657
- * behind) via readDecodeToken(slot).
1965
+ * input sub-tasks routed to members. Default: broadcast the input to
1966
+ * every member (one sub-task each, declaration order).
1967
+ */
1968
+ plan?: (input: string, members: string[]) => SwarmTask[] | Promise<SwarmTask[]>;
1969
+ /**
1970
+ * Joined sub-results final answer. Default: a member-keyed map of
1971
+ * ServeResults (a duplicated member's later tasks get `name#2`, `name#3`…).
1972
+ */
1973
+ reduce?: (results: SwarmMemberResult[]) => R | Promise<R>;
1974
+ }
1975
+ /**
1976
+ * A swarm handle: registered members + plan/reduce over the engine's
1977
+ * scheduler. Create via {@link WebGPUEngine.createSwarm}.
1978
+ */
1979
+ declare class Swarm<R = Record<string, ServeResult>> {
1980
+ private readonly scheduler;
1981
+ private readonly memberSources;
1982
+ private readonly plan;
1983
+ private readonly reduce;
1984
+ constructor(scheduler: RequestScheduler, options: SwarmOptions<R>);
1985
+ /** Member names, declaration order. */
1986
+ get members(): string[];
1987
+ /**
1988
+ * Run one input through the swarm: plan → concurrent fan-out (one batched
1989
+ * pass across the scheduler's lanes) → reduce.
1658
1990
  *
1659
- * queue.writeBuffer is queue-ordered: uniform updates land after the
1660
- * previously submitted step's command buffer and before this one's, so
1661
- * shared uniform buffers are safe with multiple steps in flight.
1991
+ * @param input The task input handed to the plan (default plan: broadcast
1992
+ * it to every member as the prompt).
1993
+ * @param options Per-request options applied to every sub-task (a task's
1994
+ * own `options` win field-by-field).
1662
1995
  */
1663
- submitGreedyDecodeStep(tokenId: number | null, slot: number): void;
1664
- /** Read back the token produced by the pipelined step that used `slot`. */
1665
- readDecodeToken(slot: number): Promise<number>;
1666
- reset(): void;
1996
+ run(input: string, options?: SwarmTaskOptions): Promise<R>;
1667
1997
  /**
1668
- * Diagnostic: dispatch ONLY the first kernel (EmbeddingInt4) using the
1669
- * production bind group, pipeline, and buffers but in isolation (1 dispatch,
1670
- * no staging, fresh encoder). Compares against full forward pass to isolate
1671
- * whether the issue is the bind group/pipeline or the multi-dispatch context.
1998
+ * Mode-A access: decode one prompt with a single member (no plan/reduce).
1999
+ * Still rides the shared scheduler, so it batches with any concurrent work.
1672
2000
  */
1673
- debugFirstDispatch(inputIds: Uint32Array): Promise<{
1674
- nodeId: string;
1675
- opType: string;
1676
- dispatchSize: [number, number, number];
1677
- output: Float32Array;
1678
- }>;
2001
+ generate(member: string, prompt: string | ChatMessage[], options?: SwarmTaskOptions): Promise<ServeResult>;
2002
+ private submitFor;
2003
+ }
2004
+ //#endregion
2005
+ //#region src/gpu/sampler.d.ts
2006
+ /**
2007
+ * CPU-side token sampling from logits.
2008
+ *
2009
+ * Applies temperature, top-k, and top-p (nucleus) filtering,
2010
+ * then samples from the resulting probability distribution.
2011
+ *
2012
+ * Uses typed arrays and min-heap for zero-allocation top-K selection.
2013
+ * For vocab_size ~152K with topK=50, this avoids creating 152K JS tuples.
2014
+ */
2015
+ interface SamplingParams {
2016
+ temperature?: number;
2017
+ topK?: number;
2018
+ topP?: number;
2019
+ repetitionPenalty?: number;
1679
2020
  /**
1680
- * Diagnostic: run a single dispatch entry by index, in isolation.
1681
- * Call after debugFirstDispatch() to test whether entry[1] (RMSNorm)
1682
- * can read embed_out written by entry[0] (EmbeddingInt4).
2021
+ * Seed for the sampling RNG. When set, sampled decoding is reproducible:
2022
+ * the same params object draws a deterministic random stream (seeded on its
2023
+ * first use pass a fresh options object per generation for run-to-run
2024
+ * reproducibility). Unset: Math.random() (non-deterministic, the default).
1683
2025
  */
1684
- debugDispatchEntry(entryIndex: number, T: number): Promise<{
1685
- nodeId: string;
1686
- opType: string;
1687
- output: Float32Array;
1688
- }>;
2026
+ seed?: number;
2027
+ }
2028
+ //#endregion
2029
+ //#region src/gpu/vision-preprocess.d.ts
2030
+ /**
2031
+ * Host-side vision preprocessing for the Qwen3.5 ViT.
2032
+ *
2033
+ * The learned position embeddings (bilinear-interpolated over the patch grid)
2034
+ * and the 2D rotary cos/sin tables are functions of the image grid (t, h, w)
2035
+ * ONLY — not of the model weights or pixel values. They are cheap to compute on
2036
+ * the CPU and fed to the GPU graph as input activations, keeping the graph to
2037
+ * weight-dependent math while staying byte-identical to HF transformers.
2038
+ *
2039
+ * Ports (verified against transformers 5.12 vision_utils.py + modeling_qwen3_5):
2040
+ * - get_vision_bilinear_indices_and_weights → buildPosEmbeds()
2041
+ * - get_vision_position_ids → buildPositionIds()
2042
+ * - Qwen3_5VisionRotaryEmbedding → buildRotaryCosSin()
2043
+ *
2044
+ * Patch ordering: both the pos-embed gather and the position ids reorder patches
2045
+ * into spatial_merge_size×spatial_merge_size groups, matching the HF image
2046
+ * processor's output ordering, so the merger's [N,h]→[N/u,h*u] reshape lines up.
2047
+ */
2048
+ interface VisionGridConfig {
2049
+ hiddenSize: number;
2050
+ numHeads: number;
2051
+ numPositionEmbeddings: number;
2052
+ spatialMergeSize: number;
2053
+ ropeTheta?: number;
2054
+ }
2055
+ interface VisionPositionTensors {
2056
+ /** [N, hidden_size] bilinear-interpolated learned position embeddings. */
2057
+ posEmbeds: Float32Array;
2058
+ /** [N, head_dim] rotary cosines. */
2059
+ cos: Float32Array;
2060
+ /** [N, head_dim] rotary sines. */
2061
+ sin: Float32Array;
2062
+ numPatches: number;
2063
+ }
2064
+ /**
2065
+ * Build bilinear-interpolated learned position embeddings [N, hidden].
2066
+ * posEmbedTable is the raw pos_embed.weight [num_position_embeddings, hidden].
2067
+ */
2068
+ declare function buildPosEmbeds(gridTHW: [number, number, number], posEmbedTable: Float32Array, cfg: VisionGridConfig): Float32Array;
2069
+ /**
2070
+ * Build the reordered (row, col) position ids [N, 2] for rotary, matching
2071
+ * get_vision_position_ids.
2072
+ */
2073
+ declare function buildPositionIds(gridTHW: [number, number, number], merge: number): Int32Array;
2074
+ /**
2075
+ * Build rotary cos/sin tables [N, head_dim] from position ids, matching
2076
+ * Qwen3_5VisionRotaryEmbedding + the cat((rotary, rotary)) in VisionModel.forward.
2077
+ *
2078
+ * rotary_pos_emb(position_ids) = (position_ids[..,None] * inv_freq).flatten(1)
2079
+ * where inv_freq has length (head_dim/2)/2 = head_dim/4, computed over dim=head_dim/2.
2080
+ * For each token the two position components (h, w) each produce head_dim/4 freqs,
2081
+ * concatenated → head_dim/2, then duplicated → head_dim for cos/sin.
2082
+ */
2083
+ declare function buildRotaryCosSin(positionIds: Int32Array, headDim: number, theta?: number): {
2084
+ cos: Float32Array;
2085
+ sin: Float32Array;
2086
+ numPatches: number;
2087
+ };
2088
+ /**
2089
+ * Build all host position tensors for a single image grid in one call.
2090
+ */
2091
+ declare function buildVisionPositionTensors(gridTHW: [number, number, number], posEmbedTable: Float32Array, cfg: VisionGridConfig): VisionPositionTensors;
2092
+ interface Gemma4VisionGridConfig {
2093
+ hiddenSize: number;
2094
+ numHeads: number;
2095
+ headDim: number;
2096
+ ropeTheta: number;
2097
+ poolingKernelSize: number;
2098
+ }
2099
+ interface Gemma4VisionPositionTensors {
2100
+ /** [N, hidden] axial position embeddings (table[0][x] + table[1][y]). */
2101
+ posEmbeds: Float32Array;
2102
+ /** [N, headDim] axial rotary cosines. */
2103
+ cos: Float32Array;
2104
+ /** [N, headDim] axial rotary sines. */
2105
+ sin: Float32Array;
2106
+ /** [Np, N] average-pooling matrix (1/k² in-cell, 0 elsewhere). */
2107
+ poolMatrix: Float32Array;
2108
+ /** number of patches N (= gridH*gridW). */
2109
+ numPatches: number;
2110
+ /** number of pooled (soft) tokens Np (= ceil(gridH/k)*ceil(gridW/k)). */
2111
+ numPooled: number;
2112
+ }
2113
+ /**
2114
+ * Build axial learned position embeddings [N, hidden] from the [2, posSize, hidden]
2115
+ * table: pos[p] = table[0][x_p] + table[1][y_p]. Direct lookup, no interpolation
2116
+ * (HF F.embedding on clamped positions).
2117
+ */
2118
+ declare function buildGemma4PosEmbeds(gridH: number, gridW: number, posEmbedTable: Float32Array,
2119
+ // [2, posSize, hidden] flattened
2120
+ hidden: number, posSize: number): Float32Array;
2121
+ /**
2122
+ * Build the 2D axial rotary cos/sin tables [N, headDim].
2123
+ * spatial_dim = headDim / 2; inv_freq[j] = 1/theta^((2j)/spatial_dim), j in [0, spatial_dim/2)
2124
+ * per spatial dim: f = pos * inv_freq (spatial_dim/2 values); emb = cat(f, f) (spatial_dim values)
2125
+ * cos/sin = cat([emb_x, emb_y]) → headDim values, layout [fx,fx,fy,fy].
2126
+ * Applied with the global-half rotate_half kernel (ApplyRotaryEmb), which computes
2127
+ * out = x*cos + rotate_half(x)*sin element-wise — exact for this layout.
2128
+ */
2129
+ declare function buildGemma4RotaryCosSin(gridH: number, gridW: number, headDim: number, theta: number): {
2130
+ cos: Float32Array;
2131
+ sin: Float32Array;
2132
+ };
2133
+ /**
2134
+ * Build the [Np, N] average-pooling matrix for k×k spatial pooling over the real
2135
+ * (unpadded) grid, matching modeling_gemma4's kernel_idxs/one_hot pooling:
2136
+ * cell(p) = floor(x_p/k) + ceil(gridW/k) * floor(y_p/k)
2137
+ * poolMatrix[cell, p] = 1/k² (so pooled = poolMatrix @ hidden = mean over the k×k block)
2138
+ * Np = ceil(gridH/k) * ceil(gridW/k). Each pooled cell averages exactly the patches
2139
+ * that fall in it (edge cells with fewer than k² patches still divide by k², matching
2140
+ * HF's fixed 1/k² normalization).
2141
+ */
2142
+ declare function buildGemma4PoolMatrix(gridH: number, gridW: number, k: number): {
2143
+ poolMatrix: Float32Array;
2144
+ numPooled: number;
2145
+ };
2146
+ /**
2147
+ * Build all Gemma 4 vision host tensors for one image grid in one call.
2148
+ * `posEmbedTable` is the raw [2, posSize, hidden] flattened table.
2149
+ */
2150
+ declare function buildGemma4VisionPositionTensors(gridH: number, gridW: number, posEmbedTable: Float32Array, posSize: number, cfg: Gemma4VisionGridConfig): Gemma4VisionPositionTensors;
2151
+ /** Gemma 4 image processor config (from processor_config.json). */
2152
+ declare const GEMMA4_IMAGE_PROCESSOR: ImageProcessorConfig;
2153
+ interface Gemma4PreprocessedImage {
2154
+ /** Flattened patches [N, 3·patch²] row-major (row-major patch grid). */
2155
+ patches: Float32Array;
2156
+ /** Patch grid (gridH, gridW). */
2157
+ gridHW: [number, number];
2158
+ }
2159
+ /**
2160
+ * Preprocess a decoded RGB image for the Gemma 4 ViT: aspect-preserving resize so
2161
+ * the patch grid is ≤ max_soft_tokens·k² patches and H,W divisible by k·patch,
2162
+ * rescale ×1/255 (no normalize), patchify row-major into [N, 3·16·16].
2163
+ *
2164
+ * @param pixels row-major HWC RGB (0..255), length width*height*3.
2165
+ */
2166
+ declare function preprocessImageGemma4(pixels: Float32Array | Uint8ClampedArray | Uint8Array, width: number, height: number, maxSoftTokens?: number, poolingKernelSize?: number, patchSize?: number): Gemma4PreprocessedImage;
2167
+ interface ImageProcessorConfig {
2168
+ patchSize: number;
2169
+ temporalPatchSize: number;
2170
+ mergeSize: number;
2171
+ imageMean: [number, number, number];
2172
+ imageStd: [number, number, number];
2173
+ /** rescale factor applied to raw 0..255 pixels before normalization (1/255). */
2174
+ rescaleFactor: number;
2175
+ /** min total pixels after resize (shortest_edge). */
2176
+ minPixels: number;
2177
+ /** max total pixels after resize (longest_edge). */
2178
+ maxPixels: number;
2179
+ }
2180
+ declare const QWEN3_5_IMAGE_PROCESSOR: ImageProcessorConfig;
2181
+ interface PreprocessedImage {
2182
+ /** Flattened patches [N, 1536] in the spatial-merge order encodeImage expects. */
2183
+ patches: Float32Array;
2184
+ /** (t, h, w) patch grid. t=1 for a single image, h/w in patch units. */
2185
+ gridTHW: [number, number, number];
2186
+ }
2187
+ /**
2188
+ * Qwen2-VL smart-resize: round H and W to multiples of factor=patch*merge,
2189
+ * keeping aspect ratio and clamping the total pixel budget to [minPixels, maxPixels].
2190
+ * Matches transformers.models.qwen2_vl.image_processing.smart_resize.
2191
+ */
2192
+ declare function smartResize(height: number, width: number, factor: number, minPixels: number, maxPixels: number): [number, number];
2193
+ /**
2194
+ * Preprocess a decoded RGB image into the [N, 1536] patch tensor + grid_thw that
2195
+ * `encodeImage()` expects, matching the HF Qwen2-VL image processor:
2196
+ * smart_resize → rescale (×1/255) → normalize → temporal-pair (×temporal_patch_size)
2197
+ * → patchify into spatial_merge×spatial_merge blocks → flatten to [N, C·T·P·P].
2198
+ *
2199
+ * @param pixels row-major HWC RGB, length width*height*3. Values 0..255 (default)
2200
+ * or already 0..1 if `rescaled` is true.
2201
+ * @param width source pixel width
2202
+ * @param height source pixel height
2203
+ */
2204
+ declare function preprocessImage(pixels: Float32Array | Uint8ClampedArray | Uint8Array, width: number, height: number, cfg?: ImageProcessorConfig, rescaled?: boolean): PreprocessedImage;
2205
+ declare function buildMRoPEPositionIds(inputIds: Int32Array | Uint32Array | number[], imageGrids: Array<[number, number, number]>, imageTokenId: number, mergeSize: number): Int32Array;
2206
+ /**
2207
+ * Per-pair frequency→dimension assignment for interleaved M-RoPE, matching
2208
+ * Qwen3_5TextRotaryEmbedding.apply_interleaved_mrope. For pair index i in
2209
+ * [0, sum(section)) the position component is section-cyclic: T,H,W,T,H,W,...
2210
+ * but each component capped at its section count. Returns an array of length
2211
+ * (rope_dim/2) with values 0=T, 1=H, 2=W.
2212
+ */
2213
+ declare function mropeFreqDims(mropeSection: [number, number, number]): Int32Array;
2214
+ /**
2215
+ * Build the interleaved-M-RoPE cos/sin tables [seq, rope_dim] from 3D position
2216
+ * ids, matching Qwen3_5TextRotaryEmbedding.forward:
2217
+ * freqs[d][i] = pos[d] * inv_freq[i], inv_freq[i] = 1/theta^(2i/rope_dim)
2218
+ * freq[i] picks component mropeFreqDims[i]; emb = cat(freqs, freqs).
2219
+ * cos/sin have length seq*rope_dim. For text-only (all 3 pos rows equal) this
2220
+ * reduces exactly to standard 1D partial RoPE.
2221
+ *
2222
+ * @param positionIds3 [3, seq] as produced by buildMRoPEPositionIds.
2223
+ * @param ropeDim number of rotated dims per head (head_dim * partial_factor).
2224
+ */
2225
+ declare function buildMRoPECosSin(positionIds3: Int32Array, seq: number, ropeDim: number, theta: number, mropeSection: [number, number, number]): {
2226
+ cos: Float32Array;
2227
+ sin: Float32Array;
2228
+ };
2229
+ //#endregion
2230
+ //#region src/gpu/defaults.d.ts
2231
+ /**
2232
+ * Default model per capability. Kept in its own tiny module (no heavy imports)
2233
+ * so the React hooks can resolve defaults without statically pulling in the GPU
2234
+ * engine — they import the engine dynamically to stay light.
2235
+ */
2236
+ declare const DEFAULT_MODELS: {
2237
+ /** Text generation (also the vision-capable checkpoint). */
2238
+ readonly text: "mlx-community/Qwen3.5-0.8B-4bit";
2239
+ /** Image understanding — same checkpoint, vision tower built on demand. */
2240
+ readonly vision: "mlx-community/Qwen3.5-0.8B-4bit";
2241
+ /** Text embeddings. */
2242
+ readonly embedding: "mlx-community/embeddinggemma-300m-4bit";
2243
+ /** Text-to-speech (Apache-2.0; LFM2-350M backbone + NanoCodec). Small/fast default. */
2244
+ readonly tts: "nineninesix/kani-tts-450m-0.2-ft";
1689
2245
  /**
1690
- * Diagnostic: compute the JS-side params for the first N decode entries
1691
- * WITHOUT dispatching. Shows what buildParams produces.
2246
+ * Selectable many-voices TTS (OuteTTS-1.0-0.6B: Qwen3-0.6B codec-LM + DAC.speech).
2247
+ * Pass this as the `repo`/`model` to engine.speak()/useTTS to route to OuteTTS.
1692
2248
  */
1693
- debugComputeParams(T: number, count?: number): Array<{
1694
- idx: number;
1695
- nodeId: string;
1696
- opType: string;
1697
- paramsU32: number[];
1698
- dispatchSize: [number, number, number];
1699
- }>;
2249
+ readonly ttsOute: "OuteAI/OuteTTS-1.0-0.6B";
1700
2250
  /**
1701
- * Diagnostic: after a forward pass, read back output tensors at several points
1702
- * in the pipeline to find where data drops to zero.
2251
+ * Selectable describe-your-voice TTS (Parler-TTS-mini-v1: Flan-T5 encoder +
2252
+ * cross-attending decoder LM + dac_44khz). Pass this as the `repo`/`model` to
2253
+ * engine.speak()/useTTS with a `describeVoice` natural-language description.
1703
2254
  */
1704
- debugPipelineProbe(T: number): Promise<Array<{
1705
- idx: number;
1706
- nodeId: string;
1707
- opType: string;
1708
- tensor: string;
1709
- sum: number;
1710
- first4: number[];
1711
- uniformParams?: number[];
1712
- }>>;
1713
- debugWriteBuffer(tensorName: string, data: ArrayBufferView): void;
2255
+ readonly ttsParler: "parler-tts/parler-tts-mini-v1";
1714
2256
  /**
1715
- * Write a host-supplied activation input buffer (e.g. multimodal M-RoPE
1716
- * cos/sin, spliced vision embeddings, image row-map). The buffer must be a
1717
- * persistent activation tensor in the graph. Call before forward().
2257
+ * Selectable streaming TTS (Gepard-1.0: stock Qwen3.5 full-attention codec-LM +
2258
+ * 32 parallel FSQ classifier heads emitting a whole audio frame per forward +
2259
+ * NanoCodec 21.5 fps decoder). Apache-2.0. Frame-synchronous, so audio streams
2260
+ * out chunk-by-chunk via speak(text, { model, onChunk }).
1718
2261
  */
1719
- writeInput(tensorName: string, data: ArrayBufferView): void;
1720
- /** Write a host activation buffer at a byte offset (for per-row decode updates). */
1721
- writeInputAt(tensorName: string, data: ArrayBufferView, byteOffset: number): void;
1722
- /** True if the graph has a buffer with this name (multimodal-capability probe). */
1723
- hasBuffer(tensorName: string): boolean;
1724
- /** Current sequence position (number of tokens processed since reset). */
1725
- get currentSeqPos(): number;
1726
- debugReadBuffer(tensorName: string, maxElements?: number, byteOffset?: number): Promise<Float32Array>;
1727
- destroy(): void;
2262
+ readonly ttsGepard: "nineninesix/gepard-1.0";
2263
+ /** Speech-to-text. */
2264
+ readonly stt: "UsefulSensors/moonshine-base";
2265
+ };
2266
+ declare const OUTETTS_ASSETS: {
2267
+ /** Folded DAC.speech.v1.0 encoder+decoder weights (prep-dac-speech.py output). */
2268
+ readonly codecWeightsURL: "https://pub-0522ecec25fc412bb35a523f3e3f63c6.r2.dev/tryhamster/gerbil-outetts-assets/dac-speech-folded.safetensors";
2269
+ /** Base URL for the preset speaker JSONs (`{name}.json`). */
2270
+ readonly speakersBaseURL: "https://pub-0522ecec25fc412bb35a523f3e3f63c6.r2.dev/tryhamster/gerbil-outetts-assets/speakers";
2271
+ /** The default preset voice. */
2272
+ readonly defaultVoice: "en-female-1-neutral";
2273
+ };
2274
+ /**
2275
+ * Bundled OuteTTS preset voices.
2276
+ *
2277
+ * Provenance (honest labelling):
2278
+ * - `en-female-1-neutral` is the ONE AUTHENTIC OuteAI voice — the OuteTTS v3 bundled
2279
+ * default speaker (default_speakers/en-female-1-neutral), copied verbatim.
2280
+ * - `gerbil-voice-2..6` are DERIVED voices: distinct timbres produced by running
2281
+ * pitch/tempo-varied renderings of that authentic clip back through the REAL
2282
+ * DAC.speech encode + prosody pipeline (scripts/engine/stage-outetts-assets.py).
2283
+ * They are genuine DAC-encoded OuteSpeakers, but they are NOT OuteAI's own
2284
+ * `en-male-*` voices, so they are not labelled as such. (Earlier builds mislabelled
2285
+ * them as `en-male-2-neutral` etc.; renamed for honesty.)
2286
+ */
2287
+ declare const OUTETTS_PRESET_VOICES: readonly ["en-female-1-neutral", "gerbil-voice-2", "gerbil-voice-3", "gerbil-voice-4", "gerbil-voice-5", "gerbil-voice-6"];
2288
+ /**
2289
+ * True when `repo`/`architecture` identify a text-to-speech checkpoint that must
2290
+ * run through a dedicated native TTS engine (KaniTTS, OuteTTS, or Parler-TTS)
2291
+ * rather than the single-graph base text loader. Mirrors the routing used by
2292
+ * `WebGPUEngine.speak()` / `Gerbil.ensureNativeTTSEngine`:
2293
+ * - Parler → encoder-decoder (`ParlerTTSForConditionalGeneration`)
2294
+ * - OuteTTS → Qwen3-0.6B codec-LM
2295
+ * - Kani → conv-backbone codec-LM (`KaniTTS2ForCausalLM`, or the Apache
2296
+ * default which is a plain `Lfm2ForCausalLM` — hence the repo-name
2297
+ * gate; the architecture string alone does not identify it).
2298
+ * At `create()` time only the repo is known (the architecture is discovered
2299
+ * during load), so the repo-name signals below cover all three DEFAULT_MODELS
2300
+ * and conventionally-named TTS repos; the architecture arg is honored when a
2301
+ * caller already has it.
2302
+ */
2303
+ declare function isTTSRepo(repo?: string, architecture?: string): boolean;
2304
+ /** Resolve the model repo for a set of options, falling back to the defaults. */
2305
+ declare function resolveDefaultRepo(opts: {
2306
+ repo?: string;
2307
+ embedding?: boolean;
2308
+ enableVision?: boolean;
2309
+ }): string;
2310
+ //#endregion
2311
+ //#region src/gpu/architectures/gemma4_vision.d.ts
2312
+ interface Gemma4VisionGraphInfo {
2313
+ hiddenSize: number;
2314
+ numHeads: number;
2315
+ headDim: number;
2316
+ depth: number;
2317
+ intermediateSize: number;
2318
+ textHidden: number;
2319
+ patchSize: number;
2320
+ patchDim: number;
2321
+ poolingKernelSize: number;
2322
+ ropeTheta: number;
2323
+ rmsNormEps: number;
2324
+ }
2325
+ /**
2326
+ * Resolve the Gemma 4 vision dims from a raw HF config. Accepts either the
2327
+ * top-level config (reads `.vision_config` + `.text_config.hidden_size`) or a
2328
+ * bare vision_config (then `textHidden` falls back to the projector row count if
2329
+ * present, else hidden). Family-general — no E2B constants.
2330
+ */
2331
+ declare function resolveGemma4VisionInfo(rawConfig: Record<string, unknown>): Gemma4VisionGraphInfo;
2332
+ /**
2333
+ * Dequantize an MLX affine-int4 weight to a plain f32 [rows, cols] matrix.
2334
+ * MLX packs 8 int4 values per u32 (low-nibble first); each group of `groupSize`
2335
+ * columns shares one scale + bias: w[r,c] = scale[r, c/gs] * q + bias[r, c/gs].
2336
+ * Used for the Gemma 4 multimodal projector (`embed_vision.embedding_projection`)
2337
+ * in MLX-4bit checkpoints, where (unlike the BF16 ViT body) the projector is int4.
2338
+ */
2339
+ declare function dequantizeMLXProjection(packed: Uint32Array, scales: Float32Array, biases: Float32Array, rows: number, cols: number, groupSize: number): Float32Array;
2340
+ /**
2341
+ * If the Gemma 4 multimodal projector arrived as an MLX affine-int4 triplet
2342
+ * (`embed_vision.embedding_projection.{weight(U32), scales, biases}`), dequantize
2343
+ * it in-place to a plain f32 `embed_vision.embedding_projection.weight` and drop
2344
+ * the scales/biases, so the vision graph's plain MatMul on the projector works for
2345
+ * MLX-4bit checkpoints too. No-op for BF16 (HF) checkpoints (weight already f32).
2346
+ */
2347
+ declare function dequantizeGemma4VisionProjection(weights: Map<string, {
2348
+ data: ArrayBufferView;
2349
+ shape: number[];
2350
+ }>, groupSize: number, rows: number, cols: number): void;
2351
+ /**
2352
+ * Patch the ClippedMatMul nodes of a Gemma 4 vision graph with the calibrated clip
2353
+ * scalars from the checkpoint (Gemma4ClippableLinear's per-tensor input/output
2354
+ * min/max buffers), then drop those scalar tensors from the weights map so the
2355
+ * vision executor doesn't try to upload them as GPU buffers. Call BEFORE
2356
+ * VisionExecutor.uploadWeights(). Missing scalars default to ±inf (clip = identity),
2357
+ * so a checkpoint without calibration still loads.
2358
+ */
2359
+ declare function patchGemma4VisionClips(graph: ModelGraph, weights: Map<string, {
2360
+ data: ArrayBufferView;
2361
+ shape: number[];
2362
+ }>): void;
2363
+ /**
2364
+ * Build the Gemma 4 ViT graph. Shaped by symbolic "N" (number of patches, runtime)
2365
+ * and "Np" (number of pooled tokens = ceil(grid_h/k)·ceil(grid_w/k), runtime),
2366
+ * resolved from input tensor dims — like the Qwen ViT's "N"/"Nm".
2367
+ */
2368
+ declare function generateGemma4VisionGraph(rawConfig: Record<string, unknown>): ModelGraph;
2369
+ //#endregion
2370
+ //#region src/gpu/architectures/gepard_tts.d.ts
2371
+ /** Per-channel FSQ cardinalities in head order (channel c = group⌊c/4⌋, dim c%4). */
2372
+ declare function gepardChannelLevels(): number[];
2373
+ /** Text-repetition layout config (stamped into gepard_config.json). */
2374
+ interface GepardTextRepetition {
2375
+ enabled: boolean;
2376
+ targetTextTokens: number;
2377
+ applyBelow: number;
2378
+ maxRepeats: number;
2379
+ }
2380
+ /** Parsed gepard_config.json — everything the host driver needs. */
2381
+ interface GepardConfig {
2382
+ /** The nested Qwen3.5 backbone config (fed to generateGepardBackboneGraph). */
2383
+ backboneConfig: Record<string, unknown>;
2384
+ /** Per-head vocab sizes in channel order (level_audio_0..31). */
2385
+ vocabSizes: number[];
2386
+ audioEmbedDim: number;
2387
+ startOfText: number;
2388
+ endOfText: number;
2389
+ startOfSpeech: number;
2390
+ textRepetition: GepardTextRepetition;
2391
+ /** Codec identity + geometry from the config's codec block. */
2392
+ codecId: string;
2393
+ fsqLevels: number[];
2394
+ sampleRate: number;
2395
+ }
2396
+ /**
2397
+ * Parse gepard_config.json. Head order is recovered from the numeric suffix of
2398
+ * each `level_audio_{i}` key (NOT the serialized key order), so the wiring is
2399
+ * immune to any JSON round-trip that re-sorts keys.
2400
+ */
2401
+ declare function parseGepardConfig(raw: Record<string, unknown>): GepardConfig;
2402
+ /**
2403
+ * Build the (possibly repeated) text-id layout — mirrors the reference
2404
+ * TextRepeater byte-for-byte (text_repetition.py):
2405
+ * R == 1 → [ SOT, *text, EOT, SOS ]
2406
+ * R > 1 → [ SOT, *text, EOT ] × (R−1) + [ SOT, *text, EOT, SOS ]
2407
+ * Only the final (canonical) copy carries SOS. R is deterministic from the
2408
+ * text-token count: ceil(target / n) capped at maxRepeats, applied only when
2409
+ * n < applyBelow (short prompts derail without the extra text mass).
2410
+ */
2411
+ declare function buildGepardTextLayout(textTokenIds: number[], cfg: GepardConfig): number[];
2412
+ /**
2413
+ * Dequantize per-channel FSQ codes [32, T] (channel-major) into the decoder
2414
+ * latent [32, T] f32: value = (code − L//2) / (L//2), matching
2415
+ * UnfoldedCodecModel.decode_from_codes / codec_ops.dequantize_codes.
2416
+ */
2417
+ declare function gepardCodesToLatent(codes: Uint32Array, numFrames: number): Float32Array;
2418
+ declare function generateGepardBackboneGraph(backboneConfig: Record<string, unknown>, dtype?: GraphDType, groupSizeOverride?: number, kvDtype?: KVDType): ModelGraph;
2419
+ //#endregion
2420
+ //#region src/gpu/architectures/kani_tts.d.ts
2421
+ /** Parsed KaniTTS2 backbone config (the LFM2 dims + the TTS-specific fields). */
2422
+ interface KaniConfig {
2423
+ textVocabSize: number;
2424
+ vocabSize: number;
2425
+ tokensPerFrame: number;
2426
+ audioStep: number;
2427
+ useLearnableRope: boolean;
2428
+ alphaMin: number;
2429
+ alphaMax: number;
2430
+ speakerEmbDim: number;
2431
+ audioTokensStart: number;
2432
+ startOfSpeech: number;
2433
+ endOfSpeech: number;
2434
+ codebookSize: number;
1728
2435
  /**
1729
- * Allocate activation buffers with liveness-based reuse.
1730
- *
1731
- * One dedicated buffer per activation tensor at full maxSeqLen is ~2.3GB for
1732
- * Qwen3.5-0.8B at T=512 — over the iOS jetsam budget on its own. Instead,
1733
- * a buffer returns to a size-keyed pool once its tensor's last reader has
1734
- * executed, so concurrently-live tensors share a small working set.
1735
- *
1736
- * Graph outputs and tensors read before they are written (cross-forward
1737
- * state) keep dedicated buffers. Within a forward, dispatches execute in
1738
- * executionOrder on every path (single-pass Dawn, per-dispatch WebKit), and
1739
- * WebGPU synchronizes hazards between dispatches, so reuse is safe.
1740
- *
1741
- * Caveat: debugReadBuffer() on an intermediate tensor is only meaningful
1742
- * before a later op reuses its buffer (probes that stop mid-graph are fine).
2436
+ * Whether the backbone uses KaniTTS2's frame-level position IDs (the 4 audio
2437
+ * tokens of a frame share one logical position) + learnable per-layer RoPE.
2438
+ * The `KaniTTS2ForCausalLM` checkpoints (e.g. kani-tts-2-en) do; the plain
2439
+ * `Lfm2ForCausalLM` family (e.g. the Apache kani-tts-450m-0.2-ft / kani-tts-370m)
2440
+ * does NOT those run standard sequential positions with α=1 plain RoPE.
2441
+ * Detected from the presence of `use_learnable_rope`/`tokens_per_frame`/the
2442
+ * `KaniTTS2ForCausalLM` architecture marker in config.json.
1743
2443
  */
1744
- private allocateActivationBuffers;
1745
- private allocateSSMStateBuffers;
1746
- private allocateKVCacheBuffers;
1747
- private resolveShapes;
1748
- private getBuffer;
2444
+ frameLevelPositions: boolean;
2445
+ }
2446
+ declare function parseKaniConfig(rawConfig: Record<string, unknown>): KaniConfig;
2447
+ /**
2448
+ * Convert the model's flat audio-token stream (the IDs between start/end-of-speech)
2449
+ * into NanoCodec codes [NUM_GROUPS, T]. Mirrors NemoAudioPlayer.get_nano_codes:
2450
+ * reshape [-1, 4]; codes[:,c] -= codebook_size*c; codes -= audio_tokens_start;
2451
+ * transpose → [4, T]. Returns a Uint32Array laid out group-major ([g*T + t]).
2452
+ */
2453
+ declare function audioTokensToCodes(audioTokenIds: number[], cfg?: KaniConfig): {
2454
+ codes: Uint32Array;
2455
+ numFrames: number;
2456
+ };
2457
+ interface NanoCodecGraphOptions {
2458
+ /** Number of audio frames T (the code grid width). PCM length = T * hop. */
2459
+ numFrames: number;
1749
2460
  /**
1750
- * Detect gate_proj + up_proj + SwiGLU patterns in decode entries and replace
1751
- * with a single fused SwiGLUMatVec dispatch. Saves 2 dispatches per MLP block.
2461
+ * Optional decoder geometry override. Defaults to the Kani 12.5 fps codec
2462
+ * (rates [7,7,6,3,2], input dim 16). Gepard's 21.5 fps codec is the SAME
2463
+ * CausalHiFiGANDecoder family with rates [8,8,4,2,2] and input dim 32.
1752
2464
  */
1753
- private fuseSwiGLUDecodeEntries;
1754
- /**
1755
- * Fuse two adjacent INT4 projections that share the same input activation and
1756
- * the same K/N (e.g. q_proj+gate_proj and k_proj+v_proj in full-attention
1757
- * decode) into a single DualMatVecInt4 dispatch. Reads the shared input vector
1758
- * once and writes both projection outputs removing one GPU round-trip (one
1759
- * submit+drain on Safari/iOS) per fused pair.
1760
- *
1761
- * Numerics are identical to running the two MatVecInt4 kernels separately
1762
- * (same dequant, same K-parallel reduction), so this is WebKit-safe: it reuses
1763
- * the proven INT4 matvec math and only merges two writes into one dispatch.
1764
- *
1765
- * Must run AFTER fuseSwiGLUDecodeEntries so the MLP gate+up pair (which is
1766
- * consumed by a SwiGLU node) is already collapsed and won't be matched here.
2465
+ geometry?: {
2466
+ upSampleRates: readonly number[];
2467
+ inputDim: number;
2468
+ };
2469
+ /**
2470
+ * When true the graph takes the dequantized FSQ latent directly as a host
2471
+ * input ("nc_latent" [inputDim, T] f32) instead of packed "audio_codes" u32 —
2472
+ * used by Gepard, whose 32 per-dimension codes dequantize trivially host-side.
1767
2473
  */
1768
- private fuseDualMatVecDecodeEntries;
2474
+ latentInput?: boolean;
2475
+ }
2476
+ declare function generateNanoCodecDecoderGraph(opts: NanoCodecGraphOptions): ModelGraph;
2477
+ /**
2478
+ * Generate the KaniTTS codec-LM backbone graph (LFM2-350M body, full-vocab logits,
2479
+ * per-layer learnable MRoPE). Mirrors generateLfm2Graph block-for-block; the only
2480
+ * difference is that each attention layer rotates Q/K with the MRoPE op fed a
2481
+ * per-layer host cos/sin table instead of the position-counter RoPE op.
2482
+ *
2483
+ * dtype:
2484
+ * • undefined / "f32" — full precision (the checkpoint is bf16/f32 → f32).
2485
+ * • "q4" — weight-only group-wise INT4 (W4A16) for the LFM2 linear projections
2486
+ * (q/k/v/o, gate/up/down, short-conv in/out_proj) via MatMulInt4. The tied
2487
+ * embedding + LM head stay at f32 on purpose: decoder-LM heads are the most
2488
+ * quantization-sensitive part of a codec-LM and the embed table is the single
2489
+ * buffer shared by both, so quantizing it would degrade audio quality for no
2490
+ * load-size win that the int4 projections do not already capture. Backbone
2491
+ * drops from ~1.5 GB (f32) to ~0.37 GB. The NanoCodec decoder is a separate
2492
+ * graph and always runs f32.
2493
+ */
2494
+ declare function generateKaniTtsGraph(rawConfig: Record<string, unknown>, dtype?: GraphDType, groupSize?: number, kvDtype?: KVDType): ModelGraph;
2495
+ //#endregion
2496
+ //#region src/gpu/architectures/moonshine.d.ts
2497
+ /**
2498
+ * Historical record of the executor-side work the keystone left open; all items
2499
+ * are now implemented (see the STATUS note above). Retained as exported
2500
+ * documentation of the dependency surface.
2501
+ */
2502
+ declare const MOONSHINE_REMAINING_WORK: readonly ["DONE: Transpose op kernel (conv output [C,L] → encoder input [L,C]).", "DONE: GroupNorm(num_groups=1) over conv channels (weight+bias).", "DONE: Tanh elementwise op (conv1 activation).", "DONE: Interleaved-RoPE variant (Moonshine rotates adjacent dim pairs 2p/2p+1).", "DONE: No-bias LayerNorm variant (Moonshine norms are weight-only).", "DONE: Per-utterance conv graph regeneration from the concrete sample count.", "DONE: Dual-graph executor — encoder once → frozen cross-attn K/V → AR decode.", "DONE: engine.transcribe(pcm) host path (MoonshineSTT): conv → encode → AR decode → detokenize."];
2503
+ interface MoonshineDims {
2504
+ hidden_size: number;
2505
+ enc_layers: number;
2506
+ dec_layers: number;
2507
+ num_heads: number;
2508
+ num_kv_heads: number;
2509
+ head_dim: number;
2510
+ rotary_dim: number;
2511
+ intermediate_size: number;
2512
+ vocab_size: number;
2513
+ rope_base: number;
2514
+ context_length: number;
2515
+ ln_eps: number;
2516
+ }
2517
+ /** Pull and derive Moonshine dimensions from the raw HF config. */
2518
+ declare function parseMoonshineConfig(raw: Record<string, unknown>): MoonshineDims;
2519
+ /** Number of encoder frames produced by the conv frontend for n_samples PCM. */
2520
+ declare function moonshineEncoderFrames(nSamples: number): number;
2521
+ /**
2522
+ * Encoder graph: raw-waveform conv frontend + bidirectional transformer.
2523
+ * @param nSamples concrete PCM sample count (the conv frontend is length-static).
2524
+ * The output tensor "encoder_out" is [T_frames, hidden] and is consumed (after
2525
+ * per-layer K/V projection) as the frozen K/V for the decoder's cross-attention.
2526
+ */
2527
+ declare function generateMoonshineEncoderGraph(raw: Record<string, unknown>, nSamples: number): ModelGraph;
2528
+ /**
2529
+ * Decoder graph: AR transformer with causal self-attn (KV-cache) + cross-attn to
2530
+ * the frozen encoder output. Built for a single decode step (T=1). The encoder K/V
2531
+ * are supplied as graph inputs "enc_k_layer{i}" / "enc_v_layer{i}" — the host
2532
+ * pre-projects the encoder output through each layer's encoder_attn.k_proj/v_proj
2533
+ * ONCE and binds them frozen for the whole decode (the CrossAttention contract).
2534
+ */
2535
+ declare function generateMoonshineDecoderGraph(raw: Record<string, unknown>, sEnc: number): ModelGraph;
2536
+ //#endregion
2537
+ //#region src/gpu/architectures/outetts.d.ts
2538
+ /** Parsed OuteTTS backbone config (the Qwen3 dims). Reuses the generic Qwen path. */
2539
+ interface OuteTtsConfig {
2540
+ hidden_size: number;
2541
+ num_layers: number;
2542
+ num_heads: number;
2543
+ num_kv_heads: number;
2544
+ head_dim: number;
2545
+ intermediate_size: number;
2546
+ vocab_size: number;
2547
+ rope_base: number;
2548
+ rms_norm_eps: number;
2549
+ max_position_embeddings: number;
2550
+ }
2551
+ declare function parseOuteTtsConfig(rawConfig: Record<string, unknown>): OuteTtsConfig;
2552
+ /**
2553
+ * Convert the flat generated token stream (the run BETWEEN <|audio_start|> and
2554
+ * <|audio_end|>) into a DAC code grid [NUM_CODEBOOKS, T] (codebook-major [c*T + t]).
2555
+ *
2556
+ * Mirrors outetts v3 `extract_audio_from_tokens`: filter the stream into two ordered
2557
+ * lists by membership of the c1 / c2 ranges, recover the raw code
2558
+ * (id − base), drop the reserved index 1024, and truncate to the shorter list. The
2559
+ * model emits c1,c2 interleaved per frame, but the parse is membership-based (it does
2560
+ * not assume strict pairing) so a stray token cannot desync the grid.
2561
+ */
2562
+ declare function audioTokensToDacCodes(tokenIds: number[]): {
2563
+ codes: Uint32Array;
2564
+ numFrames: number;
2565
+ };
2566
+ interface DacSpeechGraphOptions {
2567
+ /** Number of audio frames T (the code-grid width). */
2568
+ numFrames: number;
1769
2569
  /**
1770
- * Fuse the adjacent K-cache and V-cache appends in each full-attention layer
1771
- * into a single DualKVCacheAppend dispatch. Both are pure memcpys into f32
1772
- * caches sharing the same width and dst_offset, so one dispatch with two
1773
- * src/dst buffers writes both — removing one GPU round-trip per layer.
1774
- *
1775
- * Supports f32, native-f16, and packed-f16 caches (the dual kernel mirrors the
1776
- * single-append kernel selected for the active kvMode). Numerically identical
1777
- * to the separate appends — WebKit-safe (the packed-f16 variant is the Safari
1778
- * path and uses pack2x16float, no `enable f16`).
2570
+ * Number of RVQ codebooks. Default 2 (DAC.speech.v1.0). Parler-TTS's
2571
+ * dac_44khz_8kbps uses 9 the RVQDequant kernel loops over NCB so the only
2572
+ * change is the codebook/out_proj tensor first-dim and the codes grid height.
1779
2573
  */
1780
- private fuseDualKVCacheAppendEntries;
2574
+ numCodebooks?: number;
1781
2575
  /**
1782
- * Fuse the attention SigmoidGate (attn_out * sigmoid(gate)) into the INT4
1783
- * o_proj that consumes it: a GatedMatVecInt4 reads attn_out and gate directly,
1784
- * applies the sigmoid gate to its input vector, and runs the projection in ONE
1785
- * dispatch — removing the standalone SigmoidGate (one round-trip per
1786
- * full-attention layer).
1787
- *
1788
- * Numerically identical to SigmoidGate→MatVecInt4 (same gate formula, same INT4
1789
- * dequant + reduction). Slight extra ALU: the gated input is recomputed per
1790
- * output column, but A reads hit L1 and the saved submit+drain dominates on
1791
- * mobile. WebKit risk: low — reuses the proven INT4 matvec, only the A vector
1792
- * is built from two reads + a sigmoid (no new reduction/barrier pattern).
1793
- *
1794
- * Runs after the dual fusions so it only sees the post-attention SigmoidGate.
2576
+ * Decoder upsampling rates (the DecoderBlock strides). Default [8,5,4,2]
2577
+ * (DAC.speech.v1.0). Parler's dac_44khz uses [8,8,4,2] (hop 512). Channels
2578
+ * still halve per stage from `decoderDim`.
1795
2579
  */
1796
- private fuseGatedOProjDecodeEntries;
2580
+ decoderRates?: readonly number[];
2581
+ /** First-conv output channels (decoder_dim). Default 1536. */
2582
+ decoderDim?: number;
2583
+ /** Quantizer out_proj output / first-conv input dim (latent_dim). Default 1024. */
2584
+ latentDim?: number;
2585
+ }
2586
+ declare function generateDacSpeechDecoderGraph(opts: DacSpeechGraphOptions): ModelGraph;
2587
+ /**
2588
+ * Final decoded PCM length (samples) for a T-frame code grid, via the DAC stage chain.
2589
+ * `rates` defaults to DAC.speech.v1.0 ([8,5,4,2]); pass [8,8,4,2] for Parler's dac_44khz.
2590
+ */
2591
+ declare function dacOutputLength(numFrames: number, rates?: readonly number[]): number;
2592
+ declare function generateOuteTtsBackboneGraph(rawConfig: Record<string, unknown>, dtype?: GraphDType, groupSize?: number, kvDtype?: KVDType): ModelGraph;
2593
+ //#endregion
2594
+ //#region src/gpu/architectures/qwen3_5_vision.d.ts
2595
+ /**
2596
+ * Build the ViT graph. The graph is shaped by symbolic "N" (number of patches),
2597
+ * resolved at run time from the input tensor's first dim — exactly like the LM's
2598
+ * symbolic "T".
2599
+ */
2600
+ declare function generateQwen3_5VisionGraph(rawConfig: Record<string, unknown>): ModelGraph;
2601
+ //#endregion
2602
+ //#region src/gpu/gepard-tts.d.ts
2603
+ /**
2604
+ * Gepard-1.0 — native streaming text-to-speech engine for Gerbil's WebGPU backend.
2605
+ *
2606
+ * Gepard (nineninesix/gepard-1.0, Apache-2.0) is a frame-synchronous multihead
2607
+ * codec-LM TTS. Unlike the token-serial Kani/Oute engines (which emit one codec
2608
+ * token per forward), Gepard emits ONE WHOLE AUDIO FRAME per backbone forward:
2609
+ *
2610
+ * 1. BACKBONE (GPU): stock Qwen3.5 full-attention body (14 layers, hidden 1024)
2611
+ * run through generateGepardBackboneGraph — input is a host-written
2612
+ * `inputs_embeds` row, output is the last position's final-norm hidden state.
2613
+ * 2. OVERLAY (host): 32 parallel classifier heads (cardinalities [8,7,6,6]×8,
2614
+ * 216 logits total) + a binary stop head read the hidden state; the sampled
2615
+ * 32 FSQ codes feed back through per-channel embeddings → concat → GELU MLP
2616
+ * → affine-free LayerNorm → × audio_embed_scale as the next step's input.
2617
+ * 3. CODEC (GPU): NanoCodec 21.5 fps decoder (same validated CausalHiFiGAN
2618
+ * graph family as Kani's codec, rates [8,8,4,2,2], hop 1024) turns the
2619
+ * per-channel codes into 22.05 kHz PCM — decoded incrementally in chunks
2620
+ * with left-context lookback, so audio streams out while the model is
2621
+ * still generating (≈1.5 s of audio per 32-frame chunk).
2622
+ *
2623
+ * Mirrors gepard-inference (runner.py) semantics: adaptive text repetition,
2624
+ * SOS-seeded first frame, stop-head termination, independent per-head sampling
2625
+ * (temperature / top-k / windowed repetition penalty) in fp32. Voice cloning
2626
+ * (the ref_compressor speaker prefix) is NOT wired yet — the default voice ships
2627
+ * first; cloning is a documented follow-up.
2628
+ */
2629
+ /** Backbone weight precision for the codec-LM. */
2630
+ type GepardDType = "q4" | "f32";
2631
+ interface GepardTTSOptions {
2632
+ /** Backbone repo (default nineninesix/gepard-1.0, Apache-2.0). */
2633
+ repo?: string;
2634
+ /** NanoCodec repo (default the mlx-community 21.5 fps MLX mirror). */
2635
+ codecRepo?: string;
2636
+ revision?: string;
2637
+ hfToken?: string;
2638
+ cacheDir?: string;
1797
2639
  /**
1798
- * Fuse a standalone SwiGLU (silu(gate) * up) into the INT4 projection that
1799
- * consumes its output: a SwiGLUGatedMatVecInt4 reads gate and up directly,
1800
- * builds the gated input vector, and runs the projection in ONE dispatch.
1801
- * Targets the Mamba block's mamba_swiglu (silu(z) * norm_out) feeding out_proj
1802
- * one round-trip saved per linear-attention layer.
1803
- *
1804
- * The MLP SwiGLU is already collapsed into a SwiGLUMatVec entry by
1805
- * fuseSwiGLUDecodeEntries (it has no surviving standalone SwiGLU node), so only
1806
- * the Mamba SwiGLU matches here. Numerically identical to SwiGLU→MatVecInt4;
1807
- * WebKit risk low (reuses the proven INT4 matvec, only the A vector changes).
2640
+ * Backbone weight precision (default "q4"):
2641
+ * "q4" — group-wise INT4 (W4A16) on the Qwen3.5 linear projections
2642
+ * (backbone ~1.05 GB f32 ~0.13 GB). Recommended default.
2643
+ * "f32" full precision (largest, highest fidelity).
2644
+ * The overlay always runs f32 on the host; the codec decoder always runs f32.
1808
2645
  */
1809
- private fuseSwiGLUGatedProjDecodeEntries;
2646
+ dtype?: GepardDType;
2647
+ /** Max KV-cache length: text layout + 1 position per audio frame. Default 2048. */
2648
+ maxSeqLen?: number;
2649
+ onProgress?: (loaded: number, total: number, message: string) => void;
2650
+ }
2651
+ /** One streamed audio chunk (decoded incrementally during generation). */
2652
+ interface GepardAudioChunk {
2653
+ /** Mono PCM in [-1, 1] for this chunk only. */
2654
+ pcm: Float32Array;
2655
+ /** Sample rate (22050). */
2656
+ sampleRate: number;
2657
+ /** Index of the first audio frame this chunk covers. */
2658
+ frameIndex: number;
2659
+ /** True for the last chunk of the utterance. */
2660
+ isFinal: boolean;
2661
+ }
2662
+ interface GepardSpeakOptions {
2663
+ /** Sampling temperature (default 0.4 — the reference production operating point). */
2664
+ temperature?: number;
2665
+ /** Top-k cutoff per head (default 0 = disabled). */
2666
+ topK?: number;
2667
+ /** Stop-head sigmoid threshold (default 0.5). */
2668
+ stopThreshold?: number;
2669
+ /** Max audio frames to generate (caps duration; default 2000, ~93 s). */
2670
+ maxFrames?: number;
2671
+ /** Per-head repetition penalty over a recent-frame window (default 1.0 = off). */
2672
+ repetitionPenalty?: number;
2673
+ /** Number of recent frames tracked for repetitionPenalty (default 32). */
2674
+ repetitionWindow?: number;
1810
2675
  /**
1811
- * Fuse two adjacent per-row RMSNorms sharing hidden_size + eps into a single
1812
- * DualRMSNorm dispatch (e.g. the per-head q_norm and k_norm in full-attention
1813
- * decode). One workgroup still handles one row; the fused grid just spans both
1814
- * inputs' rows, so each row's reduction is unchanged — numerically identical to
1815
- * two separate RMSNorm dispatches. One round-trip saved per fused pair.
1816
- *
1817
- * WebKit risk: low — same single-workgroup reduction as the proven RMSNorm
1818
- * kernel, only the row→input routing is added.
2676
+ * Streaming callback: called with each decoded audio chunk while generation is
2677
+ * still running (frame-synchronous decode real streaming, not post-hoc
2678
+ * chunking). The final chunk carries `isFinal: true`. The returned SpeakResult
2679
+ * still contains the full assembled PCM.
1819
2680
  */
2681
+ onChunk?: (chunk: GepardAudioChunk) => void;
2682
+ }
2683
+ interface GepardSpeakResult {
2684
+ /** Mono PCM in [-1, 1]. */
2685
+ pcm: Float32Array;
2686
+ /** Sample rate (22050). */
2687
+ sampleRate: number;
2688
+ /** Number of audio frames decoded. */
2689
+ frames: number;
2690
+ /** Audio duration in seconds. */
2691
+ audioSeconds: number;
2692
+ }
2693
+ declare class GepardTTS {
2694
+ private ctx;
2695
+ private loaded;
2696
+ private tokenizer;
2697
+ private cfg;
2698
+ private maxSeqLen;
2699
+ private hidden;
2700
+ /** Backbone executor (built once; reused across speak() calls). */
2701
+ private backboneExec;
2702
+ /** Cached codec executor for the steady-state window size (chunk + lookback). */
2703
+ private codecExecCache;
2704
+ private _destroyed;
2705
+ /** The selected backbone precision ("q4" or "f32"). */
2706
+ readonly dtype: GepardDType;
2707
+ private audioEmbTables;
2708
+ private projW0;
2709
+ private projB0;
2710
+ private projW2;
2711
+ private projB2;
2712
+ private audioEmbedScale;
2713
+ private headW;
2714
+ private headB;
2715
+ private stopW;
2716
+ private stopB;
2717
+ private constructor();
2718
+ static create(options?: GepardTTSOptions): Promise<GepardTTS>;
2719
+ /** NanoCodec's fixed output sample rate (Hz). */
2720
+ get sampleRate(): number;
2721
+ /** Preset voices. Gepard ships one default voice (cloning is a follow-up). */
2722
+ availableVoices(): string[];
2723
+ /** Gather text-token rows from the raw BF16 embed table → f32 [T, hidden]. */
2724
+ private embedTextTokens;
1820
2725
  /**
1821
- * Fuse [CausalConv1dSiLU|CausalConv1dGated ConvStateUpdate] into one
1822
- * dispatch (decode entries only). At T=1 the conv read and the rolling-state
1823
- * write for a channel live in the same thread, so the fused kernel is
1824
- * race-free; prefill (T>1) keeps the two-dispatch path.
2726
+ * Embed one audio frame (32 FSQ codes) through the overlay stack:
2727
+ * per-channel lookup concat Linear→GELU→Linear affine-free LayerNorm
2728
+ * × audio_embed_scale. Mirrors GepardModel._embed_audio.
1825
2729
  */
1826
- private fuseConvStateUpdateDecodeEntries;
2730
+ private embedAudioFrame;
2731
+ /** Stop-head probability for a hidden state. */
2732
+ private stopProbability;
1827
2733
  /**
1828
- * Fuse [per-head q_norm RMSNorm, k_norm RMSNorm, in-place RoPE] into ONE
1829
- * DualRMSNormRoPE dispatch. Numerically identical (norm then rotate, same
1830
- * math); requires head_dim <= 256 and RoPE operating in-place on exactly the
1831
- * two norm outputs. Saves 2 dispatches per full-attention layer — a mobile
1832
- * (WebKit submit-per-dispatch) win, desktop-neutral.
2734
+ * Sample one audio frame: all 32 heads independently in fp32
2735
+ * windowed repetition penalty temperature top-k → softmax → multinomial.
2736
+ * Mirrors GepardRunner._sample_frame / _sample_head_logits.
1833
2737
  */
2738
+ private sampleFrame;
1834
2739
  /**
1835
- * Fuse [Add RMSNorm] into a single ResidualRMSNorm dispatch: the add's sum
1836
- * is still materialized (sum_output downstream residual chain reads it) and
1837
- * the norm result is written in the same dispatch. Numerically identical.
2740
+ * Synthesize speech for `text`. Returns 22.05 kHz mono PCM; when
2741
+ * `opts.onChunk` is set, audio streams out chunk-by-chunk during generation.
2742
+ *
2743
+ * Pipeline: adaptive-repetition text layout → prefill (host-gathered text
2744
+ * embeddings) → frame-synchronous AR loop (one frame = 32 codes per forward,
2745
+ * stop head decides termination) → incremental NanoCodec decode → PCM.
1838
2746
  */
1839
- private fuseAddRMSNormDecodeEntries;
1840
- private fuseDualRMSNormRoPEDecodeEntries;
1841
- private fuseDualRMSNormDecodeEntries;
2747
+ speak(text: string, opts?: GepardSpeakOptions): Promise<GepardSpeakResult>;
1842
2748
  /**
1843
- * Gather buffer entries for a bind group, matching the kernel spec's binding layout.
1844
- * Uses the pre-allocated inputIdsBuffer for the "input_ids" tensor.
2749
+ * Decode frames [from, to) of the utterance to PCM, re-decoding up to
2750
+ * `GEPARD_CODEC_LOOKBACK_FRAMES` of left context as warm-up (discarded).
2751
+ * The decoder is causal with a bounded receptive field, so chunked output
2752
+ * matches a monolithic decode.
1845
2753
  */
1846
- private gatherBuffers;
1847
- /** Lazily allocate a scratch storage buffer at least `minBytes` large. */
1848
- private getBindingScratchBuffer;
2754
+ decodeFrameRange(frames: Uint32Array[], from: number, to: number): Promise<Float32Array>;
2755
+ /** Run the codec graph for one latent window raw PCM (pre-clamp). */
2756
+ private decodeLatentWindow;
2757
+ destroy(): void;
1849
2758
  }
1850
2759
  //#endregion
1851
2760
  //#region src/gpu/moonshine-executor.d.ts
@@ -2252,6 +3161,16 @@ interface GenerateResult {
2252
3161
  totalTime: number;
2253
3162
  /** Why generation stopped. */
2254
3163
  finishReason: "eos" | "max_tokens" | "stop_sequence";
3164
+ /**
3165
+ * Decode-only wall time in ms (excludes prefill). Set by the batched path
3166
+ * (generateBatch) so aggregate decode throughput is measurable directly.
3167
+ */
3168
+ decodeTime?: number;
3169
+ /**
3170
+ * Raw generated token ids, in order. Used by parity harnesses (e.g. the
3171
+ * paged-KV token-exactness gate) to compare runs without decode ambiguity.
3172
+ */
3173
+ tokenIds?: number[];
2255
3174
  /** Thinking content if model produced it (future). */
2256
3175
  thinking?: string;
2257
3176
  }
@@ -2364,8 +3283,14 @@ declare class WebGPUEngine {
2364
3283
  private _outeVoice;
2365
3284
  /** Source of the runtime LoRA adapter currently applied on the static base, if any. */
2366
3285
  private _currentAdapter;
3286
+ /** Named adapters registered for per-lane batched decode (GERBIL_BATCH_LORA). */
3287
+ private _batchAdapters;
2367
3288
  /** Lazily-created Parler-TTS engine (Flan-T5 encoder + decoder LM + dac_44khz). */
2368
3289
  private _parlerTTS;
3290
+ /** Lazily-created Gepard-1.0 engine (Qwen3.5 multihead codec-LM + NanoCodec 21.5 fps). */
3291
+ private _gepardTTS;
3292
+ /** Lazily-created continuous-batching scheduler (GERBIL_BATCH, Phase 5). */
3293
+ private _scheduler;
2369
3294
  /**
2370
3295
  * WebKit group-size probe state. When true, a candidate group size is being
2371
3296
  * tried this page-load and must be promoted (or capped) after the FIRST
@@ -2423,6 +3348,17 @@ declare class WebGPUEngine {
2423
3348
  loadAdapter(source: string | null): Promise<void>;
2424
3349
  /** The runtime LoRA adapter currently applied on the base, or null. */
2425
3350
  getAdapter(): string | null;
3351
+ /**
3352
+ * Register a named LoRA adapter for per-lane batched decode (SGMV phase 1,
3353
+ * `GERBIL_BATCH=N` + `GERBIL_BATCH_LORA=1`). The adapter's factors are
3354
+ * fetched and packed into the executor's shared factor buffers ONCE; any
3355
+ * number of batch lanes can then run it concurrently by passing its name in
3356
+ * `generateBatch`'s per-lane `adapters` option. Registering the same name
3357
+ * twice is a no-op.
3358
+ */
3359
+ registerAdapter(name: string, source: string): Promise<void>;
3360
+ /** Names of the adapters registered for per-lane batched decode. */
3361
+ getRegisteredAdapters(): string[];
2426
3362
  /**
2427
3363
  * Write a coarse crash-phase breadcrumb that survives a GPU-process kill / page
2428
3364
  * reload. The iPad harness reads `localStorage["gerbil-crash-phase"]` after a
@@ -2501,6 +3437,55 @@ declare class WebGPUEngine {
2501
3437
  * the same instance awaits this one (see {@link _genQueueTail}).
2502
3438
  */
2503
3439
  generate(prompt: string | ChatMessage[], options?: GenerateOptions): Promise<GenerateResult>;
3440
+ /**
3441
+ * Batched lockstep generation (GERBIL_BATCH=N — Phase 2 of the
3442
+ * continuous-batching campaign, Dawn only).
3443
+ *
3444
+ * Prefills each prompt sequentially into its batch slot (KV into the slot's
3445
+ * block-table row, SSM/conv state adopted into the slot's state-pool rows),
3446
+ * then decodes all N sequences in lockstep: one batched dispatch stream per
3447
+ * step produces one token per row. Greedy-only in Phase 2 (per-row GPU
3448
+ * argmax); rows that hit EOS/stop keep decoding in lockstep with their
3449
+ * output discarded until every row has finished — rows never interact, so
3450
+ * per-row streams are token-exact vs single-sequence generation.
3451
+ *
3452
+ * Requires exactly `executor.batchSize` prompts (fixed lockstep batch; the
3453
+ * Phase 3 scheduler will lift this).
3454
+ */
3455
+ generateBatch(prompts: Array<string | ChatMessage[]>, options?: GenerateOptions & {
3456
+ maxTokensPerRow?: number[];
3457
+ /**
3458
+ * Per-lane adapter names (SGMV phase 1 — requires GERBIL_BATCH_LORA=1
3459
+ * and prior {@link registerAdapter} calls). `adapters[i]` selects the
3460
+ * adapter lane i decodes with; null/undefined = bare base. Lanes with
3461
+ * different adapters (or none) decode concurrently over the shared base.
3462
+ */
3463
+ adapters?: Array<string | null | undefined>;
3464
+ }): Promise<GenerateResult[]>;
3465
+ /**
3466
+ * Continuous-batching scheduler (GERBIL_BATCH=N — Phase 5 of the batching
3467
+ * campaign, Dawn only). Returns the engine's request scheduler (created on
3468
+ * first call): submit many requests with independent prompts/limits and the
3469
+ * scheduler multiplexes them over the batch lanes with dynamic admission,
3470
+ * immediate lane retirement, and chunked prefill. Greedy-only; per-request
3471
+ * output is token-exact vs single-sequence generate().
3472
+ */
3473
+ createScheduler(): RequestScheduler;
3474
+ /**
3475
+ * Create a swarm — the MoTA mode-B primitive (docs/research/slm-swarms.md):
3476
+ * named members (LoRA adapters on this engine's shared base, or null for the
3477
+ * bare base), an optional plan that fans an input out into per-member
3478
+ * sub-tasks, and an optional reduce that joins the results. `swarm.run()`
3479
+ * decodes every sub-task CONCURRENTLY through the continuous-batching
3480
+ * scheduler with per-request adapters — one batched pass, so the fan-out
3481
+ * costs roughly the slowest member, not the sum.
3482
+ *
3483
+ * Every non-null member source is registered via {@link registerAdapter}
3484
+ * (downloads in parallel; factors packed into shared GPU buffers once).
3485
+ * Requires `GERBIL_BATCH=N` (N >= 2) + `GERBIL_BATCH_LORA=1` on the Dawn
3486
+ * path; greedy-only like the scheduler underneath.
3487
+ */
3488
+ createSwarm<R = Record<string, ServeResult>>(options: SwarmOptions<R>): Promise<Swarm<R>>;
2504
3489
  /**
2505
3490
  * Resolve the end-of-turn stop token id, or null if the model has none.
2506
3491
  *
@@ -2652,6 +3637,21 @@ declare class WebGPUEngine {
2652
3637
  * default description is used.
2653
3638
  */
2654
3639
  describeVoice?: string;
3640
+ /**
3641
+ * Stop-head sigmoid threshold (Gepard only, default 0.5). Higher = later stop.
3642
+ */
3643
+ stopThreshold?: number;
3644
+ /**
3645
+ * Streaming audio callback (Gepard only). Gepard is frame-synchronous, so
3646
+ * audio is decoded and delivered chunk-by-chunk WHILE generation runs — the
3647
+ * callback fires with each ~1.5 s PCM chunk (the final one has isFinal:true).
3648
+ */
3649
+ onChunk?: (chunk: {
3650
+ pcm: Float32Array;
3651
+ sampleRate: number;
3652
+ frameIndex: number;
3653
+ isFinal: boolean;
3654
+ }) => void;
2655
3655
  }): Promise<{
2656
3656
  pcm: Float32Array;
2657
3657
  sampleRate: number;
@@ -2666,6 +3666,14 @@ declare class WebGPUEngine {
2666
3666
  private speakParler;
2667
3667
  /** A random natural-language voice description for Parler-TTS (host template bank). */
2668
3668
  randomVoiceDescription(): string;
3669
+ /**
3670
+ * Gepard-1.0 speak path: lazily build the Gepard engine (stock Qwen3.5
3671
+ * full-attention codec-LM + 32 host-side FSQ classifier heads + NanoCodec
3672
+ * 21.5 fps decoder) and synthesize `text`. Frame-synchronous: when `onChunk`
3673
+ * is set, audio streams out during generation. Single default voice for now
3674
+ * (voice cloning via the ref-compressor is a documented follow-up).
3675
+ */
3676
+ private speakGepard;
2669
3677
  /**
2670
3678
  * OuteTTS speak path: lazily build the OuteTTS engine (loading the chosen preset
2671
3679
  * voice's speaker JSON + the folded DAC codec weights from the mirror), rebuilding
@@ -2845,5 +3853,5 @@ declare class WebGPUEngine {
2845
3853
  private _acquireGenLock;
2846
3854
  }
2847
3855
  //#endregion
2848
- export { GEMMA4_IMAGE_PROCESSOR as $, generateDacSpeechDecoderGraph as A, AdapterConfig as At, generateNanoCodecDecoderGraph as B, SpeakOptions as Bt, TranscribeOptions as C, LoadedMoonshine as Ct, generateQwen3_5VisionGraph as D, quantizeKaniBackbone as Dt, Executor as E, loadMoonshine as Et, generateMoonshineEncoderGraph as F, fetchAdapter as Ft, generateGemma4VisionGraph as G, KVDType as Gt, Gemma4VisionGraphInfo as H, GPUDiagnosticResult as Ht, moonshineEncoderFrames as I, splitFusedQGateDelta as It, DEFAULT_MODELS as J, ModelCapabilities as Jt, patchGemma4VisionClips as K, KvMode as Kt, parseMoonshineConfig as L, KaniDType as Lt, parseOuteTtsConfig as M, LoRADelta as Mt, MOONSHINE_REMAINING_WORK as N, applyLoRAToStore as Nt, audioTokensToDacCodes as O, ChatMessage as Ot, generateMoonshineDecoderGraph as P, buildLoRADeltas as Pt, resolveDefaultRepo as Q, audioTokensToCodes as R, KaniTTS as Rt, MoonshineSTTOptions as S, LoadedKaniTTS as St, MoonshineEncoderExecutor as T, loadModel as Tt, dequantizeGemma4VisionProjection as U, initGPU as Ut, parseKaniConfig as V, SpeakResult as Vt, dequantizeMLXProjection as W, GraphDType as Wt, OUTETTS_PRESET_VOICES as X, OUTETTS_ASSETS as Y, createDefaultHFKeyMapper as Yt, isTTSRepo as Z, ParlerSpeakOptions as _, mropeFreqDims as _t, GenerateObjectOptions as a, VisionGridConfig as at, ParlerTTSOptions as b, smartResize as bt, GenerateResult as c, buildGemma4PosEmbeds as ct, ObjectSchema as d, buildMRoPECosSin as dt, Gemma4VisionGridConfig as et, ObjectValidator as f, buildMRoPEPositionIds as ft, VisionInputs as g, buildVisionPositionTensors as gt, VisionExecutor as h, buildRotaryCosSin as ht, EncodeImageResult as i, QWEN3_5_IMAGE_PROCESSOR as it, generateOuteTtsBackboneGraph as j, AdapterSource as jt, dacOutputLength as k, Tokenizer as kt, IntegrityCheckEntry as l, buildGemma4RotaryCosSin as lt, WebGPUEngineOptions as m, buildPositionIds as mt, AgentTool as n, ImageProcessorConfig as nt, GenerateObjectResult as o, VisionPositionTensors as ot, WebGPUEngine as p, buildPosEmbeds as pt, resolveGemma4VisionInfo as q, ModelArchConfig as qt, EmbedOptions as r, PreprocessedImage as rt, GenerateOptions as s, buildGemma4PoolMatrix as st, AgentStep as t, Gemma4VisionPositionTensors as tt, IntegrityCheckResult as u, buildGemma4VisionPositionTensors as ut, ParlerSpeakResult as v, preprocessImage as vt, TranscribeResult as w, loadKaniTTS as wt, MoonshineSTT as x, SamplingParams as xt, ParlerTTS as y, preprocessImageGemma4 as yt, generateKaniTtsGraph as z, KaniTTSOptions as zt };
2849
- //# sourceMappingURL=index-h8TDu1qm.d.mts.map
3856
+ export { Gemma4VisionGraphInfo as $, LoRADelta as $t, GepardTTS as A, preprocessImageGemma4 as At, generateMoonshineEncoderGraph as B, ServeOptions as Bt, TranscribeOptions as C, buildMRoPEPositionIds as Ct, GepardDType as D, buildVisionPositionTensors as Dt, GepardAudioChunk as E, buildRotaryCosSin as Et, generateDacSpeechDecoderGraph as F, SwarmOptions as Ft, generateNanoCodecDecoderGraph as G, loadKaniTTS as Gt, parseMoonshineConfig as H, Executor as Ht, generateOuteTtsBackboneGraph as I, SwarmTask as It, buildGepardTextLayout as J, quantizeKaniBackbone as Jt, parseKaniConfig as K, loadModel as Kt, parseOuteTtsConfig as L, SwarmTaskOptions as Lt, generateQwen3_5VisionGraph as M, SamplingParams as Mt, audioTokensToDacCodes as N, Swarm as Nt, GepardSpeakOptions as O, mropeFreqDims as Ot, dacOutputLength as P, SwarmMemberResult as Pt, parseGepardConfig as Q, AdapterSource as Qt, MOONSHINE_REMAINING_WORK as R, RequestScheduler as Rt, MoonshineSTTOptions as S, buildMRoPECosSin as St, MoonshineEncoderExecutor as T, buildPositionIds as Tt, audioTokensToCodes as U, LoadedKaniTTS as Ut, moonshineEncoderFrames as V, ServeResult as Vt, generateKaniTtsGraph as W, LoadedMoonshine as Wt, gepardChannelLevels as X, Tokenizer as Xt, generateGepardBackboneGraph as Y, ChatMessage as Yt, gepardCodesToLatent as Z, AdapterConfig as Zt, ParlerSpeakOptions as _, VisionPositionTensors as _t, GenerateObjectOptions as a, KaniTTS as an, DEFAULT_MODELS as at, ParlerTTSOptions as b, buildGemma4RotaryCosSin as bt, GenerateResult as c, SpeakResult as cn, isTTSRepo as ct, ObjectSchema as d, GraphDType as dn, Gemma4VisionGridConfig as dt, applyLoRAToStore as en, dequantizeGemma4VisionProjection as et, ObjectValidator as f, KVDType as fn, Gemma4VisionPositionTensors as ft, VisionInputs as g, createDefaultHFKeyMapper as gn, VisionGridConfig as gt, VisionExecutor as h, ModelCapabilities as hn, QWEN3_5_IMAGE_PROCESSOR as ht, EncodeImageResult as i, KaniDType as in, resolveGemma4VisionInfo as it, GepardTTSOptions as j, smartResize as jt, GepardSpeakResult as k, preprocessImage as kt, IntegrityCheckEntry as l, GPUDiagnosticResult as ln, resolveDefaultRepo as lt, WebGPUEngineOptions as m, ModelArchConfig as mn, PreprocessedImage as mt, AgentTool as n, fetchAdapter as nn, generateGemma4VisionGraph as nt, GenerateObjectResult as o, KaniTTSOptions as on, OUTETTS_ASSETS as ot, WebGPUEngine as p, KvMode as pn, ImageProcessorConfig as pt, GepardConfig as q, loadMoonshine as qt, EmbedOptions as r, splitFusedQGateDelta as rn, patchGemma4VisionClips as rt, GenerateOptions as s, SpeakOptions as sn, OUTETTS_PRESET_VOICES as st, AgentStep as t, buildLoRADeltas as tn, dequantizeMLXProjection as tt, IntegrityCheckResult as u, initGPU as un, GEMMA4_IMAGE_PROCESSOR as ut, ParlerSpeakResult as v, buildGemma4PoolMatrix as vt, TranscribeResult as w, buildPosEmbeds as wt, MoonshineSTT as x, buildGemma4VisionPositionTensors as xt, ParlerTTS as y, buildGemma4PosEmbeds as yt, generateMoonshineDecoderGraph as z, SchedulerStats as zt };
3857
+ //# sourceMappingURL=index-Fj2XkP-o.d.mts.map