@seanhogg/builderforce-memory-engine 2026.7.14 → 2026.9.28
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.
- package/README.md +14 -0
- package/dist/bench/adaptation.d.ts +57 -0
- package/dist/bench/adaptation.d.ts.map +1 -0
- package/dist/bench/adaptation.js +167 -0
- package/dist/bench/adaptation.js.map +1 -0
- package/dist/bench/harness.d.ts +2 -0
- package/dist/bench/harness.d.ts.map +1 -1
- package/dist/bench/harness.js +1 -1
- package/dist/bench/harness.js.map +1 -1
- package/dist/bench/index.d.ts +5 -3
- package/dist/bench/index.d.ts.map +1 -1
- package/dist/bench/index.js +4 -2
- package/dist/bench/index.js.map +1 -1
- package/dist/bench/types.d.ts +88 -0
- package/dist/bench/types.d.ts.map +1 -1
- package/dist/evermind/adapt.d.ts +40 -0
- package/dist/evermind/adapt.d.ts.map +1 -0
- package/dist/evermind/adapt.js +25 -0
- package/dist/evermind/adapt.js.map +1 -0
- package/dist/evermind/delta_wire.d.ts +43 -0
- package/dist/evermind/delta_wire.d.ts.map +1 -0
- package/dist/evermind/delta_wire.js +94 -0
- package/dist/evermind/delta_wire.js.map +1 -0
- package/dist/evermind/eval.d.ts +31 -0
- package/dist/evermind/eval.d.ts.map +1 -0
- package/dist/evermind/eval.js +36 -0
- package/dist/evermind/eval.js.map +1 -0
- package/dist/evermind/index.d.ts +16 -0
- package/dist/evermind/index.d.ts.map +1 -0
- package/dist/evermind/index.js +11 -0
- package/dist/evermind/index.js.map +1 -0
- package/dist/evermind/merge.d.ts +21 -0
- package/dist/evermind/merge.d.ts.map +1 -0
- package/dist/evermind/merge.js +79 -0
- package/dist/evermind/merge.js.map +1 -0
- package/dist/{moe/moe_package.d.ts → evermind/package.d.ts} +55 -5
- package/dist/evermind/package.d.ts.map +1 -0
- package/dist/{moe/moe_package.js → evermind/package.js} +118 -19
- package/dist/evermind/package.js.map +1 -0
- package/dist/import/foreign/adapter.d.ts +59 -0
- package/dist/import/foreign/adapter.d.ts.map +1 -0
- package/dist/import/foreign/adapter.js +76 -0
- package/dist/import/foreign/adapter.js.map +1 -0
- package/dist/import/foreign/apply.d.ts +23 -0
- package/dist/import/foreign/apply.d.ts.map +1 -0
- package/dist/import/foreign/apply.js +43 -0
- package/dist/import/foreign/apply.js.map +1 -0
- package/dist/import/foreign/codestral_mamba.d.ts +57 -0
- package/dist/import/foreign/codestral_mamba.d.ts.map +1 -0
- package/dist/import/foreign/codestral_mamba.js +174 -0
- package/dist/import/foreign/codestral_mamba.js.map +1 -0
- package/dist/import/foreign/falcon_mamba.d.ts +51 -0
- package/dist/import/foreign/falcon_mamba.d.ts.map +1 -0
- package/dist/import/foreign/falcon_mamba.js +131 -0
- package/dist/import/foreign/falcon_mamba.js.map +1 -0
- package/dist/import/foreign/index.d.ts +21 -0
- package/dist/import/foreign/index.d.ts.map +1 -0
- package/dist/import/foreign/index.js +18 -0
- package/dist/import/foreign/index.js.map +1 -0
- package/dist/import/foreign/plan.d.ts +122 -0
- package/dist/import/foreign/plan.d.ts.map +1 -0
- package/dist/import/foreign/plan.js +171 -0
- package/dist/import/foreign/plan.js.map +1 -0
- package/dist/import/foreign/registry.d.ts +35 -0
- package/dist/import/foreign/registry.d.ts.map +1 -0
- package/dist/import/foreign/registry.js +89 -0
- package/dist/import/foreign/registry.js.map +1 -0
- package/dist/import/index.d.ts +7 -0
- package/dist/import/index.d.ts.map +1 -1
- package/dist/import/index.js +7 -0
- package/dist/import/index.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -9
- package/dist/index.js.map +1 -1
- package/dist/kernels/selective_scan.d.ts.map +1 -1
- package/dist/kernels/selective_scan.js +15 -9
- package/dist/kernels/selective_scan.js.map +1 -1
- package/dist/kernels/slice.d.ts +26 -0
- package/dist/kernels/slice.d.ts.map +1 -0
- package/dist/kernels/slice.js +52 -0
- package/dist/kernels/slice.js.map +1 -0
- package/dist/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +30 -4
- package/dist/kernels/weight_update.js.map +1 -1
- package/dist/lm/evermind_lm.d.ts +110 -1
- package/dist/lm/evermind_lm.d.ts.map +1 -1
- package/dist/lm/evermind_lm.js +220 -35
- package/dist/lm/evermind_lm.js.map +1 -1
- package/dist/lm/index.d.ts +4 -2
- package/dist/lm/index.d.ts.map +1 -1
- package/dist/lm/index.js +3 -1
- package/dist/lm/index.js.map +1 -1
- package/dist/lm/text_embedder.d.ts +55 -0
- package/dist/lm/text_embedder.d.ts.map +1 -0
- package/dist/lm/text_embedder.js +62 -0
- package/dist/lm/text_embedder.js.map +1 -0
- package/dist/model/attention_block.d.ts.map +1 -1
- package/dist/model/attention_block.js +9 -5
- package/dist/model/attention_block.js.map +1 -1
- package/dist/model/mamba1_block.d.ts.map +1 -1
- package/dist/model/mamba1_block.js +28 -12
- package/dist/model/mamba1_block.js.map +1 -1
- package/dist/model/mamba2_block.d.ts.map +1 -1
- package/dist/model/mamba2_block.js +14 -9
- package/dist/model/mamba2_block.js.map +1 -1
- package/dist/model/mamba3_block.d.ts.map +1 -1
- package/dist/model/mamba3_block.js +14 -9
- package/dist/model/mamba3_block.js.map +1 -1
- package/dist/moe/index.d.ts +0 -2
- package/dist/moe/index.d.ts.map +1 -1
- package/dist/moe/index.js +0 -1
- package/dist/moe/index.js.map +1 -1
- package/dist/optim/adamw.d.ts +32 -0
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +34 -0
- package/dist/optim/adamw.js.map +1 -1
- package/dist/tokenizer/bpe.d.ts +28 -0
- package/dist/tokenizer/bpe.d.ts.map +1 -1
- package/dist/tokenizer/bpe.js +47 -0
- package/dist/tokenizer/bpe.js.map +1 -1
- package/dist/training/cpu_ops.d.ts +44 -0
- package/dist/training/cpu_ops.d.ts.map +1 -0
- package/dist/training/cpu_ops.js +111 -0
- package/dist/training/cpu_ops.js.map +1 -0
- package/dist/training/mamba1_cpu.d.ts +109 -0
- package/dist/training/mamba1_cpu.d.ts.map +1 -0
- package/dist/training/mamba1_cpu.js +316 -0
- package/dist/training/mamba1_cpu.js.map +1 -0
- package/dist/training/model_cpu.d.ts +100 -0
- package/dist/training/model_cpu.d.ts.map +1 -0
- package/dist/training/model_cpu.js +252 -0
- package/dist/training/model_cpu.js.map +1 -0
- package/dist/training/trainer.d.ts +156 -16
- package/dist/training/trainer.d.ts.map +1 -1
- package/dist/training/trainer.js +327 -101
- package/dist/training/trainer.js.map +1 -1
- package/dist/utils/base64.d.ts +12 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +27 -0
- package/dist/utils/base64.js.map +1 -0
- package/package.json +3 -1
- package/src/bench/adaptation.ts +180 -0
- package/src/bench/harness.ts +1 -1
- package/src/bench/index.ts +13 -2
- package/src/bench/types.ts +93 -0
- package/src/evermind/adapt.ts +61 -0
- package/src/evermind/delta_wire.ts +114 -0
- package/src/evermind/eval.ts +62 -0
- package/src/evermind/index.ts +32 -0
- package/src/evermind/merge.ts +106 -0
- package/src/{moe/moe_package.ts → evermind/package.ts} +148 -17
- package/src/import/foreign/adapter.ts +106 -0
- package/src/import/foreign/apply.ts +54 -0
- package/src/import/foreign/codestral_mamba.ts +224 -0
- package/src/import/foreign/falcon_mamba.ts +173 -0
- package/src/import/foreign/index.ts +37 -0
- package/src/import/foreign/plan.ts +294 -0
- package/src/import/foreign/registry.ts +126 -0
- package/src/import/index.ts +30 -0
- package/src/index.ts +94 -13
- package/src/kernels/selective_scan.ts +15 -9
- package/src/kernels/slice.ts +64 -0
- package/src/kernels/weight_update.ts +30 -4
- package/src/lm/evermind_lm.ts +244 -34
- package/src/lm/index.ts +6 -2
- package/src/lm/text_embedder.ts +84 -0
- package/src/model/attention_block.ts +9 -5
- package/src/model/mamba1_block.ts +28 -12
- package/src/model/mamba2_block.ts +14 -9
- package/src/model/mamba3_block.ts +14 -9
- package/src/moe/index.ts +0 -9
- package/src/optim/adamw.ts +60 -0
- package/src/tokenizer/bpe.ts +55 -0
- package/src/training/cpu_ops.ts +127 -0
- package/src/training/mamba1_cpu.ts +421 -0
- package/src/training/model_cpu.ts +360 -0
- package/src/training/trainer.ts +397 -154
- package/src/utils/base64.ts +28 -0
- package/dist/model/mamba_block.d.ts +0 -64
- package/dist/model/mamba_block.d.ts.map +0 -1
- package/dist/model/mamba_block.js +0 -303
- package/dist/model/mamba_block.js.map +0 -1
- package/dist/moe/moe_package.d.ts.map +0 -1
- package/dist/moe/moe_package.js.map +0 -1
- package/src/model/mamba_block.ts +0 -401
package/src/training/trainer.ts
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* trainer.ts – MambaTrainer class
|
|
3
|
+
*
|
|
4
|
+
* REAL backpropagation. Until 2026-08 `_trainStep` handed the SAME `dLogits`
|
|
5
|
+
* buffer to AdamW for every parameter, so no parameter ever received its own
|
|
6
|
+
* gradient: `learn()` was SAFE (WSLA freezes the backbone, the trust region
|
|
7
|
+
* bounds every step, the NaN guard blocks a poisoned weight, and the session
|
|
8
|
+
* rolls back a regression) but INERT. It now runs a genuine backward pass —
|
|
9
|
+
* `cpuModelBackward` in `model_cpu.ts` — and each parameter is stepped with its
|
|
10
|
+
* OWN gradient.
|
|
11
|
+
*
|
|
12
|
+
* Where the maths runs, and why:
|
|
13
|
+
* • the backward pass is the CPU reference (`cpu_ops.ts` + `mamba1_cpu.ts`),
|
|
14
|
+
* a line-for-line mirror of the WGSL forward kernels. It is exact and it is
|
|
15
|
+
* PROVEN — `tests/backprop.test.ts` checks every parameter gradient against
|
|
16
|
+
* finite differences of the real loss, with no GPU involved;
|
|
17
|
+
* • the optimiser is {@link adamwUpdateInPlace}, the CPU mirror of
|
|
18
|
+
* `WEIGHT_UPDATE_WGSL`'s `adamw_update`, keeping the trust region and the
|
|
19
|
+
* NaN guard the safety story depends on;
|
|
20
|
+
* • the model's GPU buffers are written back after every optimiser step, so
|
|
21
|
+
* inference (which IS GPU) always serves the trained weights.
|
|
22
|
+
*
|
|
23
|
+
* The residual is a WGSL backward pass so training itself runs on the GPU. The
|
|
24
|
+
* backward kernels exist (`SELECTIVE_SCAN_BACKWARD_WGSL`, `SSD_BACKWARD_WGSL`,
|
|
25
|
+
* `CONV1D_BACKWARD_WGSL`, `LINEAR_BACKWARD_WGSL`, `ACTIVATIONS_BACKWARD_WGSL`)
|
|
26
|
+
* but wiring them cannot be validated numerically without a WebGPU device, and
|
|
27
|
+
* an unvalidated GPU backward is worse than a slower exact one.
|
|
3
28
|
*/
|
|
4
29
|
|
|
5
30
|
import {
|
|
6
|
-
createUniformBuffer,
|
|
7
|
-
createStorageBuffer,
|
|
8
|
-
createEmptyStorageBuffer,
|
|
9
|
-
createComputePipeline,
|
|
10
|
-
createBindGroup,
|
|
11
|
-
dispatchKernel,
|
|
12
31
|
readBuffer,
|
|
13
|
-
|
|
32
|
+
uploadBuffer,
|
|
14
33
|
} from '../utils/gpu_utils.js';
|
|
15
34
|
|
|
16
|
-
import { crossEntropyLoss
|
|
17
|
-
import {
|
|
35
|
+
import { crossEntropyLoss } from './autograd.js';
|
|
36
|
+
import { adamwUpdateInPlace, type ShardSpec } from '../optim/adamw.js';
|
|
37
|
+
import { LoRAAdapter, type LoRAConfig } from './lora.js';
|
|
38
|
+
import {
|
|
39
|
+
cpuModelBackward,
|
|
40
|
+
cpuWeightsFromNamed,
|
|
41
|
+
namedWeights,
|
|
42
|
+
paramMatrixShape,
|
|
43
|
+
DEFAULT_LORA_TARGETS,
|
|
44
|
+
CPU_GRADIENT_LAYER_TYPES,
|
|
45
|
+
type CpuModelDims,
|
|
46
|
+
type CpuModelWeights,
|
|
47
|
+
} from './model_cpu.js';
|
|
18
48
|
import { HybridMambaModel, MambaModel } from '../model/mamba_model.js';
|
|
19
49
|
import { BPETokenizer } from '../tokenizer/bpe.js';
|
|
20
50
|
import type { LayerParam as BlockParam } from '../model/sequence_layer.js';
|
|
@@ -22,6 +52,8 @@ import type { LayerParam as BlockParam } from '../model/sequence_layer.js';
|
|
|
22
52
|
export interface TrainOptions {
|
|
23
53
|
learningRate?: number;
|
|
24
54
|
epochs?: number;
|
|
55
|
+
/** Sequences trained together per optimiser step. Chunks of equal length are
|
|
56
|
+
* grouped into batches of this size; a ragged tail trains on its own. */
|
|
25
57
|
batchSize?: number;
|
|
26
58
|
seqLen?: number;
|
|
27
59
|
maxGradNorm?: number;
|
|
@@ -33,11 +65,46 @@ export interface TrainOptions {
|
|
|
33
65
|
/** Trust region: max |Δθ| per optimizer step. 0 disables. Defaults to
|
|
34
66
|
* {@link WSLA_MAX_DELTA} in WSLA (write-through) mode, else 0 (full training). */
|
|
35
67
|
maxDelta?: number;
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
68
|
+
/**
|
|
69
|
+
* @deprecated The pre-clip gradient L2 norm is now computed exactly on the way
|
|
70
|
+
* through the optimiser and is ALWAYS reported — it no longer costs a GPU
|
|
71
|
+
* readback, so there is nothing to opt into. Accepted for API compatibility.
|
|
72
|
+
*/
|
|
40
73
|
trackGradNorm?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Train LOW-RANK ADAPTERS over the block projections instead of the weights
|
|
76
|
+
* themselves (see {@link LoRAAdapter}). The base weights are frozen; only A and
|
|
77
|
+
* B train, so an adapt costs a fraction of the optimiser state and the result is
|
|
78
|
+
* a small, mergeable delta rather than a whole checkpoint.
|
|
79
|
+
*
|
|
80
|
+
* `targets` selects which parameters get an adapter, by their unqualified name
|
|
81
|
+
* (`wInProj`, `wXProj`, `wDtProj`, `wOutProj`, `wConv`, `A_log`, `embedding`);
|
|
82
|
+
* omit for {@link DEFAULT_LORA_TARGETS}. A rank larger than a target's smaller
|
|
83
|
+
* dimension is clamped to it rather than throwing — the small projections
|
|
84
|
+
* (`wDtProj` is `dInner x dtRank`) would otherwise reject a sensible global rank.
|
|
85
|
+
*/
|
|
86
|
+
lora?: LoRAConfig & { targets?: readonly string[] };
|
|
87
|
+
/**
|
|
88
|
+
* Batches to accumulate gradients over before one optimiser step. Default 1.
|
|
89
|
+
* The effective batch size is `batchSize * gradientAccumulation` at the memory
|
|
90
|
+
* cost of `batchSize` — the standard way to train at a batch size the host
|
|
91
|
+
* cannot hold at once.
|
|
92
|
+
*/
|
|
93
|
+
gradientAccumulation?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Shard the optimiser state (the Adam moments — 2x the parameter bytes, the
|
|
96
|
+
* dominant cost of a full fine-tune). This instance then OWNS only parameters
|
|
97
|
+
* where `index % count === index`, and allocates moments for those alone. Run
|
|
98
|
+
* `count` trainers over the same model and the union of their steps equals one
|
|
99
|
+
* unsharded step. Same contract as {@link ShardSpec} on {@link AdamW}.
|
|
100
|
+
*/
|
|
101
|
+
shard?: ShardSpec;
|
|
102
|
+
/**
|
|
103
|
+
* Recompute each layer's activations in the backward pass instead of retaining
|
|
104
|
+
* all of them. Gradients are identical; peak memory drops from O(layers)
|
|
105
|
+
* activation caches to one, at the cost of a second forward per layer.
|
|
106
|
+
*/
|
|
107
|
+
activationCheckpointing?: boolean;
|
|
41
108
|
onEpochEnd?: ((epoch: number, loss: number, gradNorm?: number) => void) | null;
|
|
42
109
|
}
|
|
43
110
|
|
|
@@ -51,8 +118,8 @@ export interface TrainOptions {
|
|
|
51
118
|
export const WSLA_MAX_DELTA = 0.05;
|
|
52
119
|
|
|
53
120
|
interface AdamMoments {
|
|
54
|
-
m:
|
|
55
|
-
v:
|
|
121
|
+
m: Float32Array;
|
|
122
|
+
v: Float32Array;
|
|
56
123
|
}
|
|
57
124
|
|
|
58
125
|
interface AdamHyperparams {
|
|
@@ -64,8 +131,15 @@ interface AdamHyperparams {
|
|
|
64
131
|
beta1_t: number;
|
|
65
132
|
beta2_t: number;
|
|
66
133
|
maxDelta: number;
|
|
134
|
+
gradScale: number;
|
|
67
135
|
}
|
|
68
136
|
|
|
137
|
+
/** One training example: a token window and its next-token targets. */
|
|
138
|
+
interface TrainChunk { inputs: number[]; targets: number[] }
|
|
139
|
+
|
|
140
|
+
/** A batch of equal-length chunks trained in one optimiser step. */
|
|
141
|
+
interface TrainBatch { inputs: number[]; targets: number[]; batch: number; seqLen: number }
|
|
142
|
+
|
|
69
143
|
export class MambaTrainer {
|
|
70
144
|
model: HybridMambaModel;
|
|
71
145
|
tokenizer: BPETokenizer | null;
|
|
@@ -74,15 +148,21 @@ export class MambaTrainer {
|
|
|
74
148
|
* Adam moments keyed by parameter NAME (not array index). Name-keying is what
|
|
75
149
|
* lets WSLA toggle safely: a narrow write-through step updates only the
|
|
76
150
|
* `layer{i}.wXProj/bXProj` subset, a full fine-tune updates everything, and
|
|
77
|
-
* both reuse the SAME `m`/`v`
|
|
78
|
-
*
|
|
79
|
-
*
|
|
151
|
+
* both reuse the SAME `m`/`v` per parameter. Index-keyed moments (the old
|
|
152
|
+
* scheme) silently misaligned the moment with the wrong parameter the moment
|
|
153
|
+
* the trainable set changed shape — corrupting the update.
|
|
80
154
|
*/
|
|
81
155
|
private _moments: Map<string, AdamMoments>;
|
|
82
156
|
private _step: number;
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
private
|
|
157
|
+
private readonly _dims: CpuModelDims;
|
|
158
|
+
/** CPU mirror of every model tensor — the gradient engine's working set. */
|
|
159
|
+
private _mirror: CpuModelWeights | null;
|
|
160
|
+
private _mirrorByName: Map<string, Float32Array> | null;
|
|
161
|
+
/** Low-rank adapters by parameter name, when training with `lora`. */
|
|
162
|
+
private _adapters: Map<string, LoRAAdapter>;
|
|
163
|
+
/** Frozen base weights the adapters sit on top of (LoRA only). */
|
|
164
|
+
private _loraBase: Map<string, Float32Array>;
|
|
165
|
+
private _shard: ShardSpec | null;
|
|
86
166
|
|
|
87
167
|
constructor(model: HybridMambaModel | MambaModel, tokenizer: BPETokenizer | null = null) {
|
|
88
168
|
this.model = model;
|
|
@@ -91,25 +171,103 @@ export class MambaTrainer {
|
|
|
91
171
|
|
|
92
172
|
this._moments = new Map();
|
|
93
173
|
this._step = 0;
|
|
174
|
+
this._dims = cpuDimsFor(model);
|
|
175
|
+
this._mirror = null;
|
|
176
|
+
this._mirrorByName = null;
|
|
177
|
+
this._adapters = new Map();
|
|
178
|
+
this._loraBase = new Map();
|
|
179
|
+
this._shard = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The low-rank adapters this trainer is training, by parameter name — empty
|
|
184
|
+
* unless `train({ lora })` was used. Exposed so a caller can serialise just the
|
|
185
|
+
* adapter (a few hundred KB) instead of the whole checkpoint.
|
|
186
|
+
*/
|
|
187
|
+
get adapters(): ReadonlyMap<string, LoRAAdapter> { return this._adapters; }
|
|
188
|
+
|
|
189
|
+
/** Trainable scalar count for the current mode — the LoRA saving, measured. */
|
|
190
|
+
trainableParamCount(): number {
|
|
191
|
+
const params = this.model.getTrainableParams();
|
|
192
|
+
if (this._adapters.size === 0) return params.reduce((n, p) => n + p.numel, 0);
|
|
193
|
+
let n = 0;
|
|
194
|
+
for (const p of params) {
|
|
195
|
+
const a = this._adapters.get(p.name);
|
|
196
|
+
if (a) n += a.numParams();
|
|
197
|
+
}
|
|
198
|
+
return n;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Whether this trainer owns parameter `index` under the active shard. */
|
|
202
|
+
private _owns(index: number): boolean {
|
|
203
|
+
return this._shard === null || index % this._shard.count === this._shard.index;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build (once) an adapter for every LoRA-target parameter, snapshot the frozen
|
|
208
|
+
* base, and return the set. Rank is clamped per parameter to its smaller
|
|
209
|
+
* dimension so one global rank works across projections of very different shape.
|
|
210
|
+
*/
|
|
211
|
+
private _installAdapters(cfg: LoRAConfig & { targets?: readonly string[] }): void {
|
|
212
|
+
if (this._adapters.size > 0) return; // already installed; keep training them
|
|
213
|
+
const targets = new Set<string>(cfg.targets ?? DEFAULT_LORA_TARGETS);
|
|
214
|
+
const weights = this._mirrorByName!;
|
|
215
|
+
for (const p of this.model.getTrainableParams()) {
|
|
216
|
+
const local = p.name.includes('.') ? p.name.slice(p.name.indexOf('.') + 1) : p.name;
|
|
217
|
+
if (!targets.has(local)) continue;
|
|
218
|
+
const shape = paramMatrixShape(p.name, this._dims);
|
|
219
|
+
if (!shape) continue; // a vector parameter — nothing for a low-rank adapter to do
|
|
220
|
+
const [rows, cols] = shape;
|
|
221
|
+
const rank = Math.max(1, Math.min(cfg.rank ?? 8, rows, cols));
|
|
222
|
+
this._adapters.set(p.name, new LoRAAdapter(rows, cols, { ...cfg, rank }));
|
|
223
|
+
this._loraBase.set(p.name, Float32Array.from(weights.get(p.name)!));
|
|
224
|
+
}
|
|
225
|
+
if (this._adapters.size === 0) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`MambaTrainer: lora was requested but no trainable parameter matched targets ` +
|
|
228
|
+
`[${[...targets].join(', ')}]. Valid targets: wInProj, wXProj, wDtProj, wOutProj, wConv, A_log, embedding.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
94
232
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
233
|
+
/** Write `base + adapter.delta()` into the mirror, so the forward sees the adapted weights. */
|
|
234
|
+
private _materialiseAdapters(): void {
|
|
235
|
+
const weights = this._mirrorByName!;
|
|
236
|
+
for (const [name, adapter] of this._adapters) {
|
|
237
|
+
const w = weights.get(name)!;
|
|
238
|
+
const base = this._loraBase.get(name)!;
|
|
239
|
+
const delta = adapter.delta();
|
|
240
|
+
for (let i = 0; i < w.length; i++) w[i] = base[i]! + delta[i]!;
|
|
241
|
+
}
|
|
98
242
|
}
|
|
99
243
|
|
|
100
244
|
/** Get-or-create the Adam first/second moments for a parameter, by name. */
|
|
101
245
|
private _momentFor(p: BlockParam): AdamMoments {
|
|
102
246
|
let mom = this._moments.get(p.name);
|
|
103
247
|
if (!mom) {
|
|
104
|
-
mom = {
|
|
105
|
-
m: createEmptyStorageBuffer(this.device, p.numel * 4, false),
|
|
106
|
-
v: createEmptyStorageBuffer(this.device, p.numel * 4, false),
|
|
107
|
-
};
|
|
248
|
+
mom = { m: new Float32Array(p.numel), v: new Float32Array(p.numel) };
|
|
108
249
|
this._moments.set(p.name, mom);
|
|
109
250
|
}
|
|
110
251
|
return mom;
|
|
111
252
|
}
|
|
112
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Pull every model tensor off the GPU into the CPU mirror. Done once per
|
|
256
|
+
* `train()` call (not per step) so an externally loaded checkpoint or a
|
|
257
|
+
* previous session's weights are picked up, after which the mirror is
|
|
258
|
+
* authoritative and each step writes the updated tensors back.
|
|
259
|
+
*/
|
|
260
|
+
private async _syncMirrorFromGpu(): Promise<void> {
|
|
261
|
+
const named = new Map<string, Float32Array>();
|
|
262
|
+
for (const p of this.model.parameters()) {
|
|
263
|
+
named.set(p.name, await readBuffer(this.device, p.buf, p.numel * 4));
|
|
264
|
+
}
|
|
265
|
+
named.set('lm_head_bias',
|
|
266
|
+
await readBuffer(this.device, this.model.gpuLMHeadBias, this._dims.vocabSize * 4));
|
|
267
|
+
this._mirror = cpuWeightsFromNamed(named, this._dims);
|
|
268
|
+
this._mirrorByName = namedWeights(this._mirror);
|
|
269
|
+
}
|
|
270
|
+
|
|
113
271
|
async train(input: string | number[], opts: TrainOptions = {}): Promise<number[]> {
|
|
114
272
|
const {
|
|
115
273
|
learningRate = 1e-4,
|
|
@@ -122,9 +280,13 @@ export class MambaTrainer {
|
|
|
122
280
|
beta2 = 0.999,
|
|
123
281
|
eps = 1e-8,
|
|
124
282
|
wsla = false,
|
|
125
|
-
trackGradNorm = false,
|
|
126
283
|
onEpochEnd = null,
|
|
127
284
|
} = opts;
|
|
285
|
+
const accumulation = Math.max(1, Math.floor(opts.gradientAccumulation ?? 1));
|
|
286
|
+
this._shard = opts.shard ?? null;
|
|
287
|
+
if (this._shard && (this._shard.count <= 0 || this._shard.index < 0 || this._shard.index >= this._shard.count)) {
|
|
288
|
+
throw new Error(`MambaTrainer: invalid shard ${this._shard.index}/${this._shard.count}`);
|
|
289
|
+
}
|
|
128
290
|
// Trust region defaults ON for write-through (WSLA) adapts, OFF otherwise.
|
|
129
291
|
const maxDelta = opts.maxDelta ?? (wsla ? WSLA_MAX_DELTA : 0);
|
|
130
292
|
|
|
@@ -151,10 +313,12 @@ export class MambaTrainer {
|
|
|
151
313
|
if (chunks.length === 0) {
|
|
152
314
|
throw new Error('Input is too short to form any training chunk.');
|
|
153
315
|
}
|
|
316
|
+
const batches = buildBatches(chunks, Math.max(1, Math.floor(batchSize)));
|
|
154
317
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
318
|
+
// The mirror is the gradient engine's working set; refresh it from the GPU
|
|
319
|
+
// once per train() so a freshly loaded checkpoint is trained, not stale weights.
|
|
320
|
+
await this._syncMirrorFromGpu();
|
|
321
|
+
if (opts.lora) this._installAdapters(opts.lora);
|
|
158
322
|
|
|
159
323
|
const epochLosses: number[] = [];
|
|
160
324
|
|
|
@@ -163,158 +327,187 @@ export class MambaTrainer {
|
|
|
163
327
|
let epochGradNorm = 0;
|
|
164
328
|
let numSteps = 0;
|
|
165
329
|
|
|
166
|
-
|
|
330
|
+
// Gradient accumulation: `accumulation` batches contribute to one
|
|
331
|
+
// optimiser step, so the effective batch is batchSize x accumulation at
|
|
332
|
+
// the memory of one batch.
|
|
333
|
+
for (let i = 0; i < batches.length; i += accumulation) {
|
|
334
|
+
const group = batches.slice(i, i + accumulation);
|
|
167
335
|
const { loss, gradNorm } = await this._trainStep(
|
|
168
|
-
|
|
169
|
-
{ learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, wsla, maxDelta,
|
|
336
|
+
group,
|
|
337
|
+
{ learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, wsla, maxDelta,
|
|
338
|
+
...(opts.activationCheckpointing ? { activationCheckpointing: true } : {}) },
|
|
170
339
|
);
|
|
171
340
|
epochLoss += loss;
|
|
172
|
-
|
|
341
|
+
epochGradNorm += gradNorm;
|
|
173
342
|
numSteps++;
|
|
174
343
|
}
|
|
175
344
|
|
|
176
345
|
const avgLoss = epochLoss / numSteps;
|
|
177
346
|
epochLosses.push(avgLoss);
|
|
178
|
-
|
|
179
|
-
// Mean pre-clip grad norm this epoch (only measured when trackGradNorm is on).
|
|
180
|
-
const avgGradNorm = trackGradNorm ? epochGradNorm / numSteps : undefined;
|
|
181
|
-
if (onEpochEnd) onEpochEnd(epoch + 1, avgLoss, avgGradNorm);
|
|
347
|
+
if (onEpochEnd) onEpochEnd(epoch + 1, avgLoss, epochGradNorm / numSteps);
|
|
182
348
|
}
|
|
183
349
|
|
|
184
350
|
if (wsla) this.model.setWSLAMode(false);
|
|
185
351
|
return epochLosses;
|
|
186
352
|
}
|
|
187
353
|
|
|
354
|
+
/**
|
|
355
|
+
* One optimiser step over one batch: real forward + backward, a global
|
|
356
|
+
* gradient-norm clip over the trainable set, then AdamW per parameter with
|
|
357
|
+
* its OWN gradient, written straight back to the GPU buffer.
|
|
358
|
+
*/
|
|
188
359
|
private async _trainStep(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
): Promise<{ loss: number; gradNorm: number | null }> {
|
|
194
|
-
const { learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, maxDelta, trackGradNorm } = hyperparams;
|
|
360
|
+
group: TrainBatch[],
|
|
361
|
+
hyperparams: TrainOptions & { learningRate: number; maxGradNorm: number; weightDecay: number; beta1: number; beta2: number; eps: number; maxDelta: number }
|
|
362
|
+
): Promise<{ loss: number; gradNorm: number }> {
|
|
363
|
+
const { learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, maxDelta } = hyperparams;
|
|
195
364
|
|
|
196
|
-
this.
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const { logits, gpuLogits } = await this.model.forward(
|
|
201
|
-
new Uint32Array(inputs), batch, seqLen
|
|
202
|
-
);
|
|
365
|
+
if (!this._mirror || !this._mirrorByName) await this._syncMirrorFromGpu();
|
|
366
|
+
const mirror = this._mirror!;
|
|
367
|
+
const weights = this._mirrorByName!;
|
|
203
368
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
369
|
+
this._step++;
|
|
370
|
+
// Under LoRA the forward must see base + Delta, not the stale merge from the
|
|
371
|
+
// previous step.
|
|
372
|
+
if (this._adapters.size > 0) this._materialiseAdapters();
|
|
373
|
+
|
|
374
|
+
const bwOpts = hyperparams.activationCheckpointing ? { activationCheckpointing: true } : {};
|
|
375
|
+
let loss = 0;
|
|
376
|
+
let grads = new Map<string, Float32Array>();
|
|
377
|
+
for (const b of group) {
|
|
378
|
+
const step = cpuModelBackward(b.inputs, b.targets, mirror, this._dims, b.batch, b.seqLen, bwOpts);
|
|
379
|
+
loss += step.loss / group.length;
|
|
380
|
+
if (grads.size === 0) {
|
|
381
|
+
// First micro-batch owns the accumulator; scale it as we go so the
|
|
382
|
+
// accumulated gradient is the MEAN over the group, matching a single
|
|
383
|
+
// pass at the larger batch size.
|
|
384
|
+
for (const v of step.grads.values()) for (let i = 0; i < v.length; i++) v[i] = v[i]! / group.length;
|
|
385
|
+
grads = step.grads;
|
|
386
|
+
} else {
|
|
387
|
+
for (const [k, v] of step.grads) {
|
|
388
|
+
const acc = grads.get(k)!;
|
|
389
|
+
for (let i = 0; i < v.length; i++) acc[i] = acc[i]! + v[i]! / group.length;
|
|
390
|
+
}
|
|
215
391
|
}
|
|
216
392
|
}
|
|
217
|
-
const loss = totalLoss / seqLen;
|
|
218
|
-
|
|
219
|
-
const dLogitsBuf = createStorageBuffer(this.device, dLogits, false);
|
|
220
|
-
|
|
221
|
-
const gradNorm = await this._clipGradients(dLogitsBuf, dLogits.length, maxGradNorm, !!trackGradNorm);
|
|
222
393
|
|
|
223
394
|
// Only the trainable set is updated. Under WSLA (write-through) that is
|
|
224
395
|
// the narrow per-layer subset and the backbone (incl. every A_log) stays
|
|
225
396
|
// frozen — the guarantee that repeated adapts can't destabilise the SSM.
|
|
226
|
-
const params
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
397
|
+
const params = this.model.getTrainableParams();
|
|
398
|
+
|
|
399
|
+
// Under LoRA the base weights are frozen: project each base gradient onto
|
|
400
|
+
// its adapter, and the adapter's A/B are what the optimiser steps.
|
|
401
|
+
if (this._adapters.size > 0) {
|
|
402
|
+
for (const [name, adapter] of this._adapters) {
|
|
403
|
+
const g = grads.get(name);
|
|
404
|
+
if (!g) continue;
|
|
405
|
+
adapter.zeroGrad();
|
|
406
|
+
adapter.accumulateGradient(g);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
234
409
|
|
|
235
|
-
|
|
236
|
-
|
|
410
|
+
// Global pre-clip L2 norm ACROSS the whole trainable set. The old GPU
|
|
411
|
+
// reduce clipped one buffer at a time and raced on `norm_sq[0]` across
|
|
412
|
+
// workgroups; this is the true global norm and needs no readback.
|
|
413
|
+
let sumSq = 0;
|
|
414
|
+
for (const p of params) {
|
|
415
|
+
const g = grads.get(p.name);
|
|
416
|
+
if (!g) continue;
|
|
417
|
+
for (let i = 0; i < g.length; i++) sumSq += g[i]! * g[i]!;
|
|
418
|
+
}
|
|
419
|
+
const gradNorm = Math.sqrt(sumSq);
|
|
420
|
+
const gradScale = maxGradNorm > 0 && gradNorm > maxGradNorm ? maxGradNorm / gradNorm : 1;
|
|
421
|
+
|
|
422
|
+
this._adamwStep(params, grads, weights, {
|
|
423
|
+
learningRate, weightDecay, beta1, beta2, eps,
|
|
424
|
+
beta1_t: Math.pow(beta1, this._step),
|
|
425
|
+
beta2_t: Math.pow(beta2, this._step),
|
|
426
|
+
maxDelta, gradScale,
|
|
427
|
+
});
|
|
237
428
|
|
|
238
429
|
return { loss, gradNorm };
|
|
239
430
|
}
|
|
240
431
|
|
|
241
|
-
|
|
432
|
+
/**
|
|
433
|
+
* AdamW over the trainable set. Each parameter is paired with its OWN
|
|
434
|
+
* gradient by NAME — the bug this replaces passed one shared `dLogits`
|
|
435
|
+
* buffer to every parameter — and the updated tensor is uploaded to the GPU
|
|
436
|
+
* so inference serves it.
|
|
437
|
+
*/
|
|
438
|
+
private _adamwStep(
|
|
242
439
|
params: BlockParam[],
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
440
|
+
grads: Map<string, Float32Array>,
|
|
441
|
+
weights: Map<string, Float32Array>,
|
|
442
|
+
hp: AdamHyperparams,
|
|
443
|
+
): void {
|
|
444
|
+
const { learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta, gradScale } = hp;
|
|
445
|
+
const step = { lr: learningRate, beta1, beta2, eps, weightDecay, beta1_t, beta2_t, maxDelta, gradScale };
|
|
446
|
+
|
|
447
|
+
for (let idx = 0; idx < params.length; idx++) {
|
|
448
|
+
const p = params[idx]!;
|
|
449
|
+
if (!this._owns(idx)) continue; // another shard holds this one's moments
|
|
450
|
+
|
|
451
|
+
// LoRA: step A and B, then re-merge base + Delta into the mirror so the
|
|
452
|
+
// GPU buffer (what inference reads) always holds the adapted weights.
|
|
453
|
+
const adapter = this._adapters.get(p.name);
|
|
454
|
+
if (adapter) {
|
|
455
|
+
const w = weights.get(p.name)!;
|
|
456
|
+
const base = this._loraBase.get(p.name)!;
|
|
457
|
+
const [aP, bP] = [adapter.A, adapter.B];
|
|
458
|
+
const [gA, gB] = [adapter.gradients()[1]!.data, adapter.gradients()[0]!.data];
|
|
459
|
+
const momB = this._momentNamed(`${p.name}::loraB`, bP.length);
|
|
460
|
+
const momA = this._momentNamed(`${p.name}::loraA`, aP.length);
|
|
461
|
+
adamwUpdateInPlace(bP, gB, momB.m, momB.v, step);
|
|
462
|
+
adamwUpdateInPlace(aP, gA, momA.m, momA.v, step);
|
|
463
|
+
const delta = adapter.delta();
|
|
464
|
+
for (let i = 0; i < w.length; i++) w[i] = base[i]! + delta[i]!;
|
|
465
|
+
uploadBuffer(this.device, p.buf, w);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
251
468
|
|
|
252
|
-
|
|
469
|
+
const g = grads.get(p.name);
|
|
470
|
+
const w = weights.get(p.name);
|
|
471
|
+
if (!g || !w) continue;
|
|
472
|
+
if (g.length !== p.numel || w.length !== p.numel) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`MambaTrainer: parameter "${p.name}" is ${p.numel} elements but the ` +
|
|
475
|
+
`gradient engine produced ${g.length} — model and CPU mirror disagree.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
253
478
|
|
|
254
479
|
const mom = this._momentFor(p);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
));
|
|
258
|
-
|
|
259
|
-
const bg = createBindGroup(this.device, this._adamwPipeline, [
|
|
260
|
-
paramsBuf,
|
|
261
|
-
p.buf,
|
|
262
|
-
gradBuf,
|
|
263
|
-
mom.m,
|
|
264
|
-
mom.v,
|
|
265
|
-
]);
|
|
266
|
-
|
|
267
|
-
dispatchKernel(this.device, this._adamwPipeline, bg,
|
|
268
|
-
[cdiv(p.numel, 256), 1, 1]);
|
|
269
|
-
|
|
270
|
-
paramsBuf.destroy();
|
|
480
|
+
adamwUpdateInPlace(w, g, mom.m, mom.v, step);
|
|
481
|
+
uploadBuffer(this.device, p.buf, w);
|
|
271
482
|
}
|
|
272
483
|
}
|
|
273
484
|
|
|
274
|
-
/**
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
*/
|
|
281
|
-
private async _clipGradients(gradBuf: GPUBuffer, numel: number, maxNorm: number, trackNorm = false): Promise<number | null> {
|
|
282
|
-
const normSqBuf = createEmptyStorageBuffer(this.device, 4, true);
|
|
283
|
-
this.device.queue.writeBuffer(normSqBuf, 0, new Float32Array([0.0]));
|
|
284
|
-
|
|
285
|
-
const clipParams = new ArrayBuffer(8);
|
|
286
|
-
new Uint32Array(clipParams, 0, 1).set([numel]);
|
|
287
|
-
new Float32Array(clipParams, 4, 1).set([maxNorm * maxNorm]);
|
|
288
|
-
const pBuf = createUniformBuffer(this.device, clipParams);
|
|
289
|
-
|
|
290
|
-
const bg1 = createBindGroup(this.device, this._clipReducePipeline,
|
|
291
|
-
[pBuf, gradBuf, normSqBuf]);
|
|
292
|
-
dispatchKernel(this.device, this._clipReducePipeline, bg1,
|
|
293
|
-
[cdiv(numel, 256), 1, 1]);
|
|
294
|
-
|
|
295
|
-
// Read Σg² AFTER the reduce (the scale kernel only reads it, never overwrites),
|
|
296
|
-
// so the returned norm is the pre-clip magnitude — the instability signal.
|
|
297
|
-
let gradNorm: number | null = null;
|
|
298
|
-
if (trackNorm) {
|
|
299
|
-
const out = await readBuffer(this.device, normSqBuf, 4);
|
|
300
|
-
gradNorm = Math.sqrt(Math.max(0, out[0] ?? 0));
|
|
485
|
+
/** Get-or-create moments for a synthetic parameter (a LoRA factor), by name. */
|
|
486
|
+
private _momentNamed(name: string, numel: number): AdamMoments {
|
|
487
|
+
let mom = this._moments.get(name);
|
|
488
|
+
if (!mom) {
|
|
489
|
+
mom = { m: new Float32Array(numel), v: new Float32Array(numel) };
|
|
490
|
+
this._moments.set(name, mom);
|
|
301
491
|
}
|
|
492
|
+
return mom;
|
|
493
|
+
}
|
|
302
494
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
495
|
+
/**
|
|
496
|
+
* Bytes of optimiser state (Adam m + v) this trainer holds. Under a shard that
|
|
497
|
+
* is 1/count of the full state; under LoRA it is the adapters' state, not the
|
|
498
|
+
* weights'. The measurable point of both features.
|
|
499
|
+
*/
|
|
500
|
+
optimizerStateBytes(): number {
|
|
501
|
+
let n = 0;
|
|
502
|
+
for (const mom of this._moments.values()) n += mom.m.byteLength + mom.v.byteLength;
|
|
503
|
+
return n;
|
|
311
504
|
}
|
|
312
505
|
|
|
313
506
|
async evaluate(input: string | number[]): Promise<number> {
|
|
314
507
|
let tokenIds: number[];
|
|
315
508
|
if (typeof input === 'string') {
|
|
316
509
|
if (!this.tokenizer) throw new Error('Tokenizer required for string input.');
|
|
317
|
-
tokenIds = this.tokenizer.encode(input);
|
|
510
|
+
tokenIds = Array.from(this.tokenizer.encode(input));
|
|
318
511
|
} else {
|
|
319
512
|
tokenIds = Array.from(input);
|
|
320
513
|
}
|
|
@@ -340,8 +533,44 @@ export class MambaTrainer {
|
|
|
340
533
|
}
|
|
341
534
|
}
|
|
342
535
|
|
|
343
|
-
|
|
344
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Derive the gradient engine's dimensions from a model, rejecting layer types
|
|
538
|
+
* the CPU reference cannot differentiate.
|
|
539
|
+
*
|
|
540
|
+
* A hybrid schedule (`jamba`/`zamba`, or any explicit schedule containing
|
|
541
|
+
* `mamba2`/`mamba3`/`attention`) fails LOUDLY here rather than silently training
|
|
542
|
+
* on gradients that do not exist — which is exactly what the old trainer did.
|
|
543
|
+
*/
|
|
544
|
+
export function cpuDimsFor(model: HybridMambaModel): CpuModelDims {
|
|
545
|
+
const unsupported = model.layerSpecs
|
|
546
|
+
.map((s, i) => ({ i, type: s.type }))
|
|
547
|
+
.filter(({ type }) => !(CPU_GRADIENT_LAYER_TYPES as readonly string[]).includes(type));
|
|
548
|
+
if (unsupported.length > 0) {
|
|
549
|
+
const detail = unsupported.map(({ i, type }) => `layer${i}=${type}`).join(', ');
|
|
550
|
+
throw new Error(
|
|
551
|
+
`MambaTrainer: the gradient engine differentiates ${CPU_GRADIENT_LAYER_TYPES.join('/')} ` +
|
|
552
|
+
`layers only, but this model has ${detail}. Build the model with the default ` +
|
|
553
|
+
`mamba1 schedule to train it, or extend the CPU reference to cover those blocks.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const first = model.layers[0] as unknown as { dInner?: number; dtRank?: number } | undefined;
|
|
558
|
+
const dInner = first?.dInner ?? model.config.expand * model.config.dModel;
|
|
559
|
+
const dtRank = first?.dtRank ?? Math.ceil(model.config.dModel / 16);
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
vocabSize: model.config.vocabSize,
|
|
563
|
+
dModel : model.config.dModel,
|
|
564
|
+
dState : model.config.dState,
|
|
565
|
+
dConv : model.config.dConv,
|
|
566
|
+
dInner,
|
|
567
|
+
dtRank,
|
|
568
|
+
numLayers: model.layers.length,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function buildChunks(ids: number[], seqLen: number): TrainChunk[] {
|
|
573
|
+
const chunks: TrainChunk[] = [];
|
|
345
574
|
for (let start = 0; start + seqLen < ids.length; start += seqLen) {
|
|
346
575
|
chunks.push({
|
|
347
576
|
inputs : ids.slice(start, start + seqLen),
|
|
@@ -359,15 +588,29 @@ function buildChunks(ids: number[], seqLen: number): Array<{inputs: number[], ta
|
|
|
359
588
|
return chunks;
|
|
360
589
|
}
|
|
361
590
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Group equal-length chunks into batches of at most `batchSize`.
|
|
593
|
+
*
|
|
594
|
+
* `batchSize` used to be accepted and ignored — every chunk trained alone. The
|
|
595
|
+
* batch dimension the model and the gradient engine both take is real, so it is
|
|
596
|
+
* honoured here; a ragged tail chunk simply trains as its own batch of one.
|
|
597
|
+
*/
|
|
598
|
+
export function buildBatches(chunks: TrainChunk[], batchSize: number): TrainBatch[] {
|
|
599
|
+
const batches: TrainBatch[] = [];
|
|
600
|
+
let i = 0;
|
|
601
|
+
while (i < chunks.length) {
|
|
602
|
+
const seqLen = chunks[i]!.inputs.length;
|
|
603
|
+
const group: TrainChunk[] = [];
|
|
604
|
+
while (i < chunks.length && group.length < batchSize && chunks[i]!.inputs.length === seqLen) {
|
|
605
|
+
group.push(chunks[i]!);
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
batches.push({
|
|
609
|
+
inputs : group.flatMap((c) => c.inputs),
|
|
610
|
+
targets: group.flatMap((c) => c.targets),
|
|
611
|
+
batch : group.length,
|
|
612
|
+
seqLen,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return batches;
|
|
373
616
|
}
|