mlx-bun 0.0.11 → 0.0.13
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 +63 -27
- package/THIRD_PARTY_LICENSES.md +9 -0
- package/docs/design/colibri-glm52-port.md +1486 -0
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/planning/release-notes-v0.0.13.md +77 -0
- package/docs/reference/cli.md +28 -2
- package/docs/reference/distribution.md +25 -1
- package/docs/reference/embedding.md +5 -2
- package/docs/reference/features-matrix.md +5 -2
- package/docs/reference/library-api.md +41 -13
- package/docs/reference/models.md +49 -2
- package/docs/reference/server-api.md +113 -20
- package/docs/reference/server-config.md +21 -14
- package/docs/reference/training.md +10 -1
- package/package.json +1 -1
- package/src/anthropic.ts +54 -44
- package/src/chat-template.ts +189 -11
- package/src/cli.ts +127 -8
- package/src/config.ts +81 -26
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +103 -12
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/runner.ts +14 -5
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/generate.ts +114 -17
- package/src/hf-push.ts +1 -1
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +147 -8
- package/src/lora.ts +77 -14
- package/src/media-fetch.ts +198 -56
- package/src/mlx/array.ts +43 -0
- package/src/mlx/autograd.ts +51 -14
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/model/factory.ts +96 -5
- package/src/model/gemma4-base.ts +32 -0
- package/src/model/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- package/src/model/support.ts +8 -1
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +95 -16
- package/src/serve/generation-gateway.ts +41 -16
- package/src/server.ts +471 -93
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/serve-loop.ts +99 -18
- package/src/spec/source.ts +31 -6
- package/src/ssd-cache.ts +6 -2
- package/src/tool-call.ts +30 -2
- package/src/train/job.ts +0 -10
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +59 -26
- package/src/web/app.html +13 -2
- package/src/web/app.js +30 -3
- package/src/web/src/status.ts +51 -3
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
package/src/config.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// Parses HF config.json (Gemma 4 unified layout: text_config nested),
|
|
3
3
|
// the OptiQ per-layer quantization map, and kv_config.json when present.
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
parseGlm52Config,
|
|
7
|
+
type Glm52Config,
|
|
8
|
+
} from "./model/glm52-config";
|
|
9
|
+
|
|
5
10
|
export interface RopeParams {
|
|
6
11
|
ropeTheta: number;
|
|
7
12
|
ropeType: string;
|
|
@@ -223,6 +228,18 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
223
228
|
const raw = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
|
|
224
229
|
const modelType = raw.model_type;
|
|
225
230
|
const isDiffusion = modelType === "diffusion_gemma";
|
|
231
|
+
const isGlm52 = modelType === "glm_moe_dsa";
|
|
232
|
+
// The converted Colibri artifact keeps stop metadata split across the two
|
|
233
|
+
// HF files. Preserve the old config-only behavior for every other family;
|
|
234
|
+
// GLM-5.2 explicitly unions both lists through its dedicated parser.
|
|
235
|
+
let glm52: Glm52Config | null = null;
|
|
236
|
+
if (isGlm52) {
|
|
237
|
+
const generationFile = Bun.file(`${modelDir}/generation_config.json`);
|
|
238
|
+
const generation = await generationFile.exists()
|
|
239
|
+
? await generationFile.json() as Record<string, any>
|
|
240
|
+
: null;
|
|
241
|
+
glm52 = parseGlm52Config(modelDir, raw, generation);
|
|
242
|
+
}
|
|
226
243
|
// Gemma 4 unified nests the LM config; plain text models keep it at top level.
|
|
227
244
|
// DiffusionGemma keeps it flat too, but omits all dims — backfill from defaults.
|
|
228
245
|
const baseT = (raw.text_config ?? raw) as Record<string, any>;
|
|
@@ -236,34 +253,56 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
236
253
|
// partial_rotary_factor}), not the gemma per-attention-type map — and
|
|
237
254
|
// type "default" means plain partial nn.RoPE (mrope_section ignored for text).
|
|
238
255
|
const qwenRope = (t.rope_parameters ?? {}) as Record<string, any>;
|
|
256
|
+
// Qwen3.8 adds output_gate_type ("swish"), which every implementation
|
|
257
|
+
// (transformers ground truth, mlx-lm pinned + main) currently ignores —
|
|
258
|
+
// the attention output gate is hardcoded o_proj(out·σ(gate)). Only accept
|
|
259
|
+
// values verified to mean that; an unknown value must fail at load, not
|
|
260
|
+
// silently mis-gate a future checkpoint where the field starts mattering.
|
|
261
|
+
if (isQwen35 && t.output_gate_type != null && t.output_gate_type !== "swish")
|
|
262
|
+
throw new Error(
|
|
263
|
+
`qwen3_5: unverified output_gate_type "${t.output_gate_type}" (only "swish" ` +
|
|
264
|
+
`is verified to mean the standard sigmoid output gate)`,
|
|
265
|
+
);
|
|
239
266
|
|
|
240
267
|
const text: TextConfig = {
|
|
241
|
-
hiddenSize: t.hidden_size,
|
|
242
|
-
numHiddenLayers: t.num_hidden_layers,
|
|
243
|
-
numAttentionHeads: t.num_attention_heads,
|
|
244
|
-
numKeyValueHeads: t.num_key_value_heads,
|
|
245
|
-
headDim: t.head_dim ?? Math.floor(t.hidden_size / t.num_attention_heads),
|
|
246
|
-
numGlobalKeyValueHeads:
|
|
247
|
-
|
|
268
|
+
hiddenSize: glm52?.hiddenSize ?? t.hidden_size,
|
|
269
|
+
numHiddenLayers: glm52?.numHiddenLayers ?? t.num_hidden_layers,
|
|
270
|
+
numAttentionHeads: glm52?.numAttentionHeads ?? t.num_attention_heads,
|
|
271
|
+
numKeyValueHeads: glm52?.numKeyValueHeads ?? t.num_key_value_heads,
|
|
272
|
+
headDim: glm52?.qkHeadDim ?? t.head_dim ?? Math.floor(t.hidden_size / t.num_attention_heads),
|
|
273
|
+
numGlobalKeyValueHeads:
|
|
274
|
+
glm52?.numKeyValueHeads ?? t.num_global_key_value_heads ?? t.num_key_value_heads,
|
|
275
|
+
globalHeadDim:
|
|
276
|
+
glm52?.qkHeadDim ??
|
|
277
|
+
t.global_head_dim ??
|
|
278
|
+
t.head_dim ??
|
|
279
|
+
Math.floor(t.hidden_size / t.num_attention_heads),
|
|
248
280
|
attentionKEqV: t.attention_k_eq_v ?? false,
|
|
249
|
-
intermediateSize: t.intermediate_size,
|
|
281
|
+
intermediateSize: glm52?.intermediateSize ?? t.intermediate_size,
|
|
250
282
|
hiddenActivation: t.hidden_activation ?? t.hidden_act ?? "gelu_pytorch_tanh",
|
|
251
|
-
rmsNormEps: t.rms_norm_eps,
|
|
252
|
-
vocabSize: t.vocab_size,
|
|
253
|
-
maxPositionEmbeddings:
|
|
283
|
+
rmsNormEps: glm52?.rmsNormEps ?? t.rms_norm_eps,
|
|
284
|
+
vocabSize: glm52?.vocabSize ?? t.vocab_size,
|
|
285
|
+
maxPositionEmbeddings:
|
|
286
|
+
glm52?.maxPositionEmbeddings ?? t.max_position_embeddings,
|
|
254
287
|
slidingWindow: t.sliding_window ?? 0,
|
|
255
|
-
layerTypes:
|
|
288
|
+
layerTypes: glm52
|
|
289
|
+
? Array(glm52.numHiddenLayers).fill("full_attention")
|
|
290
|
+
: t.layer_types ?? (isLlama ? Array(t.num_hidden_layers).fill("full_attention") : []),
|
|
256
291
|
hiddenSizePerLayerInput: t.hidden_size_per_layer_input ?? 0,
|
|
257
|
-
vocabSizePerLayerInput:
|
|
292
|
+
vocabSizePerLayerInput:
|
|
293
|
+
t.vocab_size_per_layer_input ?? glm52?.vocabSize ?? t.vocab_size,
|
|
258
294
|
numKvSharedLayers: t.num_kv_shared_layers ?? 0,
|
|
259
|
-
enableMoeBlock: t.enable_moe_block ?? false,
|
|
260
|
-
numExperts: t.num_experts ?? 0,
|
|
295
|
+
enableMoeBlock: glm52 ? true : t.enable_moe_block ?? false,
|
|
296
|
+
numExperts: glm52?.numRoutedExperts ?? t.num_experts ?? 0,
|
|
261
297
|
// gemma4 uses `top_k_experts`; qwen3_moe uses `num_experts_per_tok`.
|
|
262
|
-
topKExperts:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
topKExperts:
|
|
299
|
+
glm52?.numExpertsPerToken ?? t.top_k_experts ?? t.num_experts_per_tok ?? 0,
|
|
300
|
+
moeIntermediateSize: glm52?.moeIntermediateSize ?? t.moe_intermediate_size ?? 0,
|
|
301
|
+
decoderSparseStep: glm52 ? 1 : t.decoder_sparse_step ?? 1,
|
|
302
|
+
mlpOnlyLayers: glm52
|
|
303
|
+
? Array.from({ length: glm52.firstKDenseReplace }, (_, layer) => layer)
|
|
304
|
+
: t.mlp_only_layers ?? [],
|
|
305
|
+
normTopkProb: glm52?.normTopkProb ?? t.norm_topk_prob ?? false,
|
|
267
306
|
linearNumValueHeads: t.linear_num_value_heads ?? 0,
|
|
268
307
|
linearNumKeyHeads: t.linear_num_key_heads ?? 0,
|
|
269
308
|
linearKeyHeadDim: t.linear_key_head_dim ?? 0,
|
|
@@ -271,9 +310,19 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
271
310
|
linearConvKernelDim: t.linear_conv_kernel_dim ?? 0,
|
|
272
311
|
fullAttentionInterval: t.full_attention_interval ?? 0,
|
|
273
312
|
attnOutputGate: t.attn_output_gate ?? false,
|
|
274
|
-
partialRotaryFactor:
|
|
275
|
-
|
|
276
|
-
|
|
313
|
+
partialRotaryFactor: glm52
|
|
314
|
+
? glm52.qkRopeHeadDim / glm52.qkHeadDim
|
|
315
|
+
: qwenRope.partial_rotary_factor ?? t.partial_rotary_factor ?? 1.0,
|
|
316
|
+
ropeParameters: glm52
|
|
317
|
+
? {
|
|
318
|
+
full_attention: {
|
|
319
|
+
ropeTheta: glm52.ropeTheta,
|
|
320
|
+
ropeType: "default",
|
|
321
|
+
partialRotaryFactor: glm52.qkRopeHeadDim / glm52.qkHeadDim,
|
|
322
|
+
factor: 1.0,
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
: isQwen35
|
|
277
326
|
? {
|
|
278
327
|
full_attention: {
|
|
279
328
|
ropeTheta: qwenRope.rope_theta ?? t.rope_theta ?? 10000,
|
|
@@ -297,7 +346,7 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
297
346
|
finalLogitSoftcapping: t.final_logit_softcapping ?? null,
|
|
298
347
|
tieWordEmbeddings: t.tie_word_embeddings ?? raw.tie_word_embeddings ?? false,
|
|
299
348
|
bosTokenId: t.bos_token_id ?? raw.bos_token_id,
|
|
300
|
-
eosTokenId: t.eos_token_id ?? raw.eos_token_id,
|
|
349
|
+
eosTokenId: glm52?.eosTokenIds ?? t.eos_token_id ?? raw.eos_token_id,
|
|
301
350
|
canvasLength: raw.canvas_length ?? t.canvas_length,
|
|
302
351
|
};
|
|
303
352
|
|
|
@@ -311,14 +360,20 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
311
360
|
}));
|
|
312
361
|
}
|
|
313
362
|
|
|
314
|
-
const eos = raw.eos_token_id ?? text.eosTokenId;
|
|
363
|
+
const eos = glm52?.eosTokenIds ?? raw.eos_token_id ?? text.eosTokenId;
|
|
315
364
|
return {
|
|
316
365
|
modelDir,
|
|
317
366
|
modelType,
|
|
318
367
|
architectures: raw.architectures ?? [],
|
|
319
368
|
dtype: raw.dtype ?? "bfloat16",
|
|
320
369
|
text,
|
|
321
|
-
|
|
370
|
+
// Colibri's config retains the SOURCE FP8 quantization_config, while the
|
|
371
|
+
// serving shards are converted U8 + `.qs` tensors whose int4/int8/group
|
|
372
|
+
// geometry is inferred and validated by Glm52Container. Advertising the
|
|
373
|
+
// source FP8 block as mlx affine quantization would be actively wrong.
|
|
374
|
+
quantization: glm52
|
|
375
|
+
? null
|
|
376
|
+
: parseQuantization(raw.quantization ?? raw.quantization_config),
|
|
322
377
|
kvQuant,
|
|
323
378
|
hasVisionSidecar: await Bun.file(`${modelDir}/optiq_vision.safetensors`).exists(),
|
|
324
379
|
eosTokenIds: Array.isArray(eos) ? eos : [eos],
|
|
@@ -245,9 +245,7 @@ export function diffusionGenerate(
|
|
|
245
245
|
|
|
246
246
|
let currentCanvas = ops.randint(0, vocab, [1, canvasLength], I32); // RNG: init
|
|
247
247
|
let draftReveal = ops.zeros([1, canvasLength], Dtype.bool);
|
|
248
|
-
let draftCanvas
|
|
249
|
-
draftCanvas.dispose();
|
|
250
|
-
draftCanvas = ops.reshape(currentCanvas, [1, canvasLength]); // alias copy
|
|
248
|
+
let draftCanvas = ops.reshape(currentCanvas, [1, canvasLength]); // alias copy
|
|
251
249
|
let argmaxCanvas = ops.reshape(currentCanvas, [1, canvasLength]);
|
|
252
250
|
let scEmbeddings: MlxArray | null = null;
|
|
253
251
|
const history: MlxArray[] = [];
|
|
@@ -266,7 +264,9 @@ export function diffusionGenerate(
|
|
|
266
264
|
logits.dispose();
|
|
267
265
|
|
|
268
266
|
argmaxCanvas.dispose();
|
|
269
|
-
|
|
267
|
+
const argmax = ops.argmaxAxis(processed, -1);
|
|
268
|
+
argmaxCanvas = argmax.astype(I32); // [1, L]
|
|
269
|
+
argmax.dispose();
|
|
270
270
|
|
|
271
271
|
if (curStep === 1) {
|
|
272
272
|
processed.dispose();
|
|
@@ -296,7 +296,9 @@ export function diffusionGenerate(
|
|
|
296
296
|
currentCanvas.dispose();
|
|
297
297
|
currentCanvas = newCurrent;
|
|
298
298
|
draftReveal.dispose();
|
|
299
|
-
|
|
299
|
+
const notAcceptance = ops.logicalNot(acceptance);
|
|
300
|
+
draftReveal = ops.logicalNot(notAcceptance); // = acceptance
|
|
301
|
+
notAcceptance.dispose();
|
|
300
302
|
draftCanvas.dispose();
|
|
301
303
|
draftCanvas = ops.reshape(argmaxCanvas, [1, canvasLength]);
|
|
302
304
|
accepted.dispose();
|
|
@@ -341,6 +343,10 @@ export function diffusionGenerate(
|
|
|
341
343
|
stableStop &&
|
|
342
344
|
stableAndConfident(argmaxCanvas, processed, history, stabilityThreshold, stopConfidence)
|
|
343
345
|
) {
|
|
346
|
+
// Entropy sampling precomputes the next step's self-conditioning
|
|
347
|
+
// tensor before evaluating this stop. The break means ownership is
|
|
348
|
+
// never transferred to scEmbeddings, so release the pending value.
|
|
349
|
+
nextSc?.dispose();
|
|
344
350
|
processed.dispose();
|
|
345
351
|
break;
|
|
346
352
|
}
|
|
@@ -361,9 +367,9 @@ export function diffusionGenerate(
|
|
|
361
367
|
draftCanvas.dispose();
|
|
362
368
|
|
|
363
369
|
// Emit from the final argmax canvas.
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
);
|
|
370
|
+
const argmaxFloat = argmaxCanvas.astype(Dtype.float32);
|
|
371
|
+
const blockTokens = [...argmaxFloat.toFloat32()].map((x) => Math.round(x));
|
|
372
|
+
argmaxFloat.dispose();
|
|
367
373
|
argmaxCanvas.dispose();
|
|
368
374
|
blocks.push(blockTokens);
|
|
369
375
|
totalSteps += stepsThisCanvas;
|
package/src/download.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// lore"): Xet stalls (we speak plain HTTPS resolve/CDN), no resume on
|
|
5
5
|
// flaky links (we Range-resume partial blobs), and silent corruption
|
|
6
6
|
// (every blob is verified — sha256 for LFS files against the API's
|
|
7
|
-
//
|
|
7
|
+
// normalized LFS digest, git-blob sha1 for small files against blobId).
|
|
8
8
|
//
|
|
9
9
|
// Writes the exact huggingface_hub cache layout so the registry and
|
|
10
10
|
// loaders find the result with zero changes:
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import {
|
|
19
19
|
mkdirSync, existsSync, statSync, createWriteStream, renameSync, rmSync,
|
|
20
20
|
symlinkSync, readFileSync, openSync, closeSync, readSync, writeSync,
|
|
21
|
+
statfsSync,
|
|
21
22
|
} from "node:fs";
|
|
22
23
|
import { dirname, join } from "node:path";
|
|
23
24
|
import { DEFAULT_HUB } from "./registry";
|
|
@@ -29,8 +30,8 @@ export interface RepoFile {
|
|
|
29
30
|
size: number;
|
|
30
31
|
/** git blob sha1 (non-LFS identity, and the blob filename for non-LFS). */
|
|
31
32
|
blobId: string;
|
|
32
|
-
/** LFS sha256 + size; present for large files.
|
|
33
|
-
lfs?: {
|
|
33
|
+
/** LFS sha256 + size; present for large files. sha256 is the blob filename. */
|
|
34
|
+
lfs?: { sha256: string; size: number };
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface RepoListing {
|
|
@@ -47,6 +48,56 @@ export interface DownloadOptions {
|
|
|
47
48
|
onProgress?: (file: string, received: number, total: number) => void;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export interface DownloadSpacePlan {
|
|
52
|
+
/** Bytes still absent after crediting verified blobs and resumable prefixes. */
|
|
53
|
+
remainingBytes: number;
|
|
54
|
+
/** Files that still need at least one byte. */
|
|
55
|
+
remainingFiles: number;
|
|
56
|
+
/** Files already complete in the shared HF blob store. */
|
|
57
|
+
completeFiles: number;
|
|
58
|
+
/** Existing `.incomplete` bytes that will be rehashed and resumed. */
|
|
59
|
+
resumableBytes: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One GiB for filesystem metadata, the final refs write, and normal host
|
|
63
|
+
* operation. The artifact bytes themselves are exact; this is deliberately a
|
|
64
|
+
* small fixed guard rather than a percentage that would add tens of GiB to a
|
|
65
|
+
* Colibri download. */
|
|
66
|
+
export const DOWNLOAD_DISK_RESERVE_BYTES = 2 ** 30;
|
|
67
|
+
|
|
68
|
+
/** Compute the exact remaining payload without opening or hashing any blob.
|
|
69
|
+
* A valid-size final blob is already complete; an in-range `.incomplete`
|
|
70
|
+
* prefix is credited because downloadOneLocked resumes it in place. */
|
|
71
|
+
export function planDownloadSpace(
|
|
72
|
+
files: readonly RepoFile[],
|
|
73
|
+
blobsDir: string,
|
|
74
|
+
): DownloadSpacePlan {
|
|
75
|
+
let remainingBytes = 0;
|
|
76
|
+
let remainingFiles = 0;
|
|
77
|
+
let completeFiles = 0;
|
|
78
|
+
let resumableBytes = 0;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const blobId = file.lfs?.sha256 ?? file.blobId;
|
|
81
|
+
if (!blobId) throw new Error(`no blob id for ${file.rfilename}`);
|
|
82
|
+
const blobPath = join(blobsDir, blobId);
|
|
83
|
+
if (existsSync(blobPath) && statSync(blobPath).size === file.size) {
|
|
84
|
+
completeFiles++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const partPath = `${blobPath}.incomplete`;
|
|
88
|
+
const partBytes = existsSync(partPath) ? statSync(partPath).size : 0;
|
|
89
|
+
const credited = partBytes >= 0 && partBytes <= file.size ? partBytes : 0;
|
|
90
|
+
resumableBytes += credited;
|
|
91
|
+
remainingBytes += file.size - credited;
|
|
92
|
+
remainingFiles++;
|
|
93
|
+
}
|
|
94
|
+
return { remainingBytes, remainingFiles, completeFiles, resumableBytes };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatGib(bytes: number): string {
|
|
98
|
+
return `${(bytes / 2 ** 30).toFixed(2)} GiB`;
|
|
99
|
+
}
|
|
100
|
+
|
|
50
101
|
/** HF auth: explicit > env > the file `hf auth login` writes. */
|
|
51
102
|
export function hfToken(): string | null {
|
|
52
103
|
if (process.env.HF_TOKEN) return process.env.HF_TOKEN;
|
|
@@ -161,16 +212,34 @@ export async function listRepoFiles(
|
|
|
161
212
|
);
|
|
162
213
|
const body = (await res.json()) as {
|
|
163
214
|
sha: string;
|
|
164
|
-
siblings: {
|
|
215
|
+
siblings: {
|
|
216
|
+
rfilename: string;
|
|
217
|
+
size?: number;
|
|
218
|
+
blobId?: string;
|
|
219
|
+
// The current HF API calls this field `sha256`. Accept the older `oid`
|
|
220
|
+
// spelling too, but normalize it before the download path sees it.
|
|
221
|
+
lfs?: { sha256?: string; oid?: string; size: number };
|
|
222
|
+
}[];
|
|
165
223
|
};
|
|
166
224
|
return {
|
|
167
225
|
sha: body.sha,
|
|
168
|
-
files: body.siblings.map((s) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
226
|
+
files: body.siblings.map((s) => {
|
|
227
|
+
let lfs: RepoFile["lfs"];
|
|
228
|
+
if (s.lfs) {
|
|
229
|
+
const sha256 = s.lfs.sha256 ?? s.lfs.oid;
|
|
230
|
+
if (!sha256 || !/^[0-9a-f]{64}$/i.test(sha256))
|
|
231
|
+
throw new Error(`HF API returned an invalid LFS sha256 for ${s.rfilename}`);
|
|
232
|
+
lfs = { sha256: sha256.toLowerCase(), size: s.lfs.size };
|
|
233
|
+
}
|
|
234
|
+
if (!lfs && (!s.blobId || !/^[0-9a-f]{40}$/.test(s.blobId)))
|
|
235
|
+
throw new Error(`HF API returned an invalid git blob SHA-1 for ${s.rfilename}`);
|
|
236
|
+
return {
|
|
237
|
+
rfilename: s.rfilename,
|
|
238
|
+
size: lfs?.size ?? s.size ?? 0,
|
|
239
|
+
blobId: s.blobId ?? "",
|
|
240
|
+
lfs,
|
|
241
|
+
};
|
|
242
|
+
}),
|
|
174
243
|
};
|
|
175
244
|
}
|
|
176
245
|
|
|
@@ -349,6 +418,28 @@ export async function downloadModel(
|
|
|
349
418
|
mkdirSync(blobsDir, { recursive: true });
|
|
350
419
|
mkdirSync(join(repoDir, "refs"), { recursive: true });
|
|
351
420
|
|
|
421
|
+
// Preflight before the first network payload. This matters for direct
|
|
422
|
+
// Colibri artifacts (hundreds of GiB): discovering a full volume after the
|
|
423
|
+
// 140th shard is not a usable failure mode. Existing verified blobs and
|
|
424
|
+
// partial prefixes are credited exactly, so rerunning after interruption is
|
|
425
|
+
// both resumable and progressively cheaper.
|
|
426
|
+
const space = planDownloadSpace(listing.files, blobsDir);
|
|
427
|
+
if (space.remainingBytes > 0) {
|
|
428
|
+
const fs = statfsSync(blobsDir);
|
|
429
|
+
const availableBytes = fs.bavail * fs.bsize;
|
|
430
|
+
const requiredBytes = space.remainingBytes + DOWNLOAD_DISK_RESERVE_BYTES;
|
|
431
|
+
if (requiredBytes > availableBytes) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`insufficient disk space for ${repoId}: ${formatGib(space.remainingBytes)} ` +
|
|
434
|
+
`remains across ${space.remainingFiles} files, but only ` +
|
|
435
|
+
`${formatGib(availableBytes)} is available in ${hub} ` +
|
|
436
|
+
`(${formatGib(DOWNLOAD_DISK_RESERVE_BYTES)} safety reserve). ` +
|
|
437
|
+
`Free space or choose another cache directory, then rerun; ` +
|
|
438
|
+
`${formatGib(space.resumableBytes)} of partial data will resume.`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
352
443
|
const status: DownloadStatus = {
|
|
353
444
|
repoId, state: "active", currentFile: null,
|
|
354
445
|
receivedBytes: 0, totalBytes: listing.files.reduce((a, f) => a + f.size, 0),
|
|
@@ -369,7 +460,7 @@ export async function downloadModel(
|
|
|
369
460
|
|
|
370
461
|
try {
|
|
371
462
|
for (const f of listing.files) {
|
|
372
|
-
const blobId = f.lfs?.
|
|
463
|
+
const blobId = f.lfs?.sha256 ?? f.blobId;
|
|
373
464
|
if (!blobId) throw new Error(`no blob id for ${f.rfilename} (API response missing ?blobs=true data)`);
|
|
374
465
|
const blobPath = join(blobsDir, blobId);
|
|
375
466
|
|
|
@@ -378,7 +469,7 @@ export async function downloadModel(
|
|
|
378
469
|
status.currentFile = f.rfilename;
|
|
379
470
|
await downloadOne(url, token, blobPath, {
|
|
380
471
|
size: f.size, name: f.rfilename,
|
|
381
|
-
...(f.lfs ? { sha256: f.lfs.
|
|
472
|
+
...(f.lfs ? { sha256: f.lfs.sha256 } : { sha1: f.blobId }),
|
|
382
473
|
}, (file, received, total) => {
|
|
383
474
|
status.receivedBytes = doneBytes + received;
|
|
384
475
|
sampleRate(status.receivedBytes);
|
package/src/eval/ifeval.ts
CHANGED
|
@@ -1,138 +1,81 @@
|
|
|
1
|
-
//
|
|
2
|
-
// al. 2023, "Instruction-Following Evaluation for Large Language Models"). Each
|
|
3
|
-
// instance pairs a prompt with one or more *verifiable* instructions; a response
|
|
4
|
-
// is scored by deterministic checks (no LLM judge), so before/after ORPO
|
|
5
|
-
// comparisons are exact and reproducible.
|
|
1
|
+
// Compatibility facade for the canonical IFEval scorer.
|
|
6
2
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// instruction-level accuracy — both derived from `scoreInstance`.
|
|
3
|
+
// The complete verifier registry and strict/loose aggregation contract live in
|
|
4
|
+
// `tasks/ifeval.ts`, which is also used by `scripts/eval.ts`. Keep this module
|
|
5
|
+
// as the stable import surface used by `scripts/run-ifeval.ts` and older
|
|
6
|
+
// callers; it must not grow a second verifier implementation.
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
import {
|
|
9
|
+
scoreIfevalInstance,
|
|
10
|
+
scoreIfevalPairs,
|
|
11
|
+
type IfevalInstance,
|
|
12
|
+
type IfevalResult,
|
|
13
|
+
} from "./tasks/ifeval";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
SUPPORTED_INSTRUCTIONS,
|
|
17
|
+
looseClean,
|
|
18
|
+
scoreIfevalInstance,
|
|
19
|
+
scoreIfevalPairs,
|
|
20
|
+
stripThinking,
|
|
21
|
+
verifyResponse,
|
|
22
|
+
type IfevalCoverage,
|
|
23
|
+
type IfevalInstance,
|
|
24
|
+
type IfevalInstanceScore,
|
|
25
|
+
type IfevalPair,
|
|
26
|
+
type IfevalResult,
|
|
27
|
+
type IfevalVerification,
|
|
28
|
+
} from "./tasks/ifeval";
|
|
29
|
+
|
|
30
|
+
/** Historical spelling retained for callers of this module. */
|
|
31
|
+
export type IFEvalInstance = IfevalInstance;
|
|
21
32
|
|
|
22
33
|
export interface InstanceResult {
|
|
23
|
-
/**
|
|
34
|
+
/** Unknown instructions are true here per the canonical prompt contract. */
|
|
24
35
|
perInstruction: boolean[];
|
|
25
|
-
/** Strict: all instructions passed. */
|
|
26
36
|
followedAll: boolean;
|
|
37
|
+
/** Consult this field rather than interpreting unknown IDs as verified. */
|
|
38
|
+
unhandled: string[];
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
case "exactly": return n === target;
|
|
44
|
-
default: return n >= target; // IFEval default for "around"/unspecified is lenient ≥
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** The verifiable-instruction registry. Keyed by IFEval instruction_id. */
|
|
49
|
-
export const INSTRUCTIONS: Record<string, Checker> = {
|
|
50
|
-
// ---- keywords ----
|
|
51
|
-
"keywords:existence": (r, kw) =>
|
|
52
|
-
(kw.keywords as string[]).every((k) => norm(r).includes(norm(k))),
|
|
53
|
-
"keywords:frequency": (r, kw) => {
|
|
54
|
-
const k = norm(kw.keyword as string);
|
|
55
|
-
const count = norm(r).split(k).length - 1;
|
|
56
|
-
return cmp(count, kw.relation as string, kw.frequency as number);
|
|
57
|
-
},
|
|
58
|
-
"keywords:forbidden_words": (r, kw) =>
|
|
59
|
-
!(kw.forbidden_words as string[]).some((k) => norm(r).includes(norm(k))),
|
|
60
|
-
|
|
61
|
-
// ---- length constraints ----
|
|
62
|
-
"length_constraints:number_words": (r, kw) =>
|
|
63
|
-
cmp(words(r).length, kw.relation as string, kw.num_words as number),
|
|
64
|
-
"length_constraints:number_sentences": (r, kw) =>
|
|
65
|
-
cmp(sentences(r).length, kw.relation as string, kw.num_sentences as number),
|
|
66
|
-
"length_constraints:number_paragraphs": (r, kw) =>
|
|
67
|
-
cmp(paragraphs(r).length, kw.relation as string, kw.num_paragraphs as number),
|
|
68
|
-
|
|
69
|
-
// ---- detectable format ----
|
|
70
|
-
"detectable_format:number_bullet_lists": (r, kw) => {
|
|
71
|
-
const bullets = r.split("\n").filter((l) => /^\s*[*\-]\s+/.test(l)).length;
|
|
72
|
-
return cmp(bullets, kw.relation as string, kw.num_bullets as number);
|
|
73
|
-
},
|
|
74
|
-
"detectable_format:number_highlighted_sections": (r, kw) => {
|
|
75
|
-
const hi = (r.match(/\*[^*\n]+\*/g) ?? []).length;
|
|
76
|
-
return cmp(hi, kw.relation as string, kw.num_highlights as number);
|
|
77
|
-
},
|
|
78
|
-
"detectable_format:title": (r) => /<<[^>\n]+>>/.test(r),
|
|
79
|
-
"detectable_format:json_format": (r) => {
|
|
80
|
-
const t = r.trim().replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
|
|
81
|
-
try { JSON.parse(t); return true; } catch { return false; }
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
// ---- case ----
|
|
85
|
-
"change_case:english_lowercase": (r) => r === r.toLowerCase(),
|
|
86
|
-
"change_case:english_capital": (r) => r === r.toUpperCase(),
|
|
87
|
-
|
|
88
|
-
// ---- start / end ----
|
|
89
|
-
"startend:end_checker": (r, kw) =>
|
|
90
|
-
norm(r.trimEnd()).endsWith(norm((kw.end_phrase as string).trim())),
|
|
91
|
-
"startend:quotation": (r) => {
|
|
92
|
-
const t = r.trim();
|
|
93
|
-
return t.startsWith('"') && t.endsWith('"') && t.length >= 2;
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
// ---- punctuation ----
|
|
97
|
-
"punctuation:no_comma": (r) => !r.includes(","),
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** Score one IFEval instance (strict). Unknown instruction ids count as failed
|
|
101
|
-
* (so coverage gaps never inflate the score) — filter the dataset to the
|
|
102
|
-
* supported ids, or extend INSTRUCTIONS, to avoid that. */
|
|
103
|
-
export function scoreInstance(instance: IFEvalInstance, response: string): InstanceResult {
|
|
104
|
-
const perInstruction = instance.instruction_id_list.map((id, i) => {
|
|
105
|
-
const check = INSTRUCTIONS[id];
|
|
106
|
-
if (!check) return false;
|
|
107
|
-
try { return check(response, instance.kwargs[i] ?? {}, instance); } catch { return false; }
|
|
108
|
-
});
|
|
109
|
-
return { perInstruction, followedAll: perInstruction.every(Boolean) };
|
|
41
|
+
/**
|
|
42
|
+
* Historical strict-only helper, now delegated to the canonical scorer.
|
|
43
|
+
* New code should prefer `scoreIfevalInstance` for both strict and loose modes.
|
|
44
|
+
*/
|
|
45
|
+
export function scoreInstance(
|
|
46
|
+
instance: IFEvalInstance,
|
|
47
|
+
response: string,
|
|
48
|
+
): InstanceResult {
|
|
49
|
+
const { strict } = scoreIfevalInstance(instance, response);
|
|
50
|
+
return {
|
|
51
|
+
perInstruction: strict.instructionPasses.map((result) => result ?? true),
|
|
52
|
+
followedAll: strict.pass,
|
|
53
|
+
unhandled: strict.unhandled,
|
|
54
|
+
};
|
|
110
55
|
}
|
|
111
56
|
|
|
112
|
-
export interface IFEvalReport {
|
|
57
|
+
export interface IFEvalReport extends IfevalResult {
|
|
58
|
+
/** Historical alias for nTotal. */
|
|
113
59
|
n: number;
|
|
114
|
-
/**
|
|
60
|
+
/** Historical alias for strictAcc (prompt-level strict). */
|
|
115
61
|
promptAccuracy: number;
|
|
116
|
-
/**
|
|
62
|
+
/** Historical alias for strictInstructionAcc. */
|
|
117
63
|
instructionAccuracy: number;
|
|
118
64
|
}
|
|
119
65
|
|
|
120
|
-
/**
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
66
|
+
/**
|
|
67
|
+
* Historical aggregation helper with unambiguous canonical fields plus aliases.
|
|
68
|
+
* Unknown instructions do not fail prompt accuracy, are excluded from
|
|
69
|
+
* instruction accuracy, and remain visible in `coverage`.
|
|
70
|
+
*/
|
|
71
|
+
export function aggregate(
|
|
72
|
+
pairs: Array<{ instance: IFEvalInstance; response: string }>,
|
|
73
|
+
): IFEvalReport {
|
|
74
|
+
const result = scoreIfevalPairs(pairs);
|
|
130
75
|
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
76
|
+
...result,
|
|
77
|
+
n: result.nTotal,
|
|
78
|
+
promptAccuracy: result.strictAcc,
|
|
79
|
+
instructionAccuracy: result.strictInstructionAcc,
|
|
134
80
|
};
|
|
135
81
|
}
|
|
136
|
-
|
|
137
|
-
/** Instruction ids this implementation supports (filter datasets to these). */
|
|
138
|
-
export const SUPPORTED_INSTRUCTIONS = new Set(Object.keys(INSTRUCTIONS));
|
package/src/eval/runner.ts
CHANGED
|
@@ -9,8 +9,12 @@ import { generate, type GenerateOptions } from "../generate";
|
|
|
9
9
|
import * as ops from "../mlx/ops";
|
|
10
10
|
import { ChatTemplate } from "../chat-template";
|
|
11
11
|
import { loadModelConfig, type ModelConfig } from "../config";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
openModel,
|
|
14
|
+
type Glm52RuntimeOpenOptions,
|
|
15
|
+
type RuntimeModel,
|
|
16
|
+
} from "../model/factory";
|
|
17
|
+
import { isGlm52Config } from "../model/support";
|
|
14
18
|
import { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
15
19
|
import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
|
|
16
20
|
import { resolveModelDir } from "./kl";
|
|
@@ -65,11 +69,16 @@ export interface TaskModel {
|
|
|
65
69
|
activeAdapters?: string[];
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
export async function loadTaskModel(
|
|
72
|
+
export async function loadTaskModel(
|
|
73
|
+
query: string,
|
|
74
|
+
adapterDir?: string,
|
|
75
|
+
runtimeOptions: Glm52RuntimeOpenOptions = {},
|
|
76
|
+
): Promise<TaskModel> {
|
|
69
77
|
const dir = resolveModelDir(query);
|
|
70
78
|
const config = await loadModelConfig(dir);
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
if (adapterDir && isGlm52Config(config))
|
|
80
|
+
throw new Error("GLM-5.2 does not support LoRA adapters");
|
|
81
|
+
const model = await openModel(dir, runtimeOptions);
|
|
73
82
|
const activeAdapters: string[] = [];
|
|
74
83
|
if (adapterDir) {
|
|
75
84
|
// Mount a trained LoRA adapter so the eval measures base+adapter (e.g. the
|