@mlx-bun/inference 0.0.0-stage → 1.0.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 (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1475 -2
  3. package/THIRD_PARTY_NOTICES.md +3404 -0
  4. package/dist/native/libmlx_bun_ane.dylib +0 -0
  5. package/dist/native/libmlx_bun_expert_io.dylib +0 -0
  6. package/dist/native/mlx-bun-frame-extract +0 -0
  7. package/examples/fit-model.ts +25 -0
  8. package/examples/qwen3-forward.ts +42 -0
  9. package/examples/qwen3-generate.ts +28 -0
  10. package/examples/trellis-expand.ts +23 -0
  11. package/package.json +129 -4
  12. package/src/adapters/available.ts +110 -0
  13. package/src/adapters/index.ts +3 -0
  14. package/src/adapters/manager.ts +316 -0
  15. package/src/artifacts/auxiliary-files.ts +45 -0
  16. package/src/artifacts/config-dialects.ts +94 -0
  17. package/src/artifacts/config.ts +341 -0
  18. package/src/artifacts/draft-vocabulary.ts +30 -0
  19. package/src/artifacts/expert-offload-build.ts +75 -0
  20. package/src/artifacts/expert-offload.ts +89 -0
  21. package/src/artifacts/experts/index.ts +3 -0
  22. package/src/artifacts/experts/io.ts +386 -0
  23. package/src/artifacts/experts/residency.ts +1134 -0
  24. package/src/artifacts/experts/usage.ts +415 -0
  25. package/src/artifacts/fingerprint.ts +44 -0
  26. package/src/artifacts/glm52/container.ts +305 -0
  27. package/src/artifacts/glm52/expert-layout.ts +249 -0
  28. package/src/artifacts/glm52/index.ts +7 -0
  29. package/src/artifacts/glm52/kernel-layout.ts +124 -0
  30. package/src/artifacts/glm52/layout.ts +152 -0
  31. package/src/artifacts/glm52/memory.ts +500 -0
  32. package/src/artifacts/glm52/resident-weights.ts +135 -0
  33. package/src/artifacts/glm52/weights.ts +503 -0
  34. package/src/artifacts/glm52-config.ts +262 -0
  35. package/src/artifacts/identity.ts +94 -0
  36. package/src/artifacts/index.ts +15 -0
  37. package/src/artifacts/mmap.ts +90 -0
  38. package/src/artifacts/safetensors-writer.ts +249 -0
  39. package/src/artifacts/safetensors.ts +164 -0
  40. package/src/artifacts/turboquant-fold.ts +42 -0
  41. package/src/artifacts/weight-audit.ts +43 -0
  42. package/src/artifacts/weights.ts +255 -0
  43. package/src/contracts/index.ts +2 -0
  44. package/src/contracts/mlx/attention.ts +8 -0
  45. package/src/contracts/mlx/cache.ts +510 -0
  46. package/src/contracts/mlx/checkpoint.ts +10 -0
  47. package/src/contracts/mlx/draft-checkpoint.ts +14 -0
  48. package/src/contracts/mlx/draft-projection.ts +27 -0
  49. package/src/contracts/mlx/draft-target.ts +84 -0
  50. package/src/contracts/mlx/drafter.ts +176 -0
  51. package/src/contracts/mlx/forward-work.ts +23 -0
  52. package/src/contracts/mlx/glm52-weights.ts +25 -0
  53. package/src/contracts/mlx/graph.ts +309 -0
  54. package/src/contracts/mlx/head.ts +6 -0
  55. package/src/contracts/mlx/index.ts +15 -0
  56. package/src/contracts/mlx/kv-codec.ts +9 -0
  57. package/src/contracts/mlx/media.ts +113 -0
  58. package/src/contracts/mlx/positions.ts +16 -0
  59. package/src/contracts/mlx/row-table.ts +2 -0
  60. package/src/contracts/mlx/trainable.ts +103 -0
  61. package/src/contracts/mlx/whisper.ts +96 -0
  62. package/src/contracts/portable/composition.ts +106 -0
  63. package/src/contracts/portable/denoising.ts +17 -0
  64. package/src/contracts/portable/execution.ts +79 -0
  65. package/src/contracts/portable/generation.ts +103 -0
  66. package/src/contracts/portable/graph.ts +80 -0
  67. package/src/contracts/portable/index.ts +15 -0
  68. package/src/contracts/portable/memory-plan.ts +26 -0
  69. package/src/contracts/portable/object-cache.ts +8 -0
  70. package/src/contracts/portable/prefill.ts +63 -0
  71. package/src/contracts/portable/prefix-cache.ts +33 -0
  72. package/src/contracts/portable/prompt.ts +2 -0
  73. package/src/contracts/portable/resources.ts +25 -0
  74. package/src/contracts/portable/rollback.ts +7 -0
  75. package/src/contracts/portable/sampling.ts +44 -0
  76. package/src/contracts/portable/scheduling.ts +54 -0
  77. package/src/contracts/portable/token-delivery.ts +4 -0
  78. package/src/contracts/portable/tsconfig.json +14 -0
  79. package/src/embeddings/index.ts +1 -0
  80. package/src/embeddings/text.ts +85 -0
  81. package/src/execution/admission.ts +59 -0
  82. package/src/execution/batch-group.ts +1240 -0
  83. package/src/execution/batch-types.ts +225 -0
  84. package/src/execution/cancellation.ts +38 -0
  85. package/src/execution/continuation-request.ts +45 -0
  86. package/src/execution/continuation-types.ts +10 -0
  87. package/src/execution/coordinator.ts +70 -0
  88. package/src/execution/denoising-group.ts +180 -0
  89. package/src/execution/engine.ts +56 -0
  90. package/src/execution/fill-group.ts +505 -0
  91. package/src/execution/fit.ts +184 -0
  92. package/src/execution/gateway-binding.ts +178 -0
  93. package/src/execution/grammar-group.ts +224 -0
  94. package/src/execution/index.ts +18 -0
  95. package/src/execution/methods.ts +75 -0
  96. package/src/execution/mixed-iteration.ts +96 -0
  97. package/src/execution/plan.ts +65 -0
  98. package/src/execution/prefill-cohort.ts +100 -0
  99. package/src/execution/prefill-rows.ts +263 -0
  100. package/src/execution/request-policy.ts +19 -0
  101. package/src/execution/row-sampling.ts +49 -0
  102. package/src/execution/scheduler.ts +92 -0
  103. package/src/execution/session.ts +247 -0
  104. package/src/execution/speculative-group.ts +797 -0
  105. package/src/execution/tasks.ts +44 -0
  106. package/src/generation/autoregressive.ts +840 -0
  107. package/src/generation/bindings/autoregressive.ts +104 -0
  108. package/src/generation/bindings/denoising.ts +60 -0
  109. package/src/generation/bindings/prefill-policy.ts +36 -0
  110. package/src/generation/checkpoint-identity.ts +60 -0
  111. package/src/generation/denoising.ts +85 -0
  112. package/src/generation/diffusion.ts +583 -0
  113. package/src/generation/fill/echo-index.ts +188 -0
  114. package/src/generation/fill/index.ts +3 -0
  115. package/src/generation/fill/proposal.ts +69 -0
  116. package/src/generation/fill/schema-rows.ts +506 -0
  117. package/src/generation/fill/session.ts +464 -0
  118. package/src/generation/fill/tool-boundary.ts +103 -0
  119. package/src/generation/fill-append.ts +58 -0
  120. package/src/generation/generate.ts +44 -0
  121. package/src/generation/grammar-step.ts +39 -0
  122. package/src/generation/index.ts +14 -0
  123. package/src/generation/phase-dispatch.ts +115 -0
  124. package/src/generation/prefill.ts +33 -0
  125. package/src/generation/result.ts +49 -0
  126. package/src/generation/scopes.ts +157 -0
  127. package/src/generation/speculative/acceptance.ts +41 -0
  128. package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
  129. package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
  130. package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
  131. package/src/generation/speculative/bindings/binding.ts +106 -0
  132. package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
  133. package/src/generation/speculative/bindings/draft-target.ts +10 -0
  134. package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
  135. package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
  136. package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
  137. package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
  138. package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
  139. package/src/generation/speculative/cache-identity.ts +16 -0
  140. package/src/generation/speculative/draft-kind.ts +149 -0
  141. package/src/generation/speculative/draft-policy.ts +41 -0
  142. package/src/generation/speculative/draft-registry.ts +118 -0
  143. package/src/generation/speculative/draft-vocab.ts +110 -0
  144. package/src/generation/speculative/index.ts +17 -0
  145. package/src/generation/speculative/output.ts +49 -0
  146. package/src/generation/speculative/round.ts +307 -0
  147. package/src/generation/speculative/source.ts +144 -0
  148. package/src/generation/speculative/sources/assistant-source.ts +64 -0
  149. package/src/generation/speculative/sources/deepspec-source.ts +70 -0
  150. package/src/generation/speculative/sources/dflash-source.ts +84 -0
  151. package/src/generation/speculative/sources/dflash2-source.ts +86 -0
  152. package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
  153. package/src/generation/speculative/sources/ngram-source.ts +165 -0
  154. package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
  155. package/src/generation/speculative/sources/two-model.ts +75 -0
  156. package/src/generation/types.ts +194 -0
  157. package/src/index.ts +20 -0
  158. package/src/input/audio/audiotoolbox.ts +112 -0
  159. package/src/input/audio/decode.ts +133 -0
  160. package/src/input/audio/features.ts +202 -0
  161. package/src/input/audio/index.ts +6 -0
  162. package/src/input/audio/transcode.ts +67 -0
  163. package/src/input/audio/whisper-mel.ts +185 -0
  164. package/src/input/audio/whisper-tokenizer.ts +238 -0
  165. package/src/input/chat-template.ts +389 -0
  166. package/src/input/index.ts +6 -0
  167. package/src/input/media-fetch.ts +439 -0
  168. package/src/input/packed-mixed.ts +43 -0
  169. package/src/input/special-tokens.ts +75 -0
  170. package/src/input/token-groups.ts +49 -0
  171. package/src/input/tokenizer.ts +336 -0
  172. package/src/input/tool-call.ts +689 -0
  173. package/src/input/vision/index.ts +3 -0
  174. package/src/input/vision/preprocess.ts +247 -0
  175. package/src/input/vision/prompt.ts +398 -0
  176. package/src/input/vision/qwen3vl-preprocess.ts +409 -0
  177. package/src/input/vision/qwen3vl-prompt.ts +237 -0
  178. package/src/input/vision/video-frames.ts +100 -0
  179. package/src/kernels/ane/fill.ts +101 -0
  180. package/src/kernels/ane/linear.ts +416 -0
  181. package/src/kernels/attention/flash.ts +694 -0
  182. package/src/kernels/attention/kvq4-decode.ts +155 -0
  183. package/src/kernels/attention/masks.ts +40 -0
  184. package/src/kernels/attention/multi-query.ts +193 -0
  185. package/src/kernels/attention/paged.ts +130 -0
  186. package/src/kernels/delta/gated.ts +306 -0
  187. package/src/kernels/delta/index.ts +3 -0
  188. package/src/kernels/delta/silu-rms-heads.ts +69 -0
  189. package/src/kernels/delta/state.ts +74 -0
  190. package/src/kernels/gated-rms-norm.ts +64 -0
  191. package/src/kernels/glm52/layout.ts +37 -0
  192. package/src/kernels/glm52/streamed-experts.ts +360 -0
  193. package/src/kernels/logits.ts +38 -0
  194. package/src/kernels/quantization/affine-row.ts +154 -0
  195. package/src/kernels/quantization/affine-rows.ts +36 -0
  196. package/src/kernels/quantization/affine-split-mma.ts +144 -0
  197. package/src/kernels/quantization/affine3-mma.ts +117 -0
  198. package/src/kernels/quantization/affine3-rows.ts +103 -0
  199. package/src/kernels/quantization/affine4-mma.ts +113 -0
  200. package/src/kernels/quantization/affine4-rows.ts +102 -0
  201. package/src/kernels/residual-rms-norm.ts +65 -0
  202. package/src/kernels/sampling/index.ts +2 -0
  203. package/src/kernels/sampling/normalized-argmax.ts +116 -0
  204. package/src/kernels/sampling/token-bitmask.ts +52 -0
  205. package/src/kernels/softcap.ts +35 -0
  206. package/src/kernels/trellis/codebook.ts +123 -0
  207. package/src/kernels/trellis/down-codebook.ts +184 -0
  208. package/src/kernels/trellis/down-factored.ts +180 -0
  209. package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
  210. package/src/kernels/trellis/down-mma-wide.ts +195 -0
  211. package/src/kernels/trellis/down-mma.ts +145 -0
  212. package/src/kernels/trellis/expand.ts +42 -0
  213. package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
  214. package/src/kernels/trellis/gate-up-codebook.ts +137 -0
  215. package/src/kernels/trellis/gate-up-factored.ts +197 -0
  216. package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
  217. package/src/kernels/trellis/gate-up-mma.ts +148 -0
  218. package/src/kernels/trellis/gate-up.ts +248 -0
  219. package/src/kernels/trellis/geometry.ts +22 -0
  220. package/src/kernels/trellis/index.ts +103 -0
  221. package/src/kernels/trellis/launch.ts +12 -0
  222. package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
  223. package/src/kernels/trellis/mixed-gate-up.ts +146 -0
  224. package/src/kernels/trellis/qualified.ts +34 -0
  225. package/src/kernels/trellis/reduce.ts +150 -0
  226. package/src/kernels/trellis/scatter.ts +407 -0
  227. package/src/kernels/trellis/splitk-prefill.ts +152 -0
  228. package/src/kernels/trellis/tiled-prefill.ts +131 -0
  229. package/src/kernels/trellis/vector-expand.ts +58 -0
  230. package/src/kernels/trellis/wide-prefill.ts +89 -0
  231. package/src/kernels/turboquant/decode.ts +82 -0
  232. package/src/kernels/turboquant/index.ts +5 -0
  233. package/src/kernels/turboquant/inverse.ts +122 -0
  234. package/src/kernels/turboquant/ops.ts +860 -0
  235. package/src/kernels/turboquant/signs.ts +130 -0
  236. package/src/kernels/turboquant/tables.ts +124 -0
  237. package/src/layers/affine-verify-linear.ts +75 -0
  238. package/src/layers/ane-prefill-split.ts +283 -0
  239. package/src/layers/compiled-normalization.ts +41 -0
  240. package/src/layers/delta-heads.ts +23 -0
  241. package/src/layers/dense-embedding.ts +29 -0
  242. package/src/layers/dense-linear.ts +32 -0
  243. package/src/layers/dequantized-linear.ts +72 -0
  244. package/src/layers/folded-attention.ts +42 -0
  245. package/src/layers/geglu.ts +130 -0
  246. package/src/layers/helpers.ts +7 -0
  247. package/src/layers/index.ts +36 -0
  248. package/src/layers/kv4-decode-attention.ts +15 -0
  249. package/src/layers/layer-norm.ts +24 -0
  250. package/src/layers/loading.ts +92 -0
  251. package/src/layers/lora.ts +150 -0
  252. package/src/layers/normalization.ts +9 -0
  253. package/src/layers/prefix-layout.ts +33 -0
  254. package/src/layers/quantized-append-attention.ts +32 -0
  255. package/src/layers/quantized-attention.ts +353 -0
  256. package/src/layers/quantized-embedding.ts +39 -0
  257. package/src/layers/quantized-linear.ts +137 -0
  258. package/src/layers/quantized-switch-linear.ts +37 -0
  259. package/src/layers/qwen-append.ts +12 -0
  260. package/src/layers/qwen-mrope.ts +238 -0
  261. package/src/layers/rope-2d.ts +110 -0
  262. package/src/layers/rope.ts +389 -0
  263. package/src/layers/swiglu.ts +23 -0
  264. package/src/layers/trellis-codebook-down.ts +52 -0
  265. package/src/layers/trellis-codebook-gate-up.ts +42 -0
  266. package/src/layers/trellis-down.ts +117 -0
  267. package/src/layers/trellis-gate-up.ts +71 -0
  268. package/src/layers/trellis-linear.ts +211 -0
  269. package/src/layers/trellis-prompt.ts +102 -0
  270. package/src/layers/trellis-reduce-gate-up.ts +73 -0
  271. package/src/layers/trellis-scatter-down.ts +98 -0
  272. package/src/models/ane-install.ts +74 -0
  273. package/src/models/audio/conformer.ts +656 -0
  274. package/src/models/cache-composition.ts +44 -0
  275. package/src/models/capabilities.ts +89 -0
  276. package/src/models/chat-template.ts +10 -0
  277. package/src/models/composition.ts +107 -0
  278. package/src/models/diffusion-gemma/experts.ts +59 -0
  279. package/src/models/diffusion-gemma/family.ts +21 -0
  280. package/src/models/diffusion-gemma/model.ts +900 -0
  281. package/src/models/diffusion-gemma/vision.ts +549 -0
  282. package/src/models/drafter-loaders.ts +49 -0
  283. package/src/models/drafters.ts +39 -0
  284. package/src/models/factory.ts +351 -0
  285. package/src/models/families.ts +70 -0
  286. package/src/models/family.ts +125 -0
  287. package/src/models/gemma4/assistant-target.ts +46 -0
  288. package/src/models/gemma4/assistant.ts +327 -0
  289. package/src/models/gemma4/blocks.ts +755 -0
  290. package/src/models/gemma4/compiled-decode.ts +452 -0
  291. package/src/models/gemma4/family.ts +39 -0
  292. package/src/models/gemma4/gemma4-12b.ts +677 -0
  293. package/src/models/gemma4/gemma4-26b.ts +544 -0
  294. package/src/models/gemma4/gemma4-e4b.ts +599 -0
  295. package/src/models/gemma4/graphs.ts +147 -0
  296. package/src/models/gemma4/media.ts +88 -0
  297. package/src/models/gemma4/model.ts +860 -0
  298. package/src/models/gemma4/serving-blocks.ts +72 -0
  299. package/src/models/gemma4/trainable.ts +186 -0
  300. package/src/models/gemma4/written-blocks.ts +232 -0
  301. package/src/models/gemma4/written-forward.ts +159 -0
  302. package/src/models/gemma4/written-input.ts +29 -0
  303. package/src/models/glm52/atlas.ts +507 -0
  304. package/src/models/glm52/coupling.ts +388 -0
  305. package/src/models/glm52/dsa.ts +601 -0
  306. package/src/models/glm52/family.ts +20 -0
  307. package/src/models/glm52/mla.ts +812 -0
  308. package/src/models/glm52/model.ts +1127 -0
  309. package/src/models/glm52/moe.ts +223 -0
  310. package/src/models/glm52/mtp.ts +60 -0
  311. package/src/models/glm52/pilot.ts +537 -0
  312. package/src/models/glm52/reference.ts +445 -0
  313. package/src/models/glm52/residency.ts +557 -0
  314. package/src/models/glm52/streamed-experts.ts +447 -0
  315. package/src/models/graph.ts +59 -0
  316. package/src/models/implementation.ts +230 -0
  317. package/src/models/index.ts +15 -0
  318. package/src/models/media-input.ts +15 -0
  319. package/src/models/memory-plan.ts +39 -0
  320. package/src/models/minicpm5/family.ts +29 -0
  321. package/src/models/minicpm5/model.ts +394 -0
  322. package/src/models/minicpm5/trainable.ts +90 -0
  323. package/src/models/profile.ts +337 -0
  324. package/src/models/qwen/blocks.ts +658 -0
  325. package/src/models/qwen/checkpoint.ts +190 -0
  326. package/src/models/qwen/families.ts +57 -0
  327. package/src/models/qwen/media-input.ts +161 -0
  328. package/src/models/qwen/mtp.ts +101 -0
  329. package/src/models/qwen/qwen3-moe.ts +355 -0
  330. package/src/models/qwen/qwen3.ts +323 -0
  331. package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
  332. package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
  333. package/src/models/qwen/qwen3_5.ts +433 -0
  334. package/src/models/qwen/written-layers.ts +285 -0
  335. package/src/models/row-table.ts +1 -0
  336. package/src/models/runtime.ts +73 -0
  337. package/src/models/speculative/deepspec-artifact.ts +10 -0
  338. package/src/models/speculative/deepspec.ts +744 -0
  339. package/src/models/speculative/dflash.ts +553 -0
  340. package/src/models/speculative/dflash2-artifact.ts +9 -0
  341. package/src/models/speculative/dflash2-operations.ts +1 -0
  342. package/src/models/speculative/dflash2.ts +576 -0
  343. package/src/models/speculative/loader.ts +29 -0
  344. package/src/models/support.ts +79 -0
  345. package/src/models/universal/archs.ts +365 -0
  346. package/src/models/universal/blocks.ts +479 -0
  347. package/src/models/universal/dense.ts +255 -0
  348. package/src/models/universal/family.ts +17 -0
  349. package/src/models/vision/qwen3vl.ts +566 -0
  350. package/src/models/vision/siglip.ts +520 -0
  351. package/src/models/vision/unified.ts +176 -0
  352. package/src/models/whisper/family.ts +19 -0
  353. package/src/models/whisper/fast.ts +734 -0
  354. package/src/models/whisper/model.ts +430 -0
  355. package/src/runtime/compiled-trace.ts +16 -0
  356. package/src/runtime/config.ts +77 -0
  357. package/src/runtime/expert-trace.ts +71 -0
  358. package/src/runtime/graph-forward.ts +65 -0
  359. package/src/runtime/integers.ts +6 -0
  360. package/src/runtime/native.ts +16 -0
  361. package/src/runtime/resources.ts +53 -0
  362. package/src/runtime/row-operations.ts +42 -0
  363. package/src/runtime/trace.ts +161 -0
  364. package/src/sampling/curve.ts +122 -0
  365. package/src/sampling/draft.ts +55 -0
  366. package/src/sampling/extras.ts +45 -0
  367. package/src/sampling/filters.ts +134 -0
  368. package/src/sampling/grammar.ts +630 -0
  369. package/src/sampling/hlg.ts +290 -0
  370. package/src/sampling/index.ts +9 -0
  371. package/src/sampling/processors.ts +142 -0
  372. package/src/sampling/sampler.ts +120 -0
  373. package/src/sampling/step.ts +343 -0
  374. package/src/sampling/types.ts +161 -0
  375. package/src/sampling/window-filters.ts +46 -0
  376. package/src/scoring/decisions.ts +96 -0
  377. package/src/scoring/full-sequence.ts +103 -0
  378. package/src/scoring/index.ts +9 -0
  379. package/src/scoring/kl.ts +29 -0
  380. package/src/scoring/perplexity.ts +165 -0
  381. package/src/scoring/response-logps.ts +65 -0
  382. package/src/state/affine-attention.ts +180 -0
  383. package/src/state/attachment.ts +68 -0
  384. package/src/state/attention-read.ts +215 -0
  385. package/src/state/batched-kv.ts +294 -0
  386. package/src/state/batched-mask.ts +445 -0
  387. package/src/state/batched-quant.ts +389 -0
  388. package/src/state/batched-quantized-kv.ts +209 -0
  389. package/src/state/batched-rotating-quant.ts +457 -0
  390. package/src/state/batched-rotating-state.ts +149 -0
  391. package/src/state/batched-rotating.ts +430 -0
  392. package/src/state/batched-row-storage.ts +179 -0
  393. package/src/state/batched-ssm.ts +54 -0
  394. package/src/state/batched-turboquant-kv.ts +144 -0
  395. package/src/state/bf16-first-kv.ts +663 -0
  396. package/src/state/cache-service.ts +592 -0
  397. package/src/state/capabilities.ts +65 -0
  398. package/src/state/checkpoint.ts +46 -0
  399. package/src/state/continuation-persistence.ts +126 -0
  400. package/src/state/decode-step-inputs.ts +31 -0
  401. package/src/state/decoded-kv-donor.ts +12 -0
  402. package/src/state/delayed-quantized-kv.ts +141 -0
  403. package/src/state/delayed-rotating-quantized-kv.ts +178 -0
  404. package/src/state/delayed-turboquant-kv.ts +123 -0
  405. package/src/state/durability.ts +214 -0
  406. package/src/state/encoder-cache.ts +26 -0
  407. package/src/state/full-kv-row-append.ts +33 -0
  408. package/src/state/full-kv-row-donor.ts +54 -0
  409. package/src/state/full-prefill-padding.ts +56 -0
  410. package/src/state/full-prefill-row.ts +112 -0
  411. package/src/state/full-transitioning-kv-rows.ts +70 -0
  412. package/src/state/glm52-cache.ts +783 -0
  413. package/src/state/grouped-heads-kv4-cache.ts +26 -0
  414. package/src/state/index.ts +38 -0
  415. package/src/state/kv-attention-view.ts +77 -0
  416. package/src/state/kv-budget.ts +14 -0
  417. package/src/state/kv-scheme.ts +315 -0
  418. package/src/state/kv-storage.ts +57 -0
  419. package/src/state/kv-tensor-rows.ts +212 -0
  420. package/src/state/kv.ts +324 -0
  421. package/src/state/kv4-head256-cache.ts +46 -0
  422. package/src/state/latent-legos.ts +895 -0
  423. package/src/state/layout.ts +277 -0
  424. package/src/state/leases.ts +26 -0
  425. package/src/state/paged/cache.ts +689 -0
  426. package/src/state/paged/codec.ts +36 -0
  427. package/src/state/paged/rows.ts +223 -0
  428. package/src/state/persistence-types.ts +173 -0
  429. package/src/state/persistence-worker.ts +83 -0
  430. package/src/state/persistence.ts +1036 -0
  431. package/src/state/persistence.worker.d.ts +3 -0
  432. package/src/state/persistence.worker.js +264 -0
  433. package/src/state/prefix-cache.ts +685 -0
  434. package/src/state/quantized-kv.ts +369 -0
  435. package/src/state/quantized-tensor.ts +26 -0
  436. package/src/state/request-policy.ts +48 -0
  437. package/src/state/retention-policy.ts +37 -0
  438. package/src/state/rollback.ts +131 -0
  439. package/src/state/rotating-kv-donor.ts +35 -0
  440. package/src/state/rotating-kv-layout.ts +341 -0
  441. package/src/state/rotating-kv.ts +529 -0
  442. package/src/state/rotating-quantized-kv.ts +616 -0
  443. package/src/state/rotating-row-transaction.ts +35 -0
  444. package/src/state/rows.ts +104 -0
  445. package/src/state/softcap-attention.ts +118 -0
  446. package/src/state/speculative/glm52-mtp-state.ts +38 -0
  447. package/src/state/speculative/qwen-mtp-state.ts +34 -0
  448. package/src/state/speculative-rotating-kv.ts +113 -0
  449. package/src/state/ssd-cache.ts +434 -0
  450. package/src/state/ssm-prefill-padding.ts +39 -0
  451. package/src/state/ssm.ts +422 -0
  452. package/src/state/target-layout.ts +7 -0
  453. package/src/state/tiered-prefix-cache.ts +45 -0
  454. package/src/state/training-cache.ts +300 -0
  455. package/src/state/transitioning-kv-rows.ts +267 -0
  456. package/src/state/turboquant-attention.ts +97 -0
  457. package/src/state/turboquant-codec.ts +90 -0
  458. package/src/state/turboquant-kv.ts +442 -0
  459. package/src/state/views.ts +26 -0
  460. package/src/transcription/index.ts +3 -0
  461. package/src/transcription/silero-vad.ts +379 -0
  462. package/src/transcription/whisper/decode.ts +773 -0
  463. package/src/transcription/whisper/format.ts +74 -0
  464. package/src/transcription/whisper/index.ts +4 -0
  465. package/src/transcription/whisper/timing.ts +274 -0
  466. package/src/transcription/whisper/transcribe.ts +394 -0
  467. package/src/transcription/whisper/types.ts +22 -0
@@ -0,0 +1,1134 @@
1
+ import type { MlxArray } from "@mlx-bun/mlx/array";
2
+ import type { MlxHandle } from "@mlx-bun/mlx/ffi";
3
+ import type {
4
+ ExpertIOHintSegment,
5
+ ExpertIOHintSnapshot,
6
+ ExpertIOSegment,
7
+ } from "./io";
8
+ import type {
9
+ ExpertLfruCandidate,
10
+ ExpertUsageEntry,
11
+ ExpertUsageLedger,
12
+ ExpertUsageRoute,
13
+ } from "./usage";
14
+
15
+ export const DEFAULT_EXPERT_WORKING_SLOTS = 64;
16
+
17
+ export interface ExpertResidencyBudgetInput {
18
+ readonly budgetBytes: number;
19
+ readonly fixedBytes: number;
20
+ readonly slotBytes: number;
21
+ readonly sparseLayers: number;
22
+ readonly workingSlots?: number;
23
+ readonly pinnedExperts?: number;
24
+ readonly maxSlotsPerLayer?: number;
25
+ }
26
+
27
+ export interface ExpertResidencyPlan {
28
+ readonly budgetBytes: number;
29
+ readonly fixedBytes: number;
30
+ readonly slotBytes: number;
31
+ readonly sparseLayers: number;
32
+ readonly workingSlots: number;
33
+ readonly pinnedSlots: number;
34
+ readonly slotsPerLayer: number;
35
+ readonly residentSlots: number;
36
+ readonly totalSlots: number;
37
+ readonly slabBytes: number;
38
+ readonly plannedBytes: number;
39
+ }
40
+
41
+ function positiveSafeInteger(value: number, name: string): void {
42
+ if (!Number.isSafeInteger(value) || value <= 0)
43
+ throw new RangeError(`${name} must be a positive safe integer`);
44
+ }
45
+
46
+ /**
47
+ * Derive the only admissible fixed-capacity layout. One persistent slot per
48
+ * sparse layer plus the global working bank is a hard startup floor.
49
+ */
50
+ export function planExpertResidency(
51
+ input: ExpertResidencyBudgetInput,
52
+ ): ExpertResidencyPlan {
53
+ positiveSafeInteger(input.budgetBytes, "expert budgetBytes");
54
+ if (!Number.isSafeInteger(input.fixedBytes) || input.fixedBytes < 0)
55
+ throw new RangeError("expert fixedBytes must be a non-negative safe integer");
56
+ positiveSafeInteger(input.slotBytes, "expert slotBytes");
57
+ positiveSafeInteger(input.sparseLayers, "expert sparseLayers");
58
+ const workingSlots = input.workingSlots ?? DEFAULT_EXPERT_WORKING_SLOTS;
59
+ const pinnedSlots = input.pinnedExperts ?? 0;
60
+ positiveSafeInteger(workingSlots, "expert workingSlots");
61
+ if (!Number.isSafeInteger(pinnedSlots) || pinnedSlots < 0)
62
+ throw new RangeError("expert pinnedExperts must be a non-negative safe integer");
63
+
64
+ const mandatorySlots = workingSlots + pinnedSlots + input.sparseLayers;
65
+ const mandatoryBytes = mandatorySlots * input.slotBytes;
66
+ if (!Number.isSafeInteger(mandatoryBytes) ||
67
+ input.fixedBytes + mandatoryBytes > input.budgetBytes) {
68
+ throw new Error(
69
+ `expert residency budget cannot start: ${input.budgetBytes} bytes does not fit ` +
70
+ `${input.fixedBytes} fixed + ${workingSlots} working + ${pinnedSlots} pinned + ` +
71
+ `one slot for each of ${input.sparseLayers} sparse layers`,
72
+ );
73
+ }
74
+
75
+ const variableBytes =
76
+ input.budgetBytes - input.fixedBytes -
77
+ (workingSlots + pinnedSlots) * input.slotBytes;
78
+ let slotsPerLayer = Math.floor(variableBytes / (input.sparseLayers * input.slotBytes));
79
+ if (input.maxSlotsPerLayer !== undefined) {
80
+ positiveSafeInteger(input.maxSlotsPerLayer, "expert maxSlotsPerLayer");
81
+ slotsPerLayer = Math.min(slotsPerLayer, input.maxSlotsPerLayer);
82
+ }
83
+ const residentSlots = slotsPerLayer * input.sparseLayers;
84
+ const totalSlots = workingSlots + pinnedSlots + residentSlots;
85
+ const slabBytes = totalSlots * input.slotBytes;
86
+ return {
87
+ budgetBytes: input.budgetBytes,
88
+ fixedBytes: input.fixedBytes,
89
+ slotBytes: input.slotBytes,
90
+ sparseLayers: input.sparseLayers,
91
+ workingSlots,
92
+ pinnedSlots,
93
+ slotsPerLayer,
94
+ residentSlots,
95
+ totalSlots,
96
+ slabBytes,
97
+ plannedBytes: input.fixedBytes + slabBytes,
98
+ };
99
+ }
100
+
101
+ export interface ExpertRouteLike {
102
+ readonly indices: ArrayLike<number>;
103
+ }
104
+
105
+ export interface ExpertUnionConsumer {
106
+ readonly row: number;
107
+ readonly rank: number;
108
+ }
109
+
110
+ export interface ExpertUnionEntry {
111
+ readonly expertId: number;
112
+ readonly consumers: readonly ExpertUnionConsumer[];
113
+ }
114
+
115
+ export interface ExpertUnionWave {
116
+ readonly entries: readonly ExpertUnionEntry[];
117
+ }
118
+
119
+ /**
120
+ * Stable row-major, route-rank-major union. Repeated experts keep their first
121
+ * position and collect every consumer; waves never exceed the scratch bank.
122
+ */
123
+ export function buildExpertBatchUnion(
124
+ routes: readonly ExpertRouteLike[],
125
+ maxUniquePerWave = DEFAULT_EXPERT_WORKING_SLOTS,
126
+ ): ExpertUnionWave[] {
127
+ positiveSafeInteger(maxUniquePerWave, "maxUniquePerWave");
128
+ const entries: Array<{ expertId: number; consumers: ExpertUnionConsumer[] }> = [];
129
+ const byExpert = new Map<number, { expertId: number; consumers: ExpertUnionConsumer[] }>();
130
+ for (let row = 0; row < routes.length; row++) {
131
+ const route = routes[row]!;
132
+ for (let rank = 0; rank < route.indices.length; rank++) {
133
+ const expertId = Number(route.indices[rank]);
134
+ if (!Number.isSafeInteger(expertId) || expertId < 0)
135
+ throw new RangeError(`route ${row} rank ${rank} has invalid expert ${expertId}`);
136
+ let entry = byExpert.get(expertId);
137
+ if (!entry) {
138
+ entry = { expertId, consumers: [] };
139
+ byExpert.set(expertId, entry);
140
+ entries.push(entry);
141
+ }
142
+ entry.consumers.push({ row, rank });
143
+ }
144
+ }
145
+ const waves: ExpertUnionWave[] = [];
146
+ for (let begin = 0; begin < entries.length; begin += maxUniquePerWave) {
147
+ waves.push({
148
+ entries: entries.slice(begin, begin + maxUniquePerWave).map((entry) => ({
149
+ expertId: entry.expertId,
150
+ consumers: Object.freeze(entry.consumers.slice()),
151
+ })),
152
+ });
153
+ }
154
+ return waves;
155
+ }
156
+
157
+ export interface ExpertResidencyBackend {
158
+ readonly slots: number;
159
+ submitSegments(
160
+ slot: number,
161
+ generation: bigint,
162
+ segments: readonly ExpertIOSegment[],
163
+ ): void;
164
+ wait(slot: number, generation: bigint): Promise<void>;
165
+ cancel(slot: number, generation: bigint): void;
166
+ lease(slot: number, generation: bigint, consumer?: "cpu" | "gpu"): void;
167
+ releaseGpuFenced(slot: number, generation: bigint): void;
168
+ discard(slot: number, generation: bigint): void;
169
+ physicalFootprint(): number;
170
+ /** Advisory-only readahead; returns false when its bounded queue is full. */
171
+ hintSegments?(segments: readonly ExpertIOHintSegment[]): boolean;
172
+ hintTelemetry?(): ExpertIOHintSnapshot;
173
+ }
174
+
175
+ export interface ExpertResidencyLocation {
176
+ readonly layer: number;
177
+ readonly expertId: number;
178
+ readonly segments: readonly ExpertIOSegment[];
179
+ /** Optional subset for advisory readahead (for example scales-only). */
180
+ readonly hintSegments?: readonly ExpertIOHintSegment[];
181
+ }
182
+
183
+ /**
184
+ * Colibri hint policy: buffered demand may hint the full expert; F_NOCACHE
185
+ * demand hints only the scale tail because warming weight pages would be
186
+ * wasted by the subsequent cache-bypassing read.
187
+ */
188
+ export function buildExpertHintSegments(
189
+ segments: readonly ExpertIOSegment[],
190
+ scaleOffset: number,
191
+ noCache: boolean,
192
+ ): ExpertIOHintSegment[] {
193
+ if (!Number.isSafeInteger(scaleOffset) || scaleOffset < 0)
194
+ throw new RangeError("expert hint scaleOffset must be non-negative");
195
+ if (!noCache) {
196
+ return segments.map(({ file, offset, length }) => ({
197
+ file,
198
+ offset,
199
+ length,
200
+ }));
201
+ }
202
+ return segments.flatMap((segment) => {
203
+ const skip = Math.max(0, scaleOffset - segment.destination);
204
+ if (skip >= segment.length) return [];
205
+ return [{
206
+ file: segment.file,
207
+ offset: segment.offset + skip,
208
+ length: segment.length - skip,
209
+ }];
210
+ });
211
+ }
212
+
213
+ export interface ExpertResidencyManagerOptions {
214
+ readonly plan: ExpertResidencyPlan;
215
+ readonly sparseLayerIds: readonly number[];
216
+ readonly backend: ExpertResidencyBackend;
217
+ readonly locate: (layer: number, expertId: number) => ExpertResidencyLocation;
218
+ readonly pinned?: ReadonlyArray<{ layer: number; expertId: number }>;
219
+ readonly usage?: ExpertUsageLedger;
220
+ /** Synchronous measurement seam; receives router rows before union/dedup. */
221
+ readonly routeObserver?: (
222
+ layer: number,
223
+ routes: readonly ExpertUsageRoute[],
224
+ ) => void;
225
+ }
226
+
227
+ type SlotRole = "working" | "layer" | "pinned" | "disabled";
228
+ type SlotPhase = "idle" | "loading" | "ready" | "leased";
229
+
230
+ interface SlotRecord {
231
+ readonly id: number;
232
+ role: SlotRole;
233
+ phase: SlotPhase;
234
+ layer: number | null;
235
+ expertId: number | null;
236
+ generation: bigint;
237
+ lastUse: bigint;
238
+ }
239
+
240
+ export interface ExpertResidencyLeaseEntry {
241
+ readonly layer: number;
242
+ readonly expertId: number;
243
+ readonly slot: number;
244
+ readonly generation: bigint;
245
+ readonly hit: boolean;
246
+ }
247
+
248
+ export interface ExpertResidencySnapshot {
249
+ readonly clock: bigint;
250
+ readonly generation: bigint;
251
+ readonly working: number;
252
+ readonly resident: number;
253
+ readonly pinned: number;
254
+ readonly disabled: number;
255
+ readonly loading: number;
256
+ readonly leased: number;
257
+ readonly physicalFootprint: number;
258
+ readonly budgetBytes: number;
259
+ readonly hits: number;
260
+ readonly misses: number;
261
+ readonly evictions: number;
262
+ readonly pressureEvictions: number;
263
+ readonly repinSwaps: number;
264
+ }
265
+
266
+ export interface ExpertResidencyMapEntry {
267
+ readonly slot: number;
268
+ readonly layer: number;
269
+ readonly expertId: number;
270
+ readonly tier: "resident" | "pinned";
271
+ readonly lastUse: bigint;
272
+ readonly usage: ExpertUsageEntry | null;
273
+ }
274
+
275
+ export interface ExpertRepinEvent {
276
+ readonly layer: number;
277
+ readonly evictedPin: number;
278
+ readonly admittedPin: number;
279
+ readonly gain: number;
280
+ }
281
+
282
+ export interface ExpertLatencySummary {
283
+ readonly count: number;
284
+ readonly totalMs: number;
285
+ readonly p50Ms: number;
286
+ readonly p95Ms: number;
287
+ readonly p99Ms: number;
288
+ readonly maxMs: number;
289
+ }
290
+
291
+ export interface ExpertDemandTelemetry {
292
+ readonly hits: number;
293
+ readonly misses: number;
294
+ readonly loads: number;
295
+ readonly readOperations: number;
296
+ readonly readBytes: number;
297
+ readonly diskService: ExpertLatencySummary;
298
+ readonly foregroundWait: ExpertLatencySummary;
299
+ }
300
+
301
+ export type ExpertPolicyTelemetry = ExpertDemandTelemetry;
302
+
303
+ export interface ExpertHintTelemetry {
304
+ readonly candidates: number;
305
+ readonly residentSkipped: number;
306
+ readonly submitErrors: number;
307
+ readonly submitted: number;
308
+ readonly completed: number;
309
+ readonly dropped: number;
310
+ readonly operations: number;
311
+ readonly bytes: number;
312
+ readonly errors: number;
313
+ readonly queueDepth: number;
314
+ readonly inFlight: number;
315
+ }
316
+
317
+ const EMPTY_HINT_SNAPSHOT: ExpertIOHintSnapshot = Object.freeze({
318
+ submitted: 0,
319
+ completed: 0,
320
+ dropped: 0,
321
+ operations: 0,
322
+ bytes: 0,
323
+ errors: 0,
324
+ queueDepth: 0,
325
+ inFlight: 0,
326
+ });
327
+
328
+ export function summarizeExpertLatencies(
329
+ samples: readonly number[],
330
+ ): ExpertLatencySummary {
331
+ if (samples.length === 0) {
332
+ return {
333
+ count: 0,
334
+ totalMs: 0,
335
+ p50Ms: 0,
336
+ p95Ms: 0,
337
+ p99Ms: 0,
338
+ maxMs: 0,
339
+ };
340
+ }
341
+ const sorted = samples.slice().sort((left, right) => left - right);
342
+ const percentile = (fraction: number): number =>
343
+ sorted[Math.ceil(fraction * sorted.length) - 1]!;
344
+ return {
345
+ count: sorted.length,
346
+ totalMs: sorted.reduce((sum, value) => sum + value, 0),
347
+ p50Ms: percentile(0.5),
348
+ p95Ms: percentile(0.95),
349
+ p99Ms: percentile(0.99),
350
+ maxMs: sorted[sorted.length - 1]!,
351
+ };
352
+ }
353
+
354
+ function expertKey(layer: number, expertId: number): string {
355
+ return `${layer}:${expertId}`;
356
+ }
357
+
358
+ export class ExpertBatchLease {
359
+ readonly entries: readonly ExpertResidencyLeaseEntry[];
360
+ #manager: ExpertResidencyManager | null;
361
+
362
+ constructor(
363
+ manager: ExpertResidencyManager,
364
+ entries: readonly ExpertResidencyLeaseEntry[],
365
+ ) {
366
+ this.#manager = manager;
367
+ this.entries = Object.freeze(entries.slice());
368
+ }
369
+
370
+ /**
371
+ * Release after the caller has evaluated every graph that reads these
372
+ * slots and synchronized the consuming stream.
373
+ */
374
+ releaseFenced(): void {
375
+ const manager = this.#manager;
376
+ if (!manager) throw new Error("expert batch lease has already been released");
377
+ try {
378
+ manager.releaseFenced(this.entries);
379
+ } finally {
380
+ this.#manager = null;
381
+ }
382
+ }
383
+
384
+ async releaseAfterGpuSync(
385
+ dependentOutputs: readonly MlxArray[],
386
+ stream?: MlxHandle,
387
+ ): Promise<void> {
388
+ if (dependentOutputs.length === 0)
389
+ throw new Error("GPU slot release requires dependent outputs to evaluate");
390
+ const [{ gpuStream }, { synchronize }, ops] = await Promise.all([
391
+ import("@mlx-bun/mlx/array"),
392
+ import("@mlx-bun/mlx/ffi"),
393
+ import("@mlx-bun/mlx/ops"),
394
+ ]);
395
+ ops.evalAll([...dependentOutputs]);
396
+ synchronize(stream ?? gpuStream);
397
+ this.releaseFenced();
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Deterministic, serial-forward residency policy. G7 owns concurrent model
403
+ * rows; G3 deliberately permits only one active streamed expert wave so a
404
+ * leased slot can never be shared or evicted beneath a lazy graph.
405
+ */
406
+ export class ExpertResidencyManager {
407
+ readonly plan: ExpertResidencyPlan;
408
+ readonly sparseLayerIds: readonly number[];
409
+ #backend: ExpertResidencyBackend;
410
+ #locate: ExpertResidencyManagerOptions["locate"];
411
+ #usage: ExpertUsageLedger | null;
412
+ #routeObserver: ExpertResidencyManagerOptions["routeObserver"];
413
+ #slots: SlotRecord[] = [];
414
+ #working = new Set<number>();
415
+ #layerSlots = new Map<number, Set<number>>();
416
+ #lookup = new Map<string, number>();
417
+ #pinnedKeys = new Set<string>();
418
+ #pinned: Array<{ layer: number; expertId: number }>;
419
+ #active = false;
420
+ #clock = 0n;
421
+ #generation = 0n;
422
+ #hits = 0;
423
+ #misses = 0;
424
+ #evictions = 0;
425
+ #pressureEvictions = 0;
426
+ #repinSwaps = 0;
427
+ #telemetryHits = 0;
428
+ #telemetryMisses = 0;
429
+ #telemetryLoads = 0;
430
+ #telemetryReadOperations = 0;
431
+ #telemetryReadBytes = 0;
432
+ #diskServiceMs: number[] = [];
433
+ #foregroundWaitMs: number[] = [];
434
+ #policyHits = 0;
435
+ #policyMisses = 0;
436
+ #policyLoads = 0;
437
+ #policyReadOperations = 0;
438
+ #policyReadBytes = 0;
439
+ #policyDiskServiceMs: number[] = [];
440
+ #policyForegroundWaitMs: number[] = [];
441
+ #hintCandidates = 0;
442
+ #hintResidentSkipped = 0;
443
+ #hintSubmitErrors = 0;
444
+ #hintBaseline: ExpertIOHintSnapshot;
445
+
446
+ constructor(options: ExpertResidencyManagerOptions) {
447
+ this.plan = options.plan;
448
+ this.#backend = options.backend;
449
+ this.#locate = options.locate;
450
+ this.#usage = options.usage ?? null;
451
+ this.#routeObserver = options.routeObserver;
452
+ this.#hintBaseline = options.backend.hintTelemetry?.() ??
453
+ EMPTY_HINT_SNAPSHOT;
454
+ this.sparseLayerIds = Object.freeze(options.sparseLayerIds.slice());
455
+ if (this.sparseLayerIds.length !== this.plan.sparseLayers ||
456
+ new Set(this.sparseLayerIds).size !== this.sparseLayerIds.length)
457
+ throw new Error("sparseLayerIds must contain each planned sparse layer exactly once");
458
+ if (options.backend.slots !== this.plan.totalSlots)
459
+ throw new Error(
460
+ `expert backend has ${options.backend.slots} slots; plan requires ${this.plan.totalSlots}`,
461
+ );
462
+
463
+ const pinned = options.pinned ?? [];
464
+ this.#pinned = pinned.map((item) => ({ ...item }));
465
+ if (pinned.length !== this.plan.pinnedSlots)
466
+ throw new Error(
467
+ `expert pinned set has ${pinned.length} entries; plan reserves ${this.plan.pinnedSlots}`,
468
+ );
469
+ for (const item of pinned) {
470
+ const key = expertKey(item.layer, item.expertId);
471
+ if (this.#pinnedKeys.has(key)) throw new Error(`duplicate pinned expert ${key}`);
472
+ this.#pinnedKeys.add(key);
473
+ }
474
+
475
+ let slot = 0;
476
+ for (; slot < this.plan.workingSlots; slot++) {
477
+ this.#slots.push(this.#newSlot(slot, "working", null));
478
+ this.#working.add(slot);
479
+ }
480
+ for (const layer of this.sparseLayerIds) {
481
+ const layerSlots = new Set<number>();
482
+ for (let index = 0; index < this.plan.slotsPerLayer; index++, slot++) {
483
+ this.#slots.push(this.#newSlot(slot, "layer", layer));
484
+ layerSlots.add(slot);
485
+ }
486
+ this.#layerSlots.set(layer, layerSlots);
487
+ }
488
+ for (; slot < this.plan.totalSlots; slot++)
489
+ this.#slots.push(this.#newSlot(slot, "pinned", null));
490
+ }
491
+
492
+ /** Populate the configured hot-store before the first model forward. */
493
+ async preloadPinned(): Promise<void> {
494
+ if (this.#active)
495
+ throw new Error("pinned preload requires a model safe point");
496
+ for (const layer of this.sparseLayerIds) {
497
+ const ids = this.#pinned
498
+ .filter((item) => item.layer === layer)
499
+ .map((item) => item.expertId);
500
+ for (let begin = 0; begin < ids.length; begin += this.plan.workingSlots) {
501
+ const lease = await this.acquireBlock(
502
+ layer,
503
+ ids.slice(begin, begin + this.plan.workingSlots),
504
+ undefined,
505
+ "policy",
506
+ );
507
+ lease.releaseFenced();
508
+ }
509
+ }
510
+ }
511
+
512
+ repinCandidates(): ExpertLfruCandidate[] {
513
+ if (this.#active)
514
+ throw new Error("live repin requires a model safe point");
515
+ if (!this.#usage) return [];
516
+ const candidates: ExpertLfruCandidate[] = [];
517
+ for (const layer of this.sparseLayerIds) {
518
+ const ids = this.#pinned
519
+ .filter((item) => item.layer === layer)
520
+ .map((item) => item.expertId);
521
+ const candidate = this.#usage.lfruCandidate(layer, ids);
522
+ if (candidate) candidates.push(candidate);
523
+ }
524
+ return candidates;
525
+ }
526
+
527
+ /** Apply one previously selected LFRU promotion at a model safe point. */
528
+ async applyRepin(candidate: ExpertLfruCandidate): Promise<ExpertRepinEvent> {
529
+ if (this.#active)
530
+ throw new Error("live repin requires a model safe point");
531
+ const pinIndex = this.#pinned.findIndex((item) =>
532
+ item.layer === candidate.layer &&
533
+ item.expertId === candidate.coldExpertId);
534
+ if (pinIndex < 0 || this.#pinnedKeys.has(
535
+ expertKey(candidate.layer, candidate.hotExpertId),
536
+ )) {
537
+ throw new Error("stale live-repin candidate");
538
+ }
539
+
540
+ let hotSlotId = this.#lookup.get(
541
+ expertKey(candidate.layer, candidate.hotExpertId),
542
+ );
543
+ if (hotSlotId === undefined) {
544
+ const lease = await this.acquireBlock(
545
+ candidate.layer,
546
+ [candidate.hotExpertId],
547
+ undefined,
548
+ "policy",
549
+ );
550
+ lease.releaseFenced();
551
+ hotSlotId = this.#lookup.get(
552
+ expertKey(candidate.layer, candidate.hotExpertId),
553
+ );
554
+ }
555
+ const coldSlotId = this.#lookup.get(
556
+ expertKey(candidate.layer, candidate.coldExpertId),
557
+ );
558
+ if (hotSlotId === undefined || coldSlotId === undefined)
559
+ throw new Error("live repin could not materialize both experts");
560
+ const hot = this.#slots[hotSlotId]!;
561
+ const cold = this.#slots[coldSlotId]!;
562
+ if (hot.role !== "layer" || hot.phase !== "ready" ||
563
+ cold.role !== "pinned" || cold.phase !== "ready") {
564
+ throw new Error("live repin requires ready resident and pinned slots");
565
+ }
566
+
567
+ const layerSlots = this.#layerSlots.get(candidate.layer)!;
568
+ layerSlots.delete(hot.id);
569
+ layerSlots.add(cold.id);
570
+ hot.role = "pinned";
571
+ cold.role = "layer";
572
+ cold.lastUse = 0n;
573
+ this.#pinnedKeys.delete(
574
+ expertKey(candidate.layer, candidate.coldExpertId),
575
+ );
576
+ this.#pinnedKeys.add(
577
+ expertKey(candidate.layer, candidate.hotExpertId),
578
+ );
579
+ this.#pinned[pinIndex] = {
580
+ layer: candidate.layer,
581
+ expertId: candidate.hotExpertId,
582
+ };
583
+ this.#repinSwaps++;
584
+ return {
585
+ layer: candidate.layer,
586
+ evictedPin: candidate.coldExpertId,
587
+ admittedPin: candidate.hotExpertId,
588
+ gain: candidate.gain,
589
+ };
590
+ }
591
+
592
+ residencyMap(): ExpertResidencyMapEntry[] {
593
+ return this.#slots.flatMap((slot) => {
594
+ if ((slot.role !== "layer" && slot.role !== "pinned") ||
595
+ slot.phase !== "ready" || slot.layer === null ||
596
+ slot.expertId === null) return [];
597
+ return [{
598
+ slot: slot.id,
599
+ layer: slot.layer,
600
+ expertId: slot.expertId,
601
+ tier: slot.role === "pinned" ? "pinned" as const : "resident" as const,
602
+ lastUse: slot.lastUse,
603
+ usage: this.#usage?.entry(slot.layer, slot.expertId) ?? null,
604
+ }];
605
+ });
606
+ }
607
+
608
+ /**
609
+ * Queue future-layer advisory reads without allocating or mutating slots.
610
+ * Resident predictions are skipped; queue pressure and advisory failures
611
+ * are deliberately non-fatal because demand remains the correctness path.
612
+ */
613
+ hintExperts(layer: number, expertIds: readonly number[]): void {
614
+ if (!this.#layerSlots.has(layer))
615
+ throw new RangeError(`layer ${layer} is not a sparse residency layer`);
616
+ for (const expertId of new Set(expertIds)) {
617
+ if (!Number.isSafeInteger(expertId) || expertId < 0)
618
+ throw new RangeError(`invalid hinted expert id ${expertId}`);
619
+ this.#hintCandidates++;
620
+ if (this.#lookup.has(expertKey(layer, expertId))) {
621
+ this.#hintResidentSkipped++;
622
+ continue;
623
+ }
624
+ try {
625
+ const location = this.#locate(layer, expertId);
626
+ const segments = location.hintSegments ?? location.segments;
627
+ if (segments.length === 0) continue;
628
+ if (!this.#backend.hintSegments)
629
+ throw new Error("expert backend has no advisory hint queue");
630
+ this.#backend.hintSegments(segments);
631
+ } catch {
632
+ this.#hintSubmitErrors++;
633
+ }
634
+ }
635
+ }
636
+
637
+ /** Return and reset demand-only I/O telemetry for one generation turn. */
638
+ drainDemandTelemetry(): ExpertDemandTelemetry {
639
+ const telemetry = {
640
+ hits: this.#telemetryHits,
641
+ misses: this.#telemetryMisses,
642
+ loads: this.#telemetryLoads,
643
+ readOperations: this.#telemetryReadOperations,
644
+ readBytes: this.#telemetryReadBytes,
645
+ diskService: summarizeExpertLatencies(this.#diskServiceMs),
646
+ foregroundWait: summarizeExpertLatencies(this.#foregroundWaitMs),
647
+ };
648
+ this.#telemetryHits = 0;
649
+ this.#telemetryMisses = 0;
650
+ this.#telemetryLoads = 0;
651
+ this.#telemetryReadOperations = 0;
652
+ this.#telemetryReadBytes = 0;
653
+ this.#diskServiceMs = [];
654
+ this.#foregroundWaitMs = [];
655
+ return telemetry;
656
+ }
657
+
658
+ /** Return and reset startup-preload/live-repin I/O separately from demand. */
659
+ drainPolicyTelemetry(): ExpertPolicyTelemetry {
660
+ const telemetry = {
661
+ hits: this.#policyHits,
662
+ misses: this.#policyMisses,
663
+ loads: this.#policyLoads,
664
+ readOperations: this.#policyReadOperations,
665
+ readBytes: this.#policyReadBytes,
666
+ diskService: summarizeExpertLatencies(this.#policyDiskServiceMs),
667
+ foregroundWait: summarizeExpertLatencies(this.#policyForegroundWaitMs),
668
+ };
669
+ this.#policyHits = 0;
670
+ this.#policyMisses = 0;
671
+ this.#policyLoads = 0;
672
+ this.#policyReadOperations = 0;
673
+ this.#policyReadBytes = 0;
674
+ this.#policyDiskServiceMs = [];
675
+ this.#policyForegroundWaitMs = [];
676
+ return telemetry;
677
+ }
678
+
679
+ /** Return and reset per-turn advisory PILOT telemetry. */
680
+ drainHintTelemetry(): ExpertHintTelemetry {
681
+ const current = this.#backend.hintTelemetry?.() ?? EMPTY_HINT_SNAPSHOT;
682
+ const prior = this.#hintBaseline;
683
+ const difference = (key: keyof ExpertIOHintSnapshot): number =>
684
+ Math.max(0, current[key] - prior[key]);
685
+ const telemetry = {
686
+ candidates: this.#hintCandidates,
687
+ residentSkipped: this.#hintResidentSkipped,
688
+ submitErrors: this.#hintSubmitErrors,
689
+ submitted: difference("submitted"),
690
+ completed: difference("completed"),
691
+ dropped: difference("dropped"),
692
+ operations: difference("operations"),
693
+ bytes: difference("bytes"),
694
+ errors: difference("errors"),
695
+ queueDepth: current.queueDepth,
696
+ inFlight: current.inFlight,
697
+ };
698
+ this.#hintCandidates = 0;
699
+ this.#hintResidentSkipped = 0;
700
+ this.#hintSubmitErrors = 0;
701
+ this.#hintBaseline = current;
702
+ return telemetry;
703
+ }
704
+
705
+ /** Record the full router output before batch-union deduplication. */
706
+ recordRoutes(layer: number, routes: readonly ExpertUsageRoute[]): void {
707
+ this.#usage?.recordRoutes(layer, routes);
708
+ this.#routeObserver?.(layer, routes);
709
+ }
710
+
711
+ async acquireBlock(
712
+ layer: number,
713
+ expertIds: readonly number[],
714
+ beforeMissSubmit?: (
715
+ resident: readonly ExpertResidencyLeaseEntry[],
716
+ ) => void | Promise<void>,
717
+ classification: "demand" | "policy" = "demand",
718
+ ): Promise<ExpertBatchLease> {
719
+ if (this.#active)
720
+ throw new Error("concurrent streamed expert waves are deferred to G7");
721
+ const layerSlots = this.#layerSlots.get(layer);
722
+ if (!layerSlots) throw new RangeError(`layer ${layer} is not a sparse residency layer`);
723
+ const unique = [...new Set(expertIds)];
724
+ if (unique.length !== expertIds.length)
725
+ throw new Error("expert block must already be deduplicated");
726
+ if (unique.length > this.plan.workingSlots)
727
+ throw new Error(
728
+ `expert block has ${unique.length} unique experts; working capacity is ` +
729
+ `${this.plan.workingSlots}`,
730
+ );
731
+ for (const expertId of unique)
732
+ if (!Number.isSafeInteger(expertId) || expertId < 0)
733
+ throw new RangeError(`invalid expert id ${expertId}`);
734
+
735
+ this.#active = true;
736
+ const entries: ExpertResidencyLeaseEntry[] = [];
737
+ const loaded: SlotRecord[] = [];
738
+ const loadStarted = new Map<number, number>();
739
+ try {
740
+ for (const expertId of unique) {
741
+ const hitId = this.#lookup.get(expertKey(layer, expertId));
742
+ if (hitId === undefined) continue;
743
+ const record = this.#slots[hitId]!;
744
+ if (record.phase !== "ready")
745
+ throw new Error(`resident expert ${layer}:${expertId} is not ready`);
746
+ this.#backend.lease(record.id, record.generation, "gpu");
747
+ record.phase = "leased";
748
+ record.lastUse = ++this.#clock;
749
+ if (classification === "demand") {
750
+ this.#hits++;
751
+ this.#telemetryHits++;
752
+ } else {
753
+ this.#policyHits++;
754
+ }
755
+ entries.push(this.#entry(record, true));
756
+ }
757
+
758
+ const misses = unique.filter(
759
+ (expertId) => !entries.some((entry) => entry.expertId === expertId),
760
+ );
761
+ await beforeMissSubmit?.(Object.freeze(entries.slice()));
762
+ const scratch = [...this.#working]
763
+ .map((id) => this.#slots[id]!)
764
+ .filter((record) => record.phase !== "leased" && record.phase !== "loading")
765
+ .sort((a, b) => a.id - b.id);
766
+ if (scratch.length < misses.length)
767
+ throw new Error(`working tier has ${scratch.length} free slots for ${misses.length} misses`);
768
+
769
+ for (let index = 0; index < misses.length; index++) {
770
+ const expertId = misses[index]!;
771
+ const record = scratch[index]!;
772
+ if (record.phase === "ready") {
773
+ if (record.layer !== null && record.expertId !== null)
774
+ this.#lookup.delete(expertKey(record.layer, record.expertId));
775
+ this.#backend.discard(record.id, record.generation);
776
+ record.phase = "idle";
777
+ record.layer = null;
778
+ record.expertId = null;
779
+ }
780
+ const location = this.#locate(layer, expertId);
781
+ if (location.layer !== layer || location.expertId !== expertId)
782
+ throw new Error(`expert locator returned ${location.layer}:${location.expertId}`);
783
+ record.generation = ++this.#generation;
784
+ record.layer = layer;
785
+ record.expertId = expertId;
786
+ const submittedAt = performance.now();
787
+ this.#backend.submitSegments(record.id, record.generation, location.segments);
788
+ record.phase = "loading";
789
+ loaded.push(record);
790
+ loadStarted.set(record.id, submittedAt);
791
+ if (classification === "demand") {
792
+ this.#misses++;
793
+ this.#telemetryMisses++;
794
+ this.#telemetryLoads++;
795
+ this.#telemetryReadOperations += location.segments.length;
796
+ this.#telemetryReadBytes += location.segments.reduce(
797
+ (sum, segment) => sum + segment.length,
798
+ 0,
799
+ );
800
+ } else {
801
+ this.#policyMisses++;
802
+ this.#policyLoads++;
803
+ this.#policyReadOperations += location.segments.length;
804
+ this.#policyReadBytes += location.segments.reduce(
805
+ (sum, segment) => sum + segment.length,
806
+ 0,
807
+ );
808
+ }
809
+ }
810
+
811
+ const foregroundStarted = loaded.length
812
+ ? performance.now()
813
+ : null;
814
+ const waits = await Promise.allSettled(loaded.map(async (record) => {
815
+ try {
816
+ await this.#backend.wait(record.id, record.generation);
817
+ } finally {
818
+ const started = loadStarted.get(record.id);
819
+ if (started !== undefined) {
820
+ const samples = classification === "demand"
821
+ ? this.#diskServiceMs
822
+ : this.#policyDiskServiceMs;
823
+ samples.push(performance.now() - started);
824
+ }
825
+ }
826
+ }));
827
+ if (foregroundStarted !== null) {
828
+ const samples = classification === "demand"
829
+ ? this.#foregroundWaitMs
830
+ : this.#policyForegroundWaitMs;
831
+ samples.push(performance.now() - foregroundStarted);
832
+ }
833
+ for (let index = 0; index < loaded.length; index++) {
834
+ const record = loaded[index]!;
835
+ record.phase = "ready";
836
+ if (waits[index]!.status === "rejected") continue;
837
+ this.#lookup.set(expertKey(layer, record.expertId!), record.id);
838
+ this.#backend.lease(record.id, record.generation, "gpu");
839
+ record.phase = "leased";
840
+ record.lastUse = ++this.#clock;
841
+ entries.push(this.#entry(record, false));
842
+ }
843
+ const failed = waits.find(
844
+ (result): result is PromiseRejectedResult => result.status === "rejected",
845
+ );
846
+ if (failed) throw failed.reason;
847
+ entries.sort(
848
+ (left, right) =>
849
+ expertIds.indexOf(left.expertId) - expertIds.indexOf(right.expertId),
850
+ );
851
+ return new ExpertBatchLease(this, entries);
852
+ } catch (error) {
853
+ const cleanupErrors: unknown[] = [];
854
+ if (entries.some((entry) => this.#slots[entry.slot]!.phase === "leased")) {
855
+ try {
856
+ const [{ gpuStream }, { synchronize }] = await Promise.all([
857
+ import("@mlx-bun/mlx/array"),
858
+ import("@mlx-bun/mlx/ffi"),
859
+ ]);
860
+ synchronize(gpuStream);
861
+ } catch (cleanupError) {
862
+ cleanupErrors.push(cleanupError);
863
+ }
864
+ }
865
+ for (const entry of entries) {
866
+ const record = this.#slots[entry.slot]!;
867
+ if (record.phase === "leased") {
868
+ try {
869
+ this.#backend.releaseGpuFenced(record.id, record.generation);
870
+ record.phase = "ready";
871
+ } catch (cleanupError) {
872
+ cleanupErrors.push(cleanupError);
873
+ this.#disable(record);
874
+ }
875
+ }
876
+ }
877
+ for (const record of loaded) {
878
+ if (record.role === "disabled") continue;
879
+ if (record.phase === "loading") {
880
+ try {
881
+ this.#backend.cancel(record.id, record.generation);
882
+ } catch (cleanupError) {
883
+ cleanupErrors.push(cleanupError);
884
+ // A completed job may already be READY; drain below either way.
885
+ }
886
+ try {
887
+ await this.#backend.wait(record.id, record.generation);
888
+ } catch {
889
+ // Cancellation/read errors still publish a terminal READY slot.
890
+ }
891
+ record.phase = "ready";
892
+ }
893
+ if (record.phase === "leased") {
894
+ try {
895
+ this.#backend.releaseGpuFenced(record.id, record.generation);
896
+ record.phase = "ready";
897
+ } catch (cleanupError) {
898
+ cleanupErrors.push(cleanupError);
899
+ this.#disable(record);
900
+ }
901
+ }
902
+ if (record.phase === "ready") {
903
+ this.#lookup.delete(expertKey(layer, record.expertId!));
904
+ try {
905
+ this.#backend.discard(record.id, record.generation);
906
+ record.phase = "idle";
907
+ record.layer = null;
908
+ record.expertId = null;
909
+ } catch (cleanupError) {
910
+ cleanupErrors.push(cleanupError);
911
+ this.#disable(record);
912
+ }
913
+ }
914
+ }
915
+ this.#active = false;
916
+ if (cleanupErrors.length) {
917
+ throw new AggregateError(
918
+ [error, ...cleanupErrors],
919
+ "expert acquire failed and cleanup was incomplete",
920
+ );
921
+ }
922
+ throw error;
923
+ }
924
+ }
925
+
926
+ releaseFenced(entries: readonly ExpertResidencyLeaseEntry[]): void {
927
+ if (!this.#active) throw new Error("no active expert batch lease");
928
+ const protectedSlots = new Set(entries.map((entry) => entry.slot));
929
+ for (const entry of entries) {
930
+ const record = this.#slots[entry.slot]!;
931
+ if (record.phase !== "leased" || record.generation !== entry.generation) {
932
+ this.#active = false;
933
+ throw new Error(`stale expert lease ${entry.layer}:${entry.expertId}`);
934
+ }
935
+ }
936
+ const releaseErrors: unknown[] = [];
937
+ for (const entry of entries) {
938
+ const record = this.#slots[entry.slot]!;
939
+ try {
940
+ this.#backend.releaseGpuFenced(record.id, record.generation);
941
+ record.phase = "ready";
942
+ } catch (error) {
943
+ releaseErrors.push(error);
944
+ this.#disable(record);
945
+ }
946
+ }
947
+ if (releaseErrors.length) {
948
+ for (const entry of entries) {
949
+ const record = this.#slots[entry.slot]!;
950
+ if (entry.hit || record.phase !== "ready") continue;
951
+ this.#lookup.delete(expertKey(entry.layer, entry.expertId));
952
+ try {
953
+ this.#backend.discard(record.id, record.generation);
954
+ record.phase = "idle";
955
+ record.layer = null;
956
+ record.expertId = null;
957
+ } catch (error) {
958
+ releaseErrors.push(error);
959
+ this.#disable(record);
960
+ }
961
+ }
962
+ this.#active = false;
963
+ throw new AggregateError(releaseErrors, "expert lease release failed");
964
+ }
965
+
966
+ try {
967
+ const misses = entries.filter((entry) => !entry.hit).reverse();
968
+ const promoted = new Set<number>();
969
+ for (const entry of misses) {
970
+ const record = this.#slots[entry.slot]!;
971
+ if (this.#pinnedKeys.has(expertKey(entry.layer, entry.expertId))) {
972
+ const target = this.#slots.find(
973
+ (candidate) => candidate.role === "pinned" && candidate.phase === "idle",
974
+ );
975
+ if (target) {
976
+ this.#swapRoles(record, target, "pinned", null);
977
+ promoted.add(record.id);
978
+ continue;
979
+ }
980
+ }
981
+ const target = this.#layerVictim(entry.layer, protectedSlots);
982
+ if (!target) continue;
983
+ if (target.phase === "ready") {
984
+ this.#lookup.delete(expertKey(target.layer!, target.expertId!));
985
+ this.#backend.discard(target.id, target.generation);
986
+ target.phase = "idle";
987
+ target.expertId = null;
988
+ this.#evictions++;
989
+ }
990
+ this.#swapRoles(record, target, "layer", entry.layer);
991
+ promoted.add(record.id);
992
+ }
993
+
994
+ for (const entry of misses) {
995
+ if (promoted.has(entry.slot)) continue;
996
+ const record = this.#slots[entry.slot]!;
997
+ this.#lookup.delete(expertKey(entry.layer, entry.expertId));
998
+ this.#backend.discard(record.id, record.generation);
999
+ record.phase = "idle";
1000
+ record.layer = null;
1001
+ record.expertId = null;
1002
+ }
1003
+ } finally {
1004
+ this.#active = false;
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * At a model safe point, decommit oldest unpinned resident slots until the
1010
+ * live physical footprint falls under budget. Capacity only moves downward.
1011
+ */
1012
+ correctForPressure(): ExpertResidencySnapshot {
1013
+ if (this.#active)
1014
+ throw new Error("physical-pressure correction requires a model safe point");
1015
+ let footprint = this.#backend.physicalFootprint();
1016
+ while (footprint > this.plan.budgetBytes) {
1017
+ const victim = this.#slots
1018
+ .filter((record) =>
1019
+ record.role === "layer" &&
1020
+ record.phase === "ready" &&
1021
+ this.#layerSlots.get(record.layer!)!.size > 1)
1022
+ .sort((a, b) =>
1023
+ a.lastUse === b.lastUse ? a.id - b.id : a.lastUse < b.lastUse ? -1 : 1)[0];
1024
+ if (!victim) break;
1025
+ this.#lookup.delete(expertKey(victim.layer!, victim.expertId!));
1026
+ this.#backend.discard(victim.id, victim.generation);
1027
+ this.#layerSlots.get(victim.layer!)!.delete(victim.id);
1028
+ victim.phase = "idle";
1029
+ victim.role = "disabled";
1030
+ victim.layer = null;
1031
+ victim.expertId = null;
1032
+ this.#pressureEvictions++;
1033
+ footprint = this.#backend.physicalFootprint();
1034
+ }
1035
+ if (footprint > this.plan.budgetBytes) {
1036
+ throw new Error(
1037
+ `expert physical footprint ${footprint} exceeds budget ` +
1038
+ `${this.plan.budgetBytes}, and no evictable capacity remains`,
1039
+ );
1040
+ }
1041
+ return this.snapshot(footprint);
1042
+ }
1043
+
1044
+ snapshot(physicalFootprint = this.#backend.physicalFootprint()): ExpertResidencySnapshot {
1045
+ return {
1046
+ clock: this.#clock,
1047
+ generation: this.#generation,
1048
+ working: this.#slots.filter((slot) => slot.role === "working").length,
1049
+ resident: this.#slots.filter((slot) => slot.role === "layer").length,
1050
+ pinned: this.#slots.filter((slot) => slot.role === "pinned").length,
1051
+ disabled: this.#slots.filter((slot) => slot.role === "disabled").length,
1052
+ loading: this.#slots.filter((slot) => slot.phase === "loading").length,
1053
+ leased: this.#slots.filter((slot) => slot.phase === "leased").length,
1054
+ physicalFootprint,
1055
+ budgetBytes: this.plan.budgetBytes,
1056
+ hits: this.#hits,
1057
+ misses: this.#misses,
1058
+ evictions: this.#evictions,
1059
+ pressureEvictions: this.#pressureEvictions,
1060
+ repinSwaps: this.#repinSwaps,
1061
+ };
1062
+ }
1063
+
1064
+ #newSlot(id: number, role: SlotRole, layer: number | null): SlotRecord {
1065
+ return {
1066
+ id,
1067
+ role,
1068
+ phase: "idle",
1069
+ layer,
1070
+ expertId: null,
1071
+ generation: 0n,
1072
+ lastUse: 0n,
1073
+ };
1074
+ }
1075
+
1076
+ #disable(record: SlotRecord): void {
1077
+ if (record.layer !== null && record.expertId !== null)
1078
+ this.#lookup.delete(expertKey(record.layer, record.expertId));
1079
+ this.#working.delete(record.id);
1080
+ if (record.layer !== null)
1081
+ this.#layerSlots.get(record.layer)?.delete(record.id);
1082
+ record.role = "disabled";
1083
+ record.layer = null;
1084
+ record.expertId = null;
1085
+ }
1086
+
1087
+ #entry(record: SlotRecord, hit: boolean): ExpertResidencyLeaseEntry {
1088
+ return {
1089
+ layer: record.layer!,
1090
+ expertId: record.expertId!,
1091
+ slot: record.id,
1092
+ generation: record.generation,
1093
+ hit,
1094
+ };
1095
+ }
1096
+
1097
+ #layerVictim(layer: number, protectedSlots: ReadonlySet<number>): SlotRecord | null {
1098
+ const candidates = [...this.#layerSlots.get(layer)!]
1099
+ .map((id) => this.#slots[id]!)
1100
+ .filter((record) =>
1101
+ !protectedSlots.has(record.id) &&
1102
+ (record.phase === "idle" || record.phase === "ready"))
1103
+ .sort((a, b) => {
1104
+ if (a.phase === "idle" && b.phase !== "idle") return -1;
1105
+ if (b.phase === "idle" && a.phase !== "idle") return 1;
1106
+ if (a.lastUse !== b.lastUse) return a.lastUse < b.lastUse ? -1 : 1;
1107
+ return a.id - b.id;
1108
+ });
1109
+ return candidates[0] ?? null;
1110
+ }
1111
+
1112
+ #swapRoles(
1113
+ loaded: SlotRecord,
1114
+ target: SlotRecord,
1115
+ destinationRole: "layer" | "pinned",
1116
+ destinationLayer: number | null,
1117
+ ): void {
1118
+ if (loaded.role !== "working" || loaded.phase !== "ready" ||
1119
+ target.phase !== "idle")
1120
+ throw new Error("expert promotion requires ready working and idle destination slots");
1121
+ this.#working.delete(loaded.id);
1122
+ this.#working.add(target.id);
1123
+ target.role = "working";
1124
+ target.layer = null;
1125
+ target.expertId = null;
1126
+ loaded.role = destinationRole;
1127
+ loaded.layer = destinationLayer ?? loaded.layer;
1128
+ if (destinationRole === "layer") {
1129
+ const set = this.#layerSlots.get(destinationLayer!)!;
1130
+ set.delete(target.id);
1131
+ set.add(loaded.id);
1132
+ }
1133
+ }
1134
+ }