mlx-bun 0.0.8 → 0.0.10

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 (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
package/src/sampler.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  // Sampling — port of mlx-lm's sample_utils.make_sampler /
2
- // make_logits_processors (temperature, top-p, top-k, repetition penalty).
2
+ // make_logits_processors (temperature, top-p, top-k, min-p, XTC,
3
+ // repetition/presence/frequency penalties, logit bias).
3
4
  // All filtering happens on-device; only the chosen token id crosses to JS.
4
5
  // Seeded: each step derives a fresh key from (seed, step) so runs are
5
- // reproducible without sharing global RNG state.
6
+ // reproducible without sharing global RNG state. (mlx-lm draws XTC's
7
+ // uniform and the categorical from one global stream; we split the
8
+ // per-step key into (xtc, categorical) subkeys instead — deterministic,
9
+ // and the XTC-off path is unchanged.)
6
10
 
7
11
  import { Dtype } from "./mlx/ffi";
8
12
  import { MlxArray } from "./mlx/array";
@@ -63,6 +67,18 @@ export interface SamplerOptions {
63
67
  temperature?: number;
64
68
  topP?: number;
65
69
  topK?: number;
70
+ /** min-p: keep tokens whose probability ≥ minP · p(top token). mlx-lm's
71
+ * `min_p`. Applied after top-p, before XTC/top-k (reference chain order). */
72
+ minP?: number;
73
+ /** Minimum number of tokens min-p may never filter out. Default 1. */
74
+ minTokensToKeep?: number;
75
+ /** XTC ("exclude top choices"): with this probability per step, drop every
76
+ * token above the threshold EXCEPT the least likely of them. */
77
+ xtcProbability?: number;
78
+ /** Probability a token must exceed to be an XTC removal candidate. [0, 0.5]. */
79
+ xtcThreshold?: number;
80
+ /** Token ids XTC never removes (mlx-lm's server passes EOS + "\n"). */
81
+ xtcSpecialTokens?: number[];
66
82
  seed?: number;
67
83
  /** HLG tone-curve sampling. When enabled, replaces temperature's flat slope
68
84
  * with the piecewise curve (temperature becomes the mid gain). Off/undefined
@@ -75,8 +91,19 @@ export interface SamplerOptions {
75
91
  }
76
92
 
77
93
  export interface LogitsProcessorOptions {
94
+ /** Additive per-token-id logit bias, applied before any penalty
95
+ * (mlx-lm's `logit_bias`: {tokenId: bias}). */
96
+ logitBias?: Record<number, number>;
78
97
  repetitionPenalty?: number;
79
98
  repetitionContextSize?: number;
99
+ /** OpenAI-style presence penalty: subtracted ONCE from a token's logit if it
100
+ * occurred at all in the recent window. May be negative. */
101
+ presencePenalty?: number;
102
+ presenceContextSize?: number;
103
+ /** OpenAI-style frequency penalty: subtracted once PER OCCURRENCE of the
104
+ * token in the recent window. May be negative. */
105
+ frequencyPenalty?: number;
106
+ frequencyContextSize?: number;
80
107
  }
81
108
 
82
109
  /** logprobs [1, V] → sampled token array (uint32, shape [1]). */
@@ -98,7 +125,7 @@ function negInfLike(a: MlxArray): MlxArray {
98
125
 
99
126
  /** apply_top_p: keep the smallest set of tokens whose cumulative
100
127
  * probability exceeds top_p; others → -inf. */
101
- function applyTopP(lp: MlxArray, topP: number): MlxArray {
128
+ export function applyTopP(lp: MlxArray, topP: number): MlxArray {
102
129
  const probs = ops.exp(lp);
103
130
  const sortedIdx = ops.argsortAxis(lp, -1);
104
131
  const sortedProbs = ops.takeAlongAxis(probs, sortedIdx, -1);
@@ -120,7 +147,7 @@ function applyTopP(lp: MlxArray, topP: number): MlxArray {
120
147
  }
121
148
 
122
149
  /** apply_top_k: all but the k highest logprobs → -inf. */
123
- function applyTopK(lp: MlxArray, topK: number): MlxArray {
150
+ export function applyTopK(lp: MlxArray, topK: number): MlxArray {
124
151
  const V = lp.shape[lp.shape.length - 1]!;
125
152
  const negLp = ops.neg(lp);
126
153
  const part = ops.argpartitionAxis(negLp, topK - 1, -1);
@@ -131,6 +158,88 @@ function applyTopK(lp: MlxArray, topK: number): MlxArray {
131
158
  return out;
132
159
  }
133
160
 
161
+ /** apply_min_p: mask tokens whose probability is below minP · p(top token).
162
+ * Faithful to the reference (mlx-lm 0.31.3 sample_utils.apply_min_p): the
163
+ * threshold is computed in log space directly against the max logprob —
164
+ * `scaled_min_p = max(lp) + log(minP)` — no full sort; min_tokens_to_keep
165
+ * survivors are recovered with an argpartition on the top tail. */
166
+ export function applyMinP(lp: MlxArray, minP: number, minTokensToKeep = 1): MlxArray {
167
+ if (!(minP >= 0 && minP <= 1))
168
+ throw new Error(`minP has to be a float in the [0, 1] interval, but is ${minP}`);
169
+ if (!Number.isInteger(minTokensToKeep) || minTokensToKeep < 1)
170
+ throw new Error(`minTokensToKeep has to be a positive integer, but is ${minTokensToKeep}`);
171
+
172
+ const owned: MlxArray[] = [];
173
+ const k = <T extends MlxArray>(a: T): T => { owned.push(a); return a; };
174
+
175
+ // Mask tokens that have a probability less than max(p) * min_p
176
+ const top = k(ops.maxAxis(lp, -1, true));
177
+ const scaledMinP = k(ops.add(top, k(ops.scalarLike(Math.log(minP), top))));
178
+ let remove = k(ops.less(lp, scaledMinP));
179
+
180
+ // Ensure at least min_tokens_to_keep survive the filter
181
+ if (minTokensToKeep > 1) {
182
+ const V = lp.shape[lp.shape.length - 1]!;
183
+ // mx.argpartition(lp, kth=-min_tokens_to_keep): negative kth ≡ V - k
184
+ const part = k(ops.argpartitionAxis(lp, V - minTokensToKeep, -1));
185
+ const topIdx = k(part.slice([0, V - minTokensToKeep], [1, V]));
186
+ const falses = k(ops.zeros([1, minTokensToKeep], Dtype.bool));
187
+ remove = k(ops.putAlongAxis(remove, topIdx, falses, -1));
188
+ }
189
+
190
+ const ninf = k(negInfLike(lp));
191
+ const out = ops.where(remove, ninf, lp);
192
+ for (const a of owned) a.dispose();
193
+ return out;
194
+ }
195
+
196
+ /** apply_xtc: with probability xtcProbability, remove every token whose
197
+ * probability exceeds the minimum above-threshold probability — i.e. drop
198
+ * the top choices, keeping the least likely token that still clears the
199
+ * threshold (plus everything below it). Special tokens are never removed.
200
+ * `key` seeds the per-step uniform draw (the reference uses the global RNG). */
201
+ export function applyXtc(
202
+ logits: MlxArray,
203
+ xtcProbability: number,
204
+ xtcThreshold: number,
205
+ xtcSpecialTokens: number[],
206
+ key: MlxArray | null,
207
+ ): MlxArray {
208
+ if (!(xtcThreshold >= 0 && xtcThreshold <= 0.5))
209
+ throw new Error(`xtcThreshold has to be a float in the [0, 0.5] interval, but is ${xtcThreshold}`);
210
+ if (!(xtcProbability >= 0 && xtcProbability <= 1))
211
+ throw new Error(`xtcProbability has to be a float in the [0, 1] interval, but is ${xtcProbability}`);
212
+
213
+ const owned: MlxArray[] = [];
214
+ const k = <T extends MlxArray>(a: T): T => { owned.push(a); return a; };
215
+
216
+ const probs = k(ops.softmaxAxis(logits, -1, false));
217
+ // mask = probs > min(where(probs > threshold, probs, inf))
218
+ const thresh = k(ops.scalarLike(xtcThreshold, probs));
219
+ const above = k(ops.less(thresh, probs)); // probs > threshold
220
+ const inf = k(ops.scalarLike(Infinity, probs));
221
+ const aboveProbs = k(ops.where(above, probs, inf));
222
+ // min(x) = -max(-x) (no min reduction in the ops layer)
223
+ const minAbove = k(ops.neg(k(ops.maxAxis(k(ops.neg(aboveProbs)), -1, true))));
224
+ let mask = k(ops.less(minAbove, probs)); // probs > min above-threshold prob
225
+ if (xtcSpecialTokens.length > 0) {
226
+ const n = xtcSpecialTokens.length;
227
+ const idx = k(ops.fromInt32(xtcSpecialTokens, [1, n]));
228
+ const falses = k(ops.zeros([1, n], Dtype.bool));
229
+ mask = k(ops.putAlongAxis(mask, idx, falses, -1));
230
+ }
231
+
232
+ // where(uniform() > xtcProbability, logits, where(mask, -inf, logits))
233
+ const u = k(ops.randomUniform([1], Dtype.float32, 0, 1, key));
234
+ const pArr = k(ops.scalarLike(xtcProbability, u));
235
+ const skip = k(ops.less(pArr, u)); // u > xtcProbability → leave logits alone
236
+ const ninf = k(negInfLike(logits));
237
+ const culled = k(ops.where(mask, ninf, logits));
238
+ const out = ops.where(skip, logits, culled);
239
+ for (const a of owned) a.dispose();
240
+ return out;
241
+ }
242
+
134
243
  /** HLG sampling parameters — a piecewise tone curve on the logprobs.
135
244
  * See docs/design/hlg-sampling.md. Knobs are in nats (the logprob unit). */
136
245
  export interface HlgParams {
@@ -410,7 +519,11 @@ export function applyHlgShaper(lp: MlxArray, opts: HlgShaperParams = {}): MlxArr
410
519
  }
411
520
 
412
521
  export function makeSampler(opts: SamplerOptions = {}): Sampler {
413
- const { temperature = 0, topP = 0, topK = 0, seed = 0, hlg, curve } = opts;
522
+ const {
523
+ temperature = 0, topP = 0, topK = 0, minP = 0, minTokensToKeep = 1,
524
+ xtcProbability = 0, xtcThreshold = 0, xtcSpecialTokens = [],
525
+ seed = 0, hlg, curve,
526
+ } = opts;
414
527
 
415
528
  // v2 curve sampler: the drawn log-prob transfer curve REPLACES temperature +
416
529
  // softmax. Stochastic and seeded by design (the curve IS the sampling shape),
@@ -461,13 +574,29 @@ export function makeSampler(opts: SamplerOptions = {}): Sampler {
461
574
  };
462
575
  }
463
576
 
577
+ // Filter chain order matches mlx-lm's make_sampler: top_p → min_p → xtc →
578
+ // top_k, then categorical(logprobs / temperature).
464
579
  return (lp, step) => {
465
580
  let cur = lp;
466
581
  const owned: MlxArray[] = [];
582
+ let key = stepKey(seed, step);
467
583
  if (topP > 0 && topP < 1) { cur = applyTopP(cur, topP); owned.push(cur); }
584
+ if (minP !== 0) { cur = applyMinP(cur, minP, minTokensToKeep); owned.push(cur); }
585
+ if (xtcProbability > 0) {
586
+ // Split the step key: subkey 0 → XTC's uniform, subkey 1 → categorical.
587
+ const split = ops.randomSplitNum(key, 2);
588
+ const row0 = split.slice([0, 0], [1, 2]);
589
+ const row1 = split.slice([1, 0], [2, 2]);
590
+ const xtcKey = ops.reshape(row0, [2]);
591
+ key.dispose();
592
+ key = ops.reshape(row1, [2]);
593
+ for (const a of [split, row0, row1]) a.dispose();
594
+ cur = applyXtc(cur, xtcProbability, xtcThreshold, xtcSpecialTokens, xtcKey);
595
+ owned.push(cur);
596
+ xtcKey.dispose();
597
+ }
468
598
  if (topK > 0) { cur = applyTopK(cur, topK); owned.push(cur); }
469
599
  const scaled = ops.mulScalar(cur, 1 / temperature);
470
- const key = stepKey(seed, step);
471
600
  const tok = ops.randomCategorical(scaled, key);
472
601
  scaled.dispose();
473
602
  key.dispose();
@@ -476,18 +605,48 @@ export function makeSampler(opts: SamplerOptions = {}): Sampler {
476
605
  };
477
606
  }
478
607
 
608
+ /** Start of the recent-token window, matching Python's `tokens[-context_size:]`
609
+ * slice semantics exactly (context_size 0 ⇒ `[-0:]` ⇒ the WHOLE history). */
610
+ function windowStart(n: number, contextSize: number): number {
611
+ return contextSize === 0 ? 0 : Math.max(0, n - contextSize);
612
+ }
613
+
479
614
  export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsProcessor[] {
480
615
  const out: LogitsProcessor[] = [];
481
- const { repetitionPenalty, repetitionContextSize = 20 } = opts;
616
+ const {
617
+ logitBias,
618
+ repetitionPenalty, repetitionContextSize = 20,
619
+ presencePenalty, presenceContextSize = 20,
620
+ frequencyPenalty, frequencyContextSize = 20,
621
+ } = opts;
622
+
623
+ // logit_bias first, as in mlx-lm's make_logits_processors. Reference:
624
+ // `logits.at[:, indices].add(values)` — dict keys are unique, so a
625
+ // gather → add → put round-trip is exactly equivalent to the scatter-add.
626
+ const biasIds = logitBias ? Object.keys(logitBias).map(Number) : [];
627
+ if (logitBias && biasIds.length > 0) {
628
+ const biasVals = Float32Array.from(biasIds.map((id) => logitBias[id]!));
629
+ out.push((_tokens, logits) => {
630
+ const K = biasIds.length;
631
+ const idx = ops.fromInt32(biasIds, [1, K]);
632
+ const vals32 = MlxArray.fromFloat32(biasVals, [1, K]);
633
+ const vals = vals32.astype(logits.dtype);
634
+ const selected = ops.takeAlongAxis(logits, idx, -1);
635
+ const biased = ops.add(selected, vals);
636
+ const updated = ops.putAlongAxis(logits, idx, biased, -1);
637
+ for (const a of [idx, vals32, vals, selected, biased]) a.dispose();
638
+ return updated;
639
+ });
640
+ }
482
641
 
483
- if (repetitionPenalty && repetitionPenalty !== 0) {
642
+ if (repetitionPenalty !== undefined && repetitionPenalty !== 0) {
484
643
  if (repetitionPenalty < 0)
485
644
  throw new Error("repetitionPenalty must be non-negative");
486
645
  out.push((tokens, logits) => {
487
646
  if (!tokens) return logits;
488
647
  const n = tokens.shape[0]!;
489
648
  if (n === 0) return logits;
490
- const start = Math.max(0, n - repetitionContextSize);
649
+ const start = windowStart(n, repetitionContextSize);
491
650
  const recent = tokens.slice([start], [n]);
492
651
  const idx = ops.reshape(recent, [1, n - start]);
493
652
  const selected = ops.takeAlongAxis(logits, idx, -1);
@@ -503,6 +662,58 @@ export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsP
503
662
  return updated;
504
663
  });
505
664
  }
665
+
666
+ // make_presence_penalty: `logits[:, tokens] -= penalty` — a plain
667
+ // fancy-index assignment, so duplicate occurrences all write the SAME
668
+ // value (original − penalty): the penalty applies once per distinct token.
669
+ if (presencePenalty !== undefined && presencePenalty !== 0) {
670
+ out.push((tokens, logits) => {
671
+ if (!tokens) return logits;
672
+ const n = tokens.shape[0]!;
673
+ if (n === 0) return logits;
674
+ const start = windowStart(n, presenceContextSize);
675
+ const recent = tokens.slice([start], [n]);
676
+ const idx = ops.reshape(recent, [1, n - start]);
677
+ const selected = ops.takeAlongAxis(logits, idx, -1);
678
+ const pen = ops.scalarLike(presencePenalty, selected);
679
+ const penalized = ops.sub(selected, pen);
680
+ const updated = ops.putAlongAxis(logits, idx, penalized, -1);
681
+ for (const a of [recent, idx, selected, pen, penalized]) a.dispose();
682
+ return updated;
683
+ });
684
+ }
685
+
686
+ // make_frequency_penalty: `logits.at[:, tokens].subtract(penalty)` — a
687
+ // scatter-subtract that ACCUMULATES over duplicates: each token loses
688
+ // penalty × (occurrence count in the window). We compute the count per
689
+ // position ([m,m] equality matrix summed over a row — m ≤ contextSize, tiny)
690
+ // so every duplicate writes the identical final value and a put_along_axis
691
+ // reproduces the scatter-accumulate exactly.
692
+ if (frequencyPenalty !== undefined && frequencyPenalty !== 0) {
693
+ out.push((tokens, logits) => {
694
+ if (!tokens) return logits;
695
+ const n = tokens.shape[0]!;
696
+ if (n === 0) return logits;
697
+ const start = windowStart(n, frequencyContextSize);
698
+ const m = n - start;
699
+ const recent = tokens.slice([start], [n]);
700
+ const col = ops.reshape(recent, [m, 1]);
701
+ const idx = ops.reshape(recent, [1, m]);
702
+ const eq = ops.equal(col, idx); // [m, m]
703
+ const counts = ops.sumAxis(eq, -1, false); // [m] occurrences of tokens[i]
704
+ const countsF = counts.astype(logits.dtype);
705
+ const countsRow = ops.reshape(countsF, [1, m]);
706
+ const selected = ops.takeAlongAxis(logits, idx, -1);
707
+ const pen = ops.scalarLike(frequencyPenalty, selected);
708
+ const penTotal = ops.mul(countsRow, pen); // penalty × count
709
+ const penalized = ops.sub(selected, penTotal);
710
+ const updated = ops.putAlongAxis(logits, idx, penalized, -1);
711
+ for (const a of [recent, col, idx, eq, counts, countsF, countsRow, selected, pen, penTotal, penalized])
712
+ a.dispose();
713
+ return updated;
714
+ });
715
+ }
716
+
506
717
  return out;
507
718
  }
508
719