mlx-bun 0.0.11 → 0.0.12
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 +43 -13
- package/THIRD_PARTY_LICENSES.md +9 -0
- package/docs/design/colibri-glm52-port.md +1479 -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/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 +4 -2
- package/docs/reference/library-api.md +41 -13
- package/docs/reference/models.md +49 -2
- package/docs/reference/server-api.md +90 -9
- 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 +155 -10
- package/src/cli.ts +127 -8
- package/src/config.ts +71 -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 +362 -81
- 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/chat-template.ts
CHANGED
|
@@ -42,6 +42,126 @@ export interface RenderOptions {
|
|
|
42
42
|
enableThinking?: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type ChatRenderer = (
|
|
46
|
+
messages: ChatMessage[],
|
|
47
|
+
options: RenderOptions,
|
|
48
|
+
) => string;
|
|
49
|
+
|
|
50
|
+
function glm52ContentText(content: ChatMessage["content"], label: string): string {
|
|
51
|
+
if (typeof content === "string") return content;
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
throw new Error(`${label} must be a string or an array of text parts`);
|
|
54
|
+
return content.map((part, index) => {
|
|
55
|
+
if (
|
|
56
|
+
!part ||
|
|
57
|
+
(part.type !== "text" && part.type !== "input_text") ||
|
|
58
|
+
typeof part.text !== "string"
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(`${label}.${index}: GLM-5.2 supports text content only`);
|
|
61
|
+
}
|
|
62
|
+
return part.text;
|
|
63
|
+
}).join("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Text/tool subset of the pinned GLM-5.2 template used by Colibri. */
|
|
67
|
+
export function renderGlm52Chat(
|
|
68
|
+
messages: ChatMessage[],
|
|
69
|
+
options: RenderOptions = {},
|
|
70
|
+
): string {
|
|
71
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
72
|
+
throw new Error("GLM-5.2 messages must be a non-empty array");
|
|
73
|
+
const {
|
|
74
|
+
addGenerationPrompt = true,
|
|
75
|
+
tools = null,
|
|
76
|
+
enableThinking = false,
|
|
77
|
+
} = options;
|
|
78
|
+
const prompt = ["[gMASK]<sop>"];
|
|
79
|
+
if (enableThinking)
|
|
80
|
+
prompt.push("<|system|>Reasoning Effort: Max");
|
|
81
|
+
|
|
82
|
+
const normalizedTools = normalizeToolSchemas(tools);
|
|
83
|
+
if (normalizedTools && normalizedTools.length > 0) {
|
|
84
|
+
prompt.push(
|
|
85
|
+
"<|system|>\n# Tools\n\nYou may call one or more functions to assist with the " +
|
|
86
|
+
"user query.\n\nYou are provided with function signatures within <tools></tools> " +
|
|
87
|
+
"XML tags:\n<tools>\n",
|
|
88
|
+
);
|
|
89
|
+
for (const tool of normalizedTools) {
|
|
90
|
+
const fn = { ...tool.function } as Record<string, unknown>;
|
|
91
|
+
delete fn.defer_loading;
|
|
92
|
+
delete fn.strict;
|
|
93
|
+
prompt.push(`${JSON.stringify(fn)}\n`);
|
|
94
|
+
}
|
|
95
|
+
prompt.push(
|
|
96
|
+
"</tools>\n\nFor each function call, output the function name and arguments " +
|
|
97
|
+
"within the following XML format:\n<tool_call>{function-name}" +
|
|
98
|
+
"<arg_key>{arg-key-1}</arg_key><arg_value>{arg-value-1}</arg_value>" +
|
|
99
|
+
"<arg_key>{arg-key-2}</arg_key><arg_value>{arg-value-2}</arg_value>...</tool_call>",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let previousWasTool = false;
|
|
104
|
+
for (let index = 0; index < messages.length; index++) {
|
|
105
|
+
const message = messages[index]!;
|
|
106
|
+
const role = message.role;
|
|
107
|
+
if (role === "system" || role === "developer") {
|
|
108
|
+
prompt.push(
|
|
109
|
+
`<|system|>${glm52ContentText(message.content, `messages.${index}.content`)}`,
|
|
110
|
+
);
|
|
111
|
+
} else if (role === "user") {
|
|
112
|
+
prompt.push(
|
|
113
|
+
`<|user|>${glm52ContentText(message.content, `messages.${index}.content`)}`,
|
|
114
|
+
);
|
|
115
|
+
} else if (role === "assistant") {
|
|
116
|
+
const content = message.content === null || message.content === undefined
|
|
117
|
+
? ""
|
|
118
|
+
: glm52ContentText(message.content, `messages.${index}.content`);
|
|
119
|
+
prompt.push(`<|assistant|><think></think>${content.trim()}`);
|
|
120
|
+
for (const call of message.tool_calls ?? []) {
|
|
121
|
+
const raw = call.function.arguments;
|
|
122
|
+
let args: Record<string, unknown> = {};
|
|
123
|
+
if (typeof raw === "string") {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
127
|
+
args = parsed as Record<string, unknown>;
|
|
128
|
+
} catch {
|
|
129
|
+
// Match the pinned template path: malformed historical arguments
|
|
130
|
+
// contribute an empty call body rather than changing the prompt.
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
args = raw;
|
|
134
|
+
}
|
|
135
|
+
prompt.push(`<tool_call>${call.function.name}`);
|
|
136
|
+
for (const [key, value] of Object.entries(args)) {
|
|
137
|
+
prompt.push(
|
|
138
|
+
`<arg_key>${key}</arg_key><arg_value>` +
|
|
139
|
+
`${typeof value === "string" ? value : JSON.stringify(value)}</arg_value>`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
prompt.push("</tool_call>");
|
|
143
|
+
}
|
|
144
|
+
} else if (role === "tool") {
|
|
145
|
+
if (!previousWasTool) prompt.push("<|observation|>");
|
|
146
|
+
prompt.push(
|
|
147
|
+
`<tool_response>${glm52ContentText(message.content, `messages.${index}.content`)}` +
|
|
148
|
+
"</tool_response>",
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error(`GLM-5.2 unsupported message role ${JSON.stringify(role)}`);
|
|
152
|
+
}
|
|
153
|
+
previousWasTool = role === "tool";
|
|
154
|
+
}
|
|
155
|
+
if (addGenerationPrompt) {
|
|
156
|
+
prompt.push(
|
|
157
|
+
enableThinking
|
|
158
|
+
? "<|assistant|><think>"
|
|
159
|
+
: "<|assistant|><think></think>",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return prompt.join("");
|
|
163
|
+
}
|
|
164
|
+
|
|
45
165
|
/** JSON-schema type implied by a literal value. */
|
|
46
166
|
function jsonTypeOf(v: unknown): string {
|
|
47
167
|
if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number";
|
|
@@ -116,7 +236,8 @@ export function normalizeToolSchemas(tools: ToolDefinition[] | null): ToolDefini
|
|
|
116
236
|
}
|
|
117
237
|
|
|
118
238
|
export class ChatTemplate {
|
|
119
|
-
readonly #template: Template;
|
|
239
|
+
readonly #template: Template | null;
|
|
240
|
+
readonly #renderer: ChatRenderer | null;
|
|
120
241
|
readonly #bosToken: string | null;
|
|
121
242
|
readonly #eosToken: string | null;
|
|
122
243
|
/** Which reasoning format the template's `enable_thinking` channel uses, or
|
|
@@ -132,15 +253,20 @@ export class ChatTemplate {
|
|
|
132
253
|
readonly supportsThinking: boolean;
|
|
133
254
|
|
|
134
255
|
private constructor(
|
|
135
|
-
source: string,
|
|
256
|
+
source: string | null,
|
|
136
257
|
bosToken: string | null,
|
|
137
258
|
eosToken: string | null,
|
|
138
259
|
forceNoThinking = false,
|
|
260
|
+
renderer: ChatRenderer | null = null,
|
|
261
|
+
explicitThinkingFormat: "think-tag" | "gemma-channel" | null = null,
|
|
139
262
|
) {
|
|
140
|
-
|
|
263
|
+
if (source === null && renderer === null)
|
|
264
|
+
throw new Error("ChatTemplate needs a Jinja source or renderer");
|
|
265
|
+
this.#template = source === null ? null : new Template(source);
|
|
266
|
+
this.#renderer = renderer;
|
|
141
267
|
this.#bosToken = bosToken;
|
|
142
268
|
this.#eosToken = eosToken;
|
|
143
|
-
const gatesThinking = source
|
|
269
|
+
const gatesThinking = source?.includes("enable_thinking") ?? false;
|
|
144
270
|
// `forceNoThinking` suppresses the switchable channel even when the template
|
|
145
271
|
// carries the gemma markers. DiffusionGemma ships the shared gemma-family
|
|
146
272
|
// template (with the `<|channel>thought…<channel|>` reasoning channel), but
|
|
@@ -149,13 +275,13 @@ export class ChatTemplate {
|
|
|
149
275
|
// reasoning, no answer). The OptiQ reference never enables thinking for this
|
|
150
276
|
// model — it always renders the template's `default(false)` pre-closed empty
|
|
151
277
|
// channel and decodes the whole canvas as plain text. We match that.
|
|
152
|
-
this.thinkingFormat = forceNoThinking
|
|
278
|
+
this.thinkingFormat = explicitThinkingFormat ?? (forceNoThinking
|
|
153
279
|
? null
|
|
154
|
-
: gatesThinking && source
|
|
280
|
+
: gatesThinking && source!.includes("<think>")
|
|
155
281
|
? "think-tag"
|
|
156
|
-
: gatesThinking && source
|
|
282
|
+
: gatesThinking && source!.includes("<|channel>")
|
|
157
283
|
? "gemma-channel"
|
|
158
|
-
: null;
|
|
284
|
+
: null);
|
|
159
285
|
this.supportsThinking = this.thinkingFormat !== null;
|
|
160
286
|
}
|
|
161
287
|
|
|
@@ -169,7 +295,25 @@ export class ChatTemplate {
|
|
|
169
295
|
const jinjaFile = Bun.file(`${modelDir}/chat_template.jinja`);
|
|
170
296
|
if (await jinjaFile.exists()) source = await jinjaFile.text();
|
|
171
297
|
}
|
|
172
|
-
if (!source)
|
|
298
|
+
if (!source) {
|
|
299
|
+
const modelConfigFile = Bun.file(`${modelDir}/config.json`);
|
|
300
|
+
const modelConfig = await modelConfigFile.exists()
|
|
301
|
+
? await modelConfigFile.json() as Record<string, unknown>
|
|
302
|
+
: null;
|
|
303
|
+
if (modelConfig?.model_type === "glm_moe_dsa") {
|
|
304
|
+
const tokenText = (t: unknown): string | null =>
|
|
305
|
+
t == null ? null : typeof t === "string" ? t : (t as any).content;
|
|
306
|
+
return new ChatTemplate(
|
|
307
|
+
null,
|
|
308
|
+
tokenText(config.bos_token),
|
|
309
|
+
tokenText(config.eos_token),
|
|
310
|
+
false,
|
|
311
|
+
renderGlm52Chat,
|
|
312
|
+
"think-tag",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`${modelDir}: no chat template found`);
|
|
316
|
+
}
|
|
173
317
|
// @huggingface/jinja lacks the `min`/`max` array filters that real
|
|
174
318
|
// Jinja2 has. MiniCPM5's template uses `[a, b]|min` in its assistant
|
|
175
319
|
// tool-call history branch, so without this rewrite every multi-turn
|
|
@@ -189,8 +333,9 @@ export class ChatTemplate {
|
|
|
189
333
|
}
|
|
190
334
|
|
|
191
335
|
render(messages: ChatMessage[], options: RenderOptions = {}): string {
|
|
336
|
+
if (this.#renderer) return this.#renderer(messages, options);
|
|
192
337
|
const { addGenerationPrompt = true, tools = null, enableThinking } = options;
|
|
193
|
-
return this.#template
|
|
338
|
+
return this.#template!.render({
|
|
194
339
|
messages,
|
|
195
340
|
add_generation_prompt: addGenerationPrompt,
|
|
196
341
|
// Guarantee every tool param schema has a `type` so templates that do
|
package/src/cli.ts
CHANGED
|
@@ -17,7 +17,15 @@ import { fit, skuMatrix, thisMachine } from "./fit";
|
|
|
17
17
|
import { EvalDB } from "./evaldb";
|
|
18
18
|
import pkg from "../package.json" with { type: "json" };
|
|
19
19
|
import { renderHelp } from "./tui";
|
|
20
|
-
import { isSupportedModelRecord } from "./model/support";
|
|
20
|
+
import { isGlm52Config, isSupportedModelRecord } from "./model/support";
|
|
21
|
+
import {
|
|
22
|
+
GLM52_G5_ASPIRATIONAL_DECODE_TPS,
|
|
23
|
+
GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
|
|
24
|
+
GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES,
|
|
25
|
+
GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
|
|
26
|
+
GLM52_G5_MEASURED_WARM_DECODE_TPS,
|
|
27
|
+
planGlm52MemoryForArtifact,
|
|
28
|
+
} from "./model/glm52-memory";
|
|
21
29
|
import type { JobEvent } from "./jobs/types";
|
|
22
30
|
|
|
23
31
|
const argv = process.argv.slice(2);
|
|
@@ -153,6 +161,11 @@ Model & quality:
|
|
|
153
161
|
DSpark pins to its trained block width]
|
|
154
162
|
--ngram-max <k> Prompt-lookup longest match window [default: 3]
|
|
155
163
|
--ngram-min <k> Prompt-lookup shortest match window [default: 1]
|
|
164
|
+
--mtp <on|off> GLM-5.2 checkpoint-native MTP. On by default;
|
|
165
|
+
routes requests through the exact serial verify
|
|
166
|
+
lane. off enables ordinary continuous batching.
|
|
167
|
+
--context-length <n> GLM-5.2 resource-plan context reservation
|
|
168
|
+
[default: 4096]
|
|
156
169
|
--kv-quant <mode> KV cache quantization: config (per-layer
|
|
157
170
|
kv_config.json when the model ships one), off
|
|
158
171
|
(bf16), 4 / 8 (uniform bits), or turbo[:k<bits>v
|
|
@@ -190,7 +203,8 @@ Model & quality:
|
|
|
190
203
|
[default: the model's generation_config.json]
|
|
191
204
|
--max-tokens <n> Completion cap when a request omits max_tokens
|
|
192
205
|
(mlx_lm.server flag; its default is 512 there)
|
|
193
|
-
[default: 65536 chat /
|
|
206
|
+
[default: 128 GLM-5.2; otherwise 65536 chat /
|
|
207
|
+
512 raw completion]
|
|
194
208
|
--hlg-sampling on|off Piecewise tone-curve sampling (HLG): rolls off the
|
|
195
209
|
top, boosts the mids, gentles the tail. Gain folds
|
|
196
210
|
from --temperature. [default: off]
|
|
@@ -285,7 +299,9 @@ Options:
|
|
|
285
299
|
|
|
286
300
|
Resumable (Range requests against partial blobs) and verified (sha256
|
|
287
301
|
for LFS blobs). Re-running after an interruption continues where it
|
|
288
|
-
stopped.
|
|
302
|
+
stopped. Before payload transfer, complete blobs and partial prefixes are
|
|
303
|
+
credited and the target volume must fit the remainder plus a 1 GiB reserve.
|
|
304
|
+
Uses HF_TOKEN / hf auth login credentials when present.
|
|
289
305
|
When upstream pushed a new revision, the previous snapshot stays on
|
|
290
306
|
disk — \`mlx-bun gc\` reclaims it.`,
|
|
291
307
|
|
|
@@ -1007,6 +1023,39 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
1007
1023
|
return { port: Number(opt("port", "8080")), serverOptions };
|
|
1008
1024
|
}
|
|
1009
1025
|
|
|
1026
|
+
/** Direct Colibri runtime defaults shared by `serve` and the embedded `pi`
|
|
1027
|
+
* server. Other model families ignore this load-only option block. */
|
|
1028
|
+
function glm52RuntimeFlags(
|
|
1029
|
+
serverOptions: import("./server").ServerOptions,
|
|
1030
|
+
): import("./model/factory").Glm52RuntimeOpenOptions {
|
|
1031
|
+
const mtpRaw = opt("mtp");
|
|
1032
|
+
let enableMtp: boolean | undefined;
|
|
1033
|
+
if (mtpRaw !== null) {
|
|
1034
|
+
if (["on", "1", "true"].includes(mtpRaw)) enableMtp = true;
|
|
1035
|
+
else if (["off", "0", "false"].includes(mtpRaw)) enableMtp = false;
|
|
1036
|
+
else {
|
|
1037
|
+
console.error(`--mtp expects on|off (got "${mtpRaw}")`);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const contextRaw = opt("context-length");
|
|
1042
|
+
let contextTokens: number | undefined;
|
|
1043
|
+
if (contextRaw !== null) {
|
|
1044
|
+
contextTokens = Number(contextRaw);
|
|
1045
|
+
if (!Number.isSafeInteger(contextTokens) || contextTokens < 1) {
|
|
1046
|
+
console.error(`--context-length expects an integer >= 1 (got "${contextRaw}")`);
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
memoryBudgetBytes: serverOptions.memoryBudgetBytes,
|
|
1052
|
+
batchSize: serverOptions.batch ?? 8,
|
|
1053
|
+
maxGenerationTokens: serverOptions.defaultMaxTokens ?? 128,
|
|
1054
|
+
...(contextTokens !== undefined ? { contextTokens } : {}),
|
|
1055
|
+
...(enableMtp !== undefined ? { enableMtp } : {}),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1010
1059
|
/** `--adapter <dir>` (alias `--adapter-path`, mlx_lm.server's spelling):
|
|
1011
1060
|
* mount a LoRA adapter at startup — the same machinery as POST /v1/adapters —
|
|
1012
1061
|
* and make it the default for requests that don't select one (a request's
|
|
@@ -1332,9 +1381,69 @@ switch (cmd) {
|
|
|
1332
1381
|
if (reg.list().length === 0) await reg.scan();
|
|
1333
1382
|
const m = reg.resolve(query);
|
|
1334
1383
|
const config = await loadModelConfig(m.path);
|
|
1335
|
-
const ctx = Number(opt("ctx", "8192"));
|
|
1336
|
-
const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
|
|
1337
1384
|
const { box, table, style, h1, gradient } = await import("./tui");
|
|
1385
|
+
const glm = isGlm52Config(config);
|
|
1386
|
+
const ctx = Number(opt("ctx", glm ? "4096" : "8192"));
|
|
1387
|
+
if (glm) {
|
|
1388
|
+
const machine = thisMachine();
|
|
1389
|
+
let plan;
|
|
1390
|
+
try {
|
|
1391
|
+
plan = await planGlm52MemoryForArtifact(m.path, {
|
|
1392
|
+
machineBytes: machine.ramBytes,
|
|
1393
|
+
processLimitBytes: Math.min(
|
|
1394
|
+
GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES,
|
|
1395
|
+
machine.ramBytes,
|
|
1396
|
+
),
|
|
1397
|
+
contextTokens: ctx,
|
|
1398
|
+
maxGenerationTokens: Math.min(
|
|
1399
|
+
ctx,
|
|
1400
|
+
GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
|
|
1401
|
+
),
|
|
1402
|
+
batchSize: 1,
|
|
1403
|
+
enableMtp: true,
|
|
1404
|
+
});
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
console.error(
|
|
1407
|
+
`GLM-5.2 resource plan refused: ` +
|
|
1408
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
1409
|
+
);
|
|
1410
|
+
process.exitCode = 1;
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
const gib = (bytes: number): string => `${(bytes / 2 ** 30).toFixed(2)} GiB`;
|
|
1414
|
+
const li = plan.lineItems;
|
|
1415
|
+
const kvBytes = li.targetKvBytes + li.mtpKvBytes;
|
|
1416
|
+
const otherBytes = plan.plannedProcessBytes - li.residentWeightsBytes -
|
|
1417
|
+
li.mainExpertSlabBytes - li.mtpExpertSlabBytes - kvBytes;
|
|
1418
|
+
h1("will it fit?");
|
|
1419
|
+
console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · streamed GLM plan`)}`);
|
|
1420
|
+
console.log();
|
|
1421
|
+
box([
|
|
1422
|
+
`artifact on disk ${gib(m.sizeBytes).padStart(10)} ${style.dim("(streamed, not resident)")}`,
|
|
1423
|
+
`resident weights ${gib(li.residentWeightsBytes).padStart(10)}`,
|
|
1424
|
+
`main expert slab ${gib(li.mainExpertSlabBytes).padStart(10)}`,
|
|
1425
|
+
`MTP expert slab ${gib(li.mtpExpertSlabBytes).padStart(10)}`,
|
|
1426
|
+
`target + MTP KV ${gib(kvBytes).padStart(10)}`,
|
|
1427
|
+
`runtime reserves ${gib(otherBytes).padStart(10)}`,
|
|
1428
|
+
`process plan ${gib(plan.plannedProcessBytes).padStart(10)} ${style.dim(`of ${gib(plan.processLimitBytes)}`)} ${style.green(style.bold("FITS"))}`,
|
|
1429
|
+
`macOS headroom ${gib(plan.machineHeadroomBytes).padStart(10)}`,
|
|
1430
|
+
"",
|
|
1431
|
+
`max safe context ${style.bold(plan.contextTokens.toLocaleString())} tokens`,
|
|
1432
|
+
`max generation ${style.bold(plan.maxGenerationTokens.toLocaleString())} tokens`,
|
|
1433
|
+
`measured warm ${gradient(`${GLM52_G5_MEASURED_WARM_DECODE_TPS.toFixed(3)} tok/s`)} ${style.dim("(quality-preserving, MTP on)")}`,
|
|
1434
|
+
`direct oracle ${GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS.toFixed(2)} tok/s`,
|
|
1435
|
+
`aspirational ${GLM52_G5_ASPIRATIONAL_DECODE_TPS.toFixed(1)} tok/s ${style.dim("(not a fit prediction or release gate)")}`,
|
|
1436
|
+
]);
|
|
1437
|
+
if (flag("skus")) {
|
|
1438
|
+
console.log(style.dim(
|
|
1439
|
+
" GLM-5.2's direct-container preset is validated on the M1 Max 32 GB; " +
|
|
1440
|
+
"the generic resident-weight SKU projection does not apply.",
|
|
1441
|
+
));
|
|
1442
|
+
}
|
|
1443
|
+
console.log();
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
|
|
1338
1447
|
h1("will it fit?");
|
|
1339
1448
|
console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}`);
|
|
1340
1449
|
console.log();
|
|
@@ -1529,6 +1638,7 @@ switch (cmd) {
|
|
|
1529
1638
|
const t0 = performance.now();
|
|
1530
1639
|
const ctx = await loadContext(m.path, m.repoId, {
|
|
1531
1640
|
memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
|
|
1641
|
+
glm: glm52RuntimeFlags(rt.serverOptions),
|
|
1532
1642
|
...(draftModelDir || draftKind === "ngram"
|
|
1533
1643
|
? {
|
|
1534
1644
|
...(draftModelDir ? { draftModelDir } : {}),
|
|
@@ -1585,7 +1695,12 @@ switch (cmd) {
|
|
|
1585
1695
|
process.exit(1);
|
|
1586
1696
|
}
|
|
1587
1697
|
const { loadTaskModel, generateText } = await import("./eval/runner");
|
|
1588
|
-
const
|
|
1698
|
+
const maxTokens = Number(opt("max-tokens", "256"));
|
|
1699
|
+
const tm = await loadTaskModel(
|
|
1700
|
+
positional(0) ?? opt("query") ?? "",
|
|
1701
|
+
undefined,
|
|
1702
|
+
{ maxGenerationTokens: maxTokens, enableMtp: false },
|
|
1703
|
+
);
|
|
1589
1704
|
// Resolve the tier/--kv-quant KV scheme exactly the way serve does
|
|
1590
1705
|
// (server.ts kvScheme): "config" = the model's kv_config.json (absent →
|
|
1591
1706
|
// bf16, same silent fallback as serve), N = uniform bits from decode
|
|
@@ -1601,7 +1716,7 @@ switch (cmd) {
|
|
|
1601
1716
|
: undefined;
|
|
1602
1717
|
const num = (n: string): number | undefined => { const v = opt(n); return v == null ? undefined : Number(v); };
|
|
1603
1718
|
const text = await generateText(tm, prompt, {
|
|
1604
|
-
maxTokens
|
|
1719
|
+
maxTokens,
|
|
1605
1720
|
useChat: !flag("raw"),
|
|
1606
1721
|
...(kvScheme !== undefined ? { kvScheme } : {}),
|
|
1607
1722
|
sampler: { temperature: num("temperature") ?? num("temp"), topP: num("top-p"), topK: num("top-k"), seed: num("seed") },
|
|
@@ -1883,6 +1998,7 @@ switch (cmd) {
|
|
|
1883
1998
|
"--draft-model", "--num-draft-tokens", "--draft-kind", "--ngram-max", "--ngram-min",
|
|
1884
1999
|
"--compiled-decode", "--compiled-activations", "--fused-sdpa", "--thinking",
|
|
1885
2000
|
"--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
|
|
2001
|
+
"--mtp", "--context-length",
|
|
1886
2002
|
"--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
|
|
1887
2003
|
"--paged-kv-block-size",
|
|
1888
2004
|
]);
|
|
@@ -1948,7 +2064,10 @@ switch (cmd) {
|
|
|
1948
2064
|
const { createServer, loadContext } = await import("./server");
|
|
1949
2065
|
const sLoad = step(`loading ${m.repoId}`);
|
|
1950
2066
|
const t0 = performance.now();
|
|
1951
|
-
const ctx = await loadContext(m.path, m.repoId, {
|
|
2067
|
+
const ctx = await loadContext(m.path, m.repoId, {
|
|
2068
|
+
memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
|
|
2069
|
+
glm: glm52RuntimeFlags(rt.serverOptions),
|
|
2070
|
+
});
|
|
1952
2071
|
await mountStartupAdapter(ctx, rt.serverOptions);
|
|
1953
2072
|
createServer(ctx, port, { ...rt.serverOptions, owner: "pi-session" });
|
|
1954
2073
|
startedServer = true;
|
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>;
|
|
@@ -238,32 +255,44 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
238
255
|
const qwenRope = (t.rope_parameters ?? {}) as Record<string, any>;
|
|
239
256
|
|
|
240
257
|
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
|
-
|
|
258
|
+
hiddenSize: glm52?.hiddenSize ?? t.hidden_size,
|
|
259
|
+
numHiddenLayers: glm52?.numHiddenLayers ?? t.num_hidden_layers,
|
|
260
|
+
numAttentionHeads: glm52?.numAttentionHeads ?? t.num_attention_heads,
|
|
261
|
+
numKeyValueHeads: glm52?.numKeyValueHeads ?? t.num_key_value_heads,
|
|
262
|
+
headDim: glm52?.qkHeadDim ?? t.head_dim ?? Math.floor(t.hidden_size / t.num_attention_heads),
|
|
263
|
+
numGlobalKeyValueHeads:
|
|
264
|
+
glm52?.numKeyValueHeads ?? t.num_global_key_value_heads ?? t.num_key_value_heads,
|
|
265
|
+
globalHeadDim:
|
|
266
|
+
glm52?.qkHeadDim ??
|
|
267
|
+
t.global_head_dim ??
|
|
268
|
+
t.head_dim ??
|
|
269
|
+
Math.floor(t.hidden_size / t.num_attention_heads),
|
|
248
270
|
attentionKEqV: t.attention_k_eq_v ?? false,
|
|
249
|
-
intermediateSize: t.intermediate_size,
|
|
271
|
+
intermediateSize: glm52?.intermediateSize ?? t.intermediate_size,
|
|
250
272
|
hiddenActivation: t.hidden_activation ?? t.hidden_act ?? "gelu_pytorch_tanh",
|
|
251
|
-
rmsNormEps: t.rms_norm_eps,
|
|
252
|
-
vocabSize: t.vocab_size,
|
|
253
|
-
maxPositionEmbeddings:
|
|
273
|
+
rmsNormEps: glm52?.rmsNormEps ?? t.rms_norm_eps,
|
|
274
|
+
vocabSize: glm52?.vocabSize ?? t.vocab_size,
|
|
275
|
+
maxPositionEmbeddings:
|
|
276
|
+
glm52?.maxPositionEmbeddings ?? t.max_position_embeddings,
|
|
254
277
|
slidingWindow: t.sliding_window ?? 0,
|
|
255
|
-
layerTypes:
|
|
278
|
+
layerTypes: glm52
|
|
279
|
+
? Array(glm52.numHiddenLayers).fill("full_attention")
|
|
280
|
+
: t.layer_types ?? (isLlama ? Array(t.num_hidden_layers).fill("full_attention") : []),
|
|
256
281
|
hiddenSizePerLayerInput: t.hidden_size_per_layer_input ?? 0,
|
|
257
|
-
vocabSizePerLayerInput:
|
|
282
|
+
vocabSizePerLayerInput:
|
|
283
|
+
t.vocab_size_per_layer_input ?? glm52?.vocabSize ?? t.vocab_size,
|
|
258
284
|
numKvSharedLayers: t.num_kv_shared_layers ?? 0,
|
|
259
|
-
enableMoeBlock: t.enable_moe_block ?? false,
|
|
260
|
-
numExperts: t.num_experts ?? 0,
|
|
285
|
+
enableMoeBlock: glm52 ? true : t.enable_moe_block ?? false,
|
|
286
|
+
numExperts: glm52?.numRoutedExperts ?? t.num_experts ?? 0,
|
|
261
287
|
// gemma4 uses `top_k_experts`; qwen3_moe uses `num_experts_per_tok`.
|
|
262
|
-
topKExperts:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
288
|
+
topKExperts:
|
|
289
|
+
glm52?.numExpertsPerToken ?? t.top_k_experts ?? t.num_experts_per_tok ?? 0,
|
|
290
|
+
moeIntermediateSize: glm52?.moeIntermediateSize ?? t.moe_intermediate_size ?? 0,
|
|
291
|
+
decoderSparseStep: glm52 ? 1 : t.decoder_sparse_step ?? 1,
|
|
292
|
+
mlpOnlyLayers: glm52
|
|
293
|
+
? Array.from({ length: glm52.firstKDenseReplace }, (_, layer) => layer)
|
|
294
|
+
: t.mlp_only_layers ?? [],
|
|
295
|
+
normTopkProb: glm52?.normTopkProb ?? t.norm_topk_prob ?? false,
|
|
267
296
|
linearNumValueHeads: t.linear_num_value_heads ?? 0,
|
|
268
297
|
linearNumKeyHeads: t.linear_num_key_heads ?? 0,
|
|
269
298
|
linearKeyHeadDim: t.linear_key_head_dim ?? 0,
|
|
@@ -271,9 +300,19 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
271
300
|
linearConvKernelDim: t.linear_conv_kernel_dim ?? 0,
|
|
272
301
|
fullAttentionInterval: t.full_attention_interval ?? 0,
|
|
273
302
|
attnOutputGate: t.attn_output_gate ?? false,
|
|
274
|
-
partialRotaryFactor:
|
|
275
|
-
|
|
276
|
-
|
|
303
|
+
partialRotaryFactor: glm52
|
|
304
|
+
? glm52.qkRopeHeadDim / glm52.qkHeadDim
|
|
305
|
+
: qwenRope.partial_rotary_factor ?? t.partial_rotary_factor ?? 1.0,
|
|
306
|
+
ropeParameters: glm52
|
|
307
|
+
? {
|
|
308
|
+
full_attention: {
|
|
309
|
+
ropeTheta: glm52.ropeTheta,
|
|
310
|
+
ropeType: "default",
|
|
311
|
+
partialRotaryFactor: glm52.qkRopeHeadDim / glm52.qkHeadDim,
|
|
312
|
+
factor: 1.0,
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: isQwen35
|
|
277
316
|
? {
|
|
278
317
|
full_attention: {
|
|
279
318
|
ropeTheta: qwenRope.rope_theta ?? t.rope_theta ?? 10000,
|
|
@@ -297,7 +336,7 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
297
336
|
finalLogitSoftcapping: t.final_logit_softcapping ?? null,
|
|
298
337
|
tieWordEmbeddings: t.tie_word_embeddings ?? raw.tie_word_embeddings ?? false,
|
|
299
338
|
bosTokenId: t.bos_token_id ?? raw.bos_token_id,
|
|
300
|
-
eosTokenId: t.eos_token_id ?? raw.eos_token_id,
|
|
339
|
+
eosTokenId: glm52?.eosTokenIds ?? t.eos_token_id ?? raw.eos_token_id,
|
|
301
340
|
canvasLength: raw.canvas_length ?? t.canvas_length,
|
|
302
341
|
};
|
|
303
342
|
|
|
@@ -311,14 +350,20 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
311
350
|
}));
|
|
312
351
|
}
|
|
313
352
|
|
|
314
|
-
const eos = raw.eos_token_id ?? text.eosTokenId;
|
|
353
|
+
const eos = glm52?.eosTokenIds ?? raw.eos_token_id ?? text.eosTokenId;
|
|
315
354
|
return {
|
|
316
355
|
modelDir,
|
|
317
356
|
modelType,
|
|
318
357
|
architectures: raw.architectures ?? [],
|
|
319
358
|
dtype: raw.dtype ?? "bfloat16",
|
|
320
359
|
text,
|
|
321
|
-
|
|
360
|
+
// Colibri's config retains the SOURCE FP8 quantization_config, while the
|
|
361
|
+
// serving shards are converted U8 + `.qs` tensors whose int4/int8/group
|
|
362
|
+
// geometry is inferred and validated by Glm52Container. Advertising the
|
|
363
|
+
// source FP8 block as mlx affine quantization would be actively wrong.
|
|
364
|
+
quantization: glm52
|
|
365
|
+
? null
|
|
366
|
+
: parseQuantization(raw.quantization ?? raw.quantization_config),
|
|
322
367
|
kvQuant,
|
|
323
368
|
hasVisionSidecar: await Bun.file(`${modelDir}/optiq_vision.safetensors`).exists(),
|
|
324
369
|
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;
|