@mlx-node/lm 0.0.12 → 0.0.15
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/dist/chat-session.d.ts +1 -1
- package/dist/chat-session.d.ts.map +1 -1
- package/dist/chat-session.js +2 -2
- package/dist/draft-companion.d.ts +16 -0
- package/dist/draft-companion.d.ts.map +1 -0
- package/dist/draft-companion.js +76 -0
- package/dist/family-data.d.ts +2 -0
- package/dist/family-data.d.ts.map +1 -1
- package/dist/family-data.js +2 -0
- package/dist/gguf-metadata.d.ts +2 -0
- package/dist/gguf-metadata.d.ts.map +1 -0
- package/dist/gguf-metadata.js +128 -0
- package/dist/model-detection.d.ts +6 -0
- package/dist/model-detection.d.ts.map +1 -0
- package/dist/model-detection.js +38 -0
- package/dist/model-discovery.d.ts +24 -0
- package/dist/model-discovery.d.ts.map +1 -0
- package/dist/model-discovery.js +274 -0
- package/dist/models/model-loader.d.ts +6 -0
- package/dist/models/model-loader.d.ts.map +1 -1
- package/dist/models/model-loader.js +10 -32
- package/dist/models/paged-config-override.d.ts.map +1 -1
- package/dist/models/paged-config-override.js +21 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +5 -5
- package/package.json +21 -3
- package/src/chat-session.ts +2369 -0
- package/src/draft-companion.ts +74 -0
- package/src/family-data.ts +542 -0
- package/src/gguf-metadata.ts +117 -0
- package/src/index.ts +151 -0
- package/src/model-detection.ts +46 -0
- package/src/model-discovery.ts +329 -0
- package/src/models/lfm2-configs.ts +110 -0
- package/src/models/model-loader.ts +256 -0
- package/src/models/paged-config-override.ts +387 -0
- package/src/models/qwen3-configs.ts +113 -0
- package/src/models/qwen3_5-configs.ts +60 -0
- package/src/profiling.ts +69 -0
- package/src/stream.ts +960 -0
- package/src/tools/index.ts +58 -0
- package/src/tools/types.ts +215 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Lfm2Config } from '@mlx-node/core';
|
|
2
|
+
|
|
3
|
+
export const LFM2_CONFIGS: { [key: string]: Lfm2Config } = {
|
|
4
|
+
'lfm2.5-8b-a1b': {
|
|
5
|
+
vocabSize: 128000,
|
|
6
|
+
hiddenSize: 2048,
|
|
7
|
+
numHiddenLayers: 24,
|
|
8
|
+
numAttentionHeads: 32,
|
|
9
|
+
numKeyValueHeads: 8,
|
|
10
|
+
maxPositionEmbeddings: 128000,
|
|
11
|
+
normEps: 1e-5,
|
|
12
|
+
convBias: false,
|
|
13
|
+
convLCache: 3,
|
|
14
|
+
// block* fields are unused for MoE checkpoints (MoE dense layers use
|
|
15
|
+
// intermediateSize directly; MoE sparse layers use moeIntermediateSize).
|
|
16
|
+
// Required by the NAPI-derived Lfm2Config interface; set to 0 / defaults.
|
|
17
|
+
blockDim: 0,
|
|
18
|
+
blockFfDim: 0,
|
|
19
|
+
blockMultipleOf: 256,
|
|
20
|
+
blockFfnDimMultiplier: 1.0,
|
|
21
|
+
blockAutoAdjustFfDim: false,
|
|
22
|
+
ropeTheta: 5000000.0,
|
|
23
|
+
layerTypes: [
|
|
24
|
+
'conv',
|
|
25
|
+
'conv',
|
|
26
|
+
'full_attention',
|
|
27
|
+
'conv',
|
|
28
|
+
'conv',
|
|
29
|
+
'conv',
|
|
30
|
+
'full_attention',
|
|
31
|
+
'conv',
|
|
32
|
+
'conv',
|
|
33
|
+
'conv',
|
|
34
|
+
'full_attention',
|
|
35
|
+
'conv',
|
|
36
|
+
'conv',
|
|
37
|
+
'conv',
|
|
38
|
+
'full_attention',
|
|
39
|
+
'conv',
|
|
40
|
+
'conv',
|
|
41
|
+
'conv',
|
|
42
|
+
'full_attention',
|
|
43
|
+
'conv',
|
|
44
|
+
'conv',
|
|
45
|
+
'full_attention',
|
|
46
|
+
'conv',
|
|
47
|
+
'conv',
|
|
48
|
+
],
|
|
49
|
+
tieEmbedding: true,
|
|
50
|
+
eosTokenId: 124900,
|
|
51
|
+
bosTokenId: 124894,
|
|
52
|
+
padTokenId: 124893,
|
|
53
|
+
intermediateSize: 7168,
|
|
54
|
+
moeIntermediateSize: 1792,
|
|
55
|
+
numExperts: 32,
|
|
56
|
+
numExpertsPerTok: 4,
|
|
57
|
+
numDenseLayers: 2,
|
|
58
|
+
normTopkProb: true,
|
|
59
|
+
useExpertBias: true,
|
|
60
|
+
},
|
|
61
|
+
'lfm2.5-1.2b-thinking': {
|
|
62
|
+
vocabSize: 65536,
|
|
63
|
+
hiddenSize: 2048,
|
|
64
|
+
numHiddenLayers: 16,
|
|
65
|
+
numAttentionHeads: 32,
|
|
66
|
+
numKeyValueHeads: 8,
|
|
67
|
+
maxPositionEmbeddings: 128000,
|
|
68
|
+
normEps: 1e-5,
|
|
69
|
+
convBias: false,
|
|
70
|
+
convLCache: 3,
|
|
71
|
+
blockDim: 2048,
|
|
72
|
+
blockFfDim: 12288,
|
|
73
|
+
blockMultipleOf: 256,
|
|
74
|
+
blockFfnDimMultiplier: 1.0,
|
|
75
|
+
blockAutoAdjustFfDim: true,
|
|
76
|
+
ropeTheta: 1000000.0,
|
|
77
|
+
layerTypes: [
|
|
78
|
+
'conv',
|
|
79
|
+
'conv',
|
|
80
|
+
'full_attention',
|
|
81
|
+
'conv',
|
|
82
|
+
'conv',
|
|
83
|
+
'full_attention',
|
|
84
|
+
'conv',
|
|
85
|
+
'conv',
|
|
86
|
+
'full_attention',
|
|
87
|
+
'conv',
|
|
88
|
+
'full_attention',
|
|
89
|
+
'conv',
|
|
90
|
+
'full_attention',
|
|
91
|
+
'conv',
|
|
92
|
+
'full_attention',
|
|
93
|
+
'conv',
|
|
94
|
+
],
|
|
95
|
+
tieEmbedding: true,
|
|
96
|
+
eosTokenId: 7,
|
|
97
|
+
bosTokenId: 1,
|
|
98
|
+
padTokenId: 0,
|
|
99
|
+
normTopkProb: true,
|
|
100
|
+
useExpertBias: true,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function getLfm2Config(name: string): Lfm2Config {
|
|
105
|
+
const config = LFM2_CONFIGS[name];
|
|
106
|
+
if (!config) {
|
|
107
|
+
throw new Error(`Unknown LFM2 config: ${name}. Available: ${Object.keys(LFM2_CONFIGS).join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native half of the family registry: one loader binding per
|
|
3
|
+
* `MODEL_FAMILY_DATA` row, plus `detectModelType` (filesystem + GGUF).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Gemma4Model as NativeGemma4Model,
|
|
8
|
+
ggufArchitecture,
|
|
9
|
+
HarrierModel,
|
|
10
|
+
Lfm2Model as NativeLfm2Model,
|
|
11
|
+
MuseGlimmerModel as NativeMuseGlimmerModel,
|
|
12
|
+
NemotronHModel as NativeNemotronHModel,
|
|
13
|
+
QianfanOCRModel,
|
|
14
|
+
Qwen3Model as NativeQwen3Model,
|
|
15
|
+
Qwen35Model as NativeQwen35Model,
|
|
16
|
+
Qwen35MoeModel as NativeQwen35MoeModel,
|
|
17
|
+
} from '@mlx-node/core';
|
|
18
|
+
|
|
19
|
+
import { ChatSession, type SessionCapableModel } from '../chat-session.js';
|
|
20
|
+
import { findDFlash2Draft } from '../draft-companion.js';
|
|
21
|
+
import { familyDataFor, type ModelType, type TrainableFamilyId } from '../family-data.js';
|
|
22
|
+
import { detectModelType as detectLocalModelType } from '../model-detection.js';
|
|
23
|
+
import {
|
|
24
|
+
Gemma4Model,
|
|
25
|
+
Lfm2Model,
|
|
26
|
+
MuseGlimmerModel,
|
|
27
|
+
NemotronHModel,
|
|
28
|
+
Qwen3Model,
|
|
29
|
+
Qwen35Model,
|
|
30
|
+
Qwen35MoeModel,
|
|
31
|
+
} from '../stream.js';
|
|
32
|
+
|
|
33
|
+
/** Optional settings for {@link loadModel} / {@link loadSession}. */
|
|
34
|
+
export interface LoadModelOptions {
|
|
35
|
+
/** Discover a Qwen DFlash2 companion on disk (default true). Explicit draftModelPath wins. */
|
|
36
|
+
autoLoadDraft?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Directory of an external draft checkpoint (config.json +
|
|
39
|
+
* model.safetensors) loaded alongside the target for speculative decoding.
|
|
40
|
+
* Gemma4 accepts either a DSpark draft or a Google gemma-4 assistant draft
|
|
41
|
+
* (`google/gemma-4-*-it-assistant`); the variant is auto-detected from
|
|
42
|
+
* the draft's config.json (`model_type` `gemma4_assistant` /
|
|
43
|
+
* `gemma4_unified_assistant` → assistant, `architectures` containing
|
|
44
|
+
* `Gemma4DSparkModel` → DSpark). When omitted, Gemma4 automatically loads
|
|
45
|
+
* an embedded draft from `<modelPath>/draft/` when present. Draft decoding
|
|
46
|
+
* runs on the flat KV-cache path, so the target checkpoint must not
|
|
47
|
+
* explicitly enable `use_block_paged_cache`.
|
|
48
|
+
*
|
|
49
|
+
* Dense `qwen3_5` accepts a z-lab `DFlash2DraftModel` companion such as
|
|
50
|
+
* `z-lab/Qwen3.8-27B-DFlash2`. It shares the Qwen3.8 target embedding and
|
|
51
|
+
* LM head, validates all companion tensors at load time, and takes
|
|
52
|
+
* precedence over an inline target MTP head. Other model families reject
|
|
53
|
+
* this option.
|
|
54
|
+
*/
|
|
55
|
+
draftModelPath?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type NativeModelClass = abstract new (...args: never[]) => object;
|
|
59
|
+
|
|
60
|
+
interface LoaderBinding {
|
|
61
|
+
readonly load: (modelPath: string, options?: LoadModelOptions) => Promise<unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Native `@mlx-node/core` class behind this family. The public
|
|
64
|
+
* `LoadableModel` / `TrainableModel` unions derive from these classes —
|
|
65
|
+
* NOT from the streaming-wrapper types the loaders return — so native
|
|
66
|
+
* instances stay assignable and trainers can pass them directly to the
|
|
67
|
+
* Rust engine factory methods without type conflicts. Loaded wrapper
|
|
68
|
+
* instances are runtime subclasses of their native class, so
|
|
69
|
+
* `instanceof` narrowing against these classes still works on
|
|
70
|
+
* `loadModel` results.
|
|
71
|
+
*/
|
|
72
|
+
readonly nativeModelClass: NativeModelClass;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Native half of the family registry: one loader + native class per
|
|
77
|
+
* `MODEL_FAMILY_DATA` row (the native-free half in `../family-data.ts`).
|
|
78
|
+
* `satisfies Record<ModelType, LoaderBinding>` makes the zip exhaustive both
|
|
79
|
+
* ways — a data row without a binding, or a binding without a row, fails to
|
|
80
|
+
* compile.
|
|
81
|
+
*/
|
|
82
|
+
const LOADER_BINDINGS = {
|
|
83
|
+
gemma4: {
|
|
84
|
+
load: (modelPath: string, options?: LoadModelOptions) =>
|
|
85
|
+
Gemma4Model.load(
|
|
86
|
+
modelPath,
|
|
87
|
+
options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath },
|
|
88
|
+
),
|
|
89
|
+
nativeModelClass: NativeGemma4Model,
|
|
90
|
+
},
|
|
91
|
+
muse_glimmer: {
|
|
92
|
+
load: (modelPath: string) => MuseGlimmerModel.load(modelPath),
|
|
93
|
+
nativeModelClass: NativeMuseGlimmerModel,
|
|
94
|
+
},
|
|
95
|
+
harrier: {
|
|
96
|
+
load: (modelPath: string) => HarrierModel.load(modelPath),
|
|
97
|
+
nativeModelClass: HarrierModel,
|
|
98
|
+
},
|
|
99
|
+
qwen3: {
|
|
100
|
+
load: (modelPath: string) => Qwen3Model.load(modelPath),
|
|
101
|
+
nativeModelClass: NativeQwen3Model,
|
|
102
|
+
},
|
|
103
|
+
qwen3_5: {
|
|
104
|
+
load: (modelPath: string, options?: LoadModelOptions) =>
|
|
105
|
+
Qwen35Model.load(
|
|
106
|
+
modelPath,
|
|
107
|
+
options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath },
|
|
108
|
+
),
|
|
109
|
+
nativeModelClass: NativeQwen35Model,
|
|
110
|
+
},
|
|
111
|
+
qwen3_5_moe: {
|
|
112
|
+
load: (modelPath: string) => Qwen35MoeModel.load(modelPath),
|
|
113
|
+
nativeModelClass: NativeQwen35MoeModel,
|
|
114
|
+
},
|
|
115
|
+
lfm2: {
|
|
116
|
+
load: (modelPath: string) => Lfm2Model.load(modelPath),
|
|
117
|
+
nativeModelClass: NativeLfm2Model,
|
|
118
|
+
},
|
|
119
|
+
lfm2_moe: {
|
|
120
|
+
load: (modelPath: string) => Lfm2Model.load(modelPath),
|
|
121
|
+
nativeModelClass: NativeLfm2Model,
|
|
122
|
+
},
|
|
123
|
+
nemotron_h: {
|
|
124
|
+
load: (modelPath: string) => NemotronHModel.load(modelPath),
|
|
125
|
+
nativeModelClass: NativeNemotronHModel,
|
|
126
|
+
},
|
|
127
|
+
internvl_chat: {
|
|
128
|
+
load: (modelPath: string) => QianfanOCRModel.load(modelPath),
|
|
129
|
+
nativeModelClass: QianfanOCRModel,
|
|
130
|
+
},
|
|
131
|
+
'qianfan-ocr': {
|
|
132
|
+
load: (modelPath: string) => QianfanOCRModel.load(modelPath),
|
|
133
|
+
nativeModelClass: QianfanOCRModel,
|
|
134
|
+
},
|
|
135
|
+
} as const satisfies Record<ModelType, LoaderBinding>;
|
|
136
|
+
|
|
137
|
+
type LoaderBindings = typeof LOADER_BINDINGS;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Union of the native `@mlx-node/core` model classes across every registered
|
|
141
|
+
* family — the public contract of {@link loadModel}. At runtime the chat
|
|
142
|
+
* families resolve to streaming-wrapper subclasses of these classes
|
|
143
|
+
* (AsyncGenerator `chatStream*` overrides), but the public type names the
|
|
144
|
+
* native classes so downstream code can pass instances directly to Rust
|
|
145
|
+
* engine factory methods without type conflicts.
|
|
146
|
+
*/
|
|
147
|
+
export type LoadableModel = InstanceType<LoaderBindings[ModelType]['nativeModelClass']>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Union accepted by trainer APIs: registered wrapper results plus their native
|
|
151
|
+
* FFI instances. Both sides derive from the same trainable family ids.
|
|
152
|
+
*/
|
|
153
|
+
export type TrainableModel =
|
|
154
|
+
| Awaited<ReturnType<LoaderBindings[TrainableFamilyId]['load']>>
|
|
155
|
+
| InstanceType<LoaderBindings[TrainableFamilyId]['nativeModelClass']>;
|
|
156
|
+
|
|
157
|
+
function requireFamilyData(modelType: ModelType) {
|
|
158
|
+
const family = familyDataFor(modelType);
|
|
159
|
+
if (family === undefined) {
|
|
160
|
+
throw new Error(`Internal error: missing model family descriptor for "${modelType}"`);
|
|
161
|
+
}
|
|
162
|
+
return family;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Dispatch a load through the registry, validating draft-capable families.
|
|
167
|
+
* `draftModelPath` reaches only gemma4 and dense qwen3_5; every other family
|
|
168
|
+
* rejects it loudly instead of silently ignoring speculative-decode intent.
|
|
169
|
+
*/
|
|
170
|
+
function dispatchLoad(
|
|
171
|
+
modelType: ModelType,
|
|
172
|
+
modelPath: string,
|
|
173
|
+
options: LoadModelOptions | undefined,
|
|
174
|
+
): Promise<unknown> {
|
|
175
|
+
if (options?.draftModelPath !== undefined && requireFamilyData(modelType).acceptsDraftModel !== true) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`draftModelPath (speculative-decoding draft) is only supported by gemma4 and qwen3_5 models; ` +
|
|
178
|
+
`${modelPath} has model_type "${modelType}"`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const binding: LoaderBinding = LOADER_BINDINGS[modelType];
|
|
182
|
+
const draftModelPath =
|
|
183
|
+
options?.draftModelPath ?? (options?.autoLoadDraft === false ? undefined : findDFlash2Draft(modelPath, modelType));
|
|
184
|
+
return binding.load(modelPath, draftModelPath === undefined ? options : { ...options, draftModelPath });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Load a model from disk, auto-detecting architecture from config.json.
|
|
189
|
+
*
|
|
190
|
+
* Supports both language models (Qwen3, Qwen3.5) and vision-language models
|
|
191
|
+
* (Qianfan-OCR / InternVL). Use `instanceof` to narrow the returned type.
|
|
192
|
+
*
|
|
193
|
+
* `options.draftModelPath` attaches an external draft checkpoint for
|
|
194
|
+
* speculative decoding — gemma4 and dense qwen3_5 only; every other family
|
|
195
|
+
* rejects it.
|
|
196
|
+
* Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
|
|
197
|
+
* that embedded checkpoint is present.
|
|
198
|
+
* Qwen also discovers `draft/` or a shared `qwen3.8-27b-dflash2` directory
|
|
199
|
+
* beside a Qwen3.8-27B target. Set `autoLoadDraft: false` to disable discovery.
|
|
200
|
+
*/
|
|
201
|
+
export async function loadModel(modelPath: string, options?: LoadModelOptions): Promise<LoadableModel> {
|
|
202
|
+
const modelType = await detectModelType(modelPath);
|
|
203
|
+
return dispatchLoad(modelType, modelPath, options) as Promise<LoadableModel>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Load a model and wrap it in a {@link ChatSession} for multi-turn chat.
|
|
208
|
+
*
|
|
209
|
+
* Convenience around `loadModel()` + `new ChatSession(model)` for the
|
|
210
|
+
* common case where a caller just wants an ergonomic session handle.
|
|
211
|
+
*
|
|
212
|
+
* Rejects models that cannot be driven by a `ChatSession`:
|
|
213
|
+
* - Embedding models (`HarrierModel`) have no chat surface.
|
|
214
|
+
* - The native `QianfanOCRModel` exposes callback-based streaming
|
|
215
|
+
* methods that do not structurally satisfy `SessionCapableModel`'s
|
|
216
|
+
* `AsyncGenerator` overloads. The VLM AsyncGenerator wrapper lives
|
|
217
|
+
* in `@mlx-node/vlm` (importing it here would create a circular
|
|
218
|
+
* package dependency), so callers who want a Qianfan-OCR session
|
|
219
|
+
* must import `QianfanOCRModel` from `@mlx-node/vlm` and construct
|
|
220
|
+
* `new ChatSession(model)` directly.
|
|
221
|
+
*
|
|
222
|
+
* `options.draftModelPath` attaches an external draft checkpoint for
|
|
223
|
+
* speculative decoding — gemma4 and dense qwen3_5 only; every other family
|
|
224
|
+
* rejects it.
|
|
225
|
+
* Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
|
|
226
|
+
* that embedded checkpoint is present.
|
|
227
|
+
* Qwen uses the same companion discovery as `loadModel()` unless
|
|
228
|
+
* `autoLoadDraft: false` is passed.
|
|
229
|
+
* The resulting session auto-enables the speculative path when the model
|
|
230
|
+
* reports `hasMtpWeights()` AND does not opt out of the auto-default; pass
|
|
231
|
+
* `enableMtp: false` per call to suppress it, or `enableMtp: true` to force
|
|
232
|
+
* it on a family that opts out. NemotronH opts out (`mtpAutoEnabled()`
|
|
233
|
+
* returns false) because forcing MTP moves the turn into the exclusive lane
|
|
234
|
+
* and out of continuous batching; see `ChatSession.mtpAutoDefaultAllowed`.
|
|
235
|
+
*/
|
|
236
|
+
export async function loadSession(
|
|
237
|
+
modelPath: string,
|
|
238
|
+
options?: LoadModelOptions,
|
|
239
|
+
): Promise<ChatSession<SessionCapableModel>> {
|
|
240
|
+
const modelType = await detectModelType(modelPath);
|
|
241
|
+
const kind = requireFamilyData(modelType).kind;
|
|
242
|
+
if (kind === 'embedding') {
|
|
243
|
+
throw new Error('loadSession: embedding models (Harrier) cannot be wrapped in a ChatSession');
|
|
244
|
+
}
|
|
245
|
+
if (kind === 'vlm') {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'loadSession: Qianfan-OCR / InternVL session support lives in @mlx-node/vlm. Import QianfanOCRModel from @mlx-node/vlm and construct ChatSession(model) directly.',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const m = await dispatchLoad(modelType, modelPath, options);
|
|
251
|
+
return new ChatSession(m as unknown as SessionCapableModel);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function detectModelType(modelPath: string): Promise<ModelType> {
|
|
255
|
+
return detectLocalModelType(modelPath, ggufArchitecture);
|
|
256
|
+
}
|