mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// Native model quantizer: walk a model directory's weights, quantize every
|
|
2
|
+
// eligible Linear / embedding / lm_head weight to uniform affine N-bit, pass
|
|
3
|
+
// everything else through unchanged, and write a loadable quantized snapshot.
|
|
4
|
+
//
|
|
5
|
+
// No Python. The whole pipeline (load → dequantize-if-needed → cast bf16 →
|
|
6
|
+
// mx.quantize → save) runs through the mlx-c FFI on the CPU stream where the
|
|
7
|
+
// safetensors loader lives.
|
|
8
|
+
//
|
|
9
|
+
// Memory discipline: one module is materialized, quantized, written, and
|
|
10
|
+
// disposed at a time; the allocator cache is cleared every CACHE_CLEAR_EVERY
|
|
11
|
+
// modules so a large model never holds more than a few resident tensors.
|
|
12
|
+
//
|
|
13
|
+
// Uniform affine (every eligible weight gets the same bits/group_size) is the
|
|
14
|
+
// default. When `opts.targetBpw` is set, the OptiQ sensitivity-driven
|
|
15
|
+
// mixed-precision path runs: calibration → per-layer KL sensitivity → greedy
|
|
16
|
+
// knapsack → a heterogeneous per-module bit allocation fed to the same writer.
|
|
17
|
+
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { mkdirSync } from "node:fs";
|
|
20
|
+
import { clearCache, Dtype } from "../mlx/ffi";
|
|
21
|
+
import { cpuStream, type MlxArray } from "../mlx/array";
|
|
22
|
+
import { quantize, dequantize } from "../mlx/ops";
|
|
23
|
+
import { Weights } from "../weights";
|
|
24
|
+
import { loadModelConfig, quantFor, type QuantizationConfig } from "../config";
|
|
25
|
+
import { createModel } from "../model/factory";
|
|
26
|
+
import { loadTokenizer } from "../tokenizer";
|
|
27
|
+
import {
|
|
28
|
+
writeShardedSafetensors,
|
|
29
|
+
type NamedTensor,
|
|
30
|
+
type WriteResult,
|
|
31
|
+
} from "./safetensors-writer";
|
|
32
|
+
import {
|
|
33
|
+
buildQuantizationBlock,
|
|
34
|
+
writeQuantizedConfig,
|
|
35
|
+
type PerLayerEntry,
|
|
36
|
+
} from "./config-writer";
|
|
37
|
+
import { loadLlmCalibration } from "./calibration";
|
|
38
|
+
import { analyzeSensitivityExact, type SensitivityResult } from "./sensitivity";
|
|
39
|
+
import { optimizeMixedPrecision } from "./allocator";
|
|
40
|
+
|
|
41
|
+
/** Quantize the allocator cache this often (in modules processed). */
|
|
42
|
+
const CACHE_CLEAR_EVERY = 16;
|
|
43
|
+
|
|
44
|
+
/** Bits used per group for affine scale + bias (one bf16 each = 32 bits). */
|
|
45
|
+
const AFFINE_GROUP_OVERHEAD_BITS = 32;
|
|
46
|
+
|
|
47
|
+
export interface QuantizeOptions {
|
|
48
|
+
bits: 4 | 8;
|
|
49
|
+
groupSize: 32 | 64;
|
|
50
|
+
/** Quantization scheme; v1 only exercises "affine". */
|
|
51
|
+
mode?: string;
|
|
52
|
+
|
|
53
|
+
// --- mixed-precision (OptiQ sensitivity + knapsack) -------------------
|
|
54
|
+
// Setting targetBpw switches quantizeModelDir to the mixed path: it loads the
|
|
55
|
+
// model, runs calibration + per-layer KL sensitivity, knapsacks a per-module
|
|
56
|
+
// bit allocation, and feeds that heterogeneous map to the same writer.
|
|
57
|
+
/** Target bits-per-weight for sensitivity+knapsack mixed precision. */
|
|
58
|
+
targetBpw?: number;
|
|
59
|
+
/** Reference dtype for sensitivity scoring (e.g. "bf16"). Informational
|
|
60
|
+
* only — the native path dequantizes the running model's own weights as
|
|
61
|
+
* the self-contained bf16 source. */
|
|
62
|
+
reference?: string;
|
|
63
|
+
/** Candidate bit-widths the knapsack may choose from (e.g. [4, 8]). */
|
|
64
|
+
candidateBits?: number[];
|
|
65
|
+
/** Calibration data mixture spec ("optiq" or a JSONL path). */
|
|
66
|
+
calibrationMix?: string;
|
|
67
|
+
/** Number of calibration samples (forward passes per layer×bit probe). */
|
|
68
|
+
nCalibration?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ProgressEvent {
|
|
72
|
+
stage: string;
|
|
73
|
+
message: string;
|
|
74
|
+
/** 0..1 fraction of modules processed. */
|
|
75
|
+
progress: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface QuantizeResult {
|
|
79
|
+
outDir: string;
|
|
80
|
+
/** Achieved bits-per-weight over the quantized parameters (incl overhead). */
|
|
81
|
+
achievedBpw: number;
|
|
82
|
+
/** Number of modules quantized. */
|
|
83
|
+
nQuantized: number;
|
|
84
|
+
/** Sharding / size details from the writer. */
|
|
85
|
+
write: WriteResult;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type Suffix = "weight" | "scales" | "biases";
|
|
89
|
+
|
|
90
|
+
/** Split a tensor name into (modulePath, suffix) for `.weight|.scales|.biases`. */
|
|
91
|
+
function splitName(name: string): { base: string; suffix: Suffix } | null {
|
|
92
|
+
const m = /^(.*)\.(weight|scales|biases)$/.exec(name);
|
|
93
|
+
if (!m) return null;
|
|
94
|
+
return { base: m[1]!, suffix: m[2] as Suffix };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Eligibility predicate (port of mlx-lm's default class_predicate): a module
|
|
99
|
+
* is quantizable iff it has a 2D `.weight` whose last dim is divisible by the
|
|
100
|
+
* group size. This captures Linear projections, the token embedding, and the
|
|
101
|
+
* (untied) lm_head, and excludes norms/biases/1D tensors.
|
|
102
|
+
*/
|
|
103
|
+
export function isQuantizable(weightShape: number[], groupSize: number): boolean {
|
|
104
|
+
return weightShape.length === 2 && weightShape[weightShape.length - 1]! % groupSize === 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Quantize a model directory and write a loadable snapshot to `outDir`.
|
|
109
|
+
*
|
|
110
|
+
* Uniform affine N-bit by default. When `opts.targetBpw` is set, runs the
|
|
111
|
+
* OptiQ mixed-precision path (calibration → sensitivity → knapsack) to choose
|
|
112
|
+
* per-module bit-widths before writing.
|
|
113
|
+
*/
|
|
114
|
+
export async function quantizeModelDir(
|
|
115
|
+
srcDir: string,
|
|
116
|
+
outDir: string,
|
|
117
|
+
opts: QuantizeOptions,
|
|
118
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
119
|
+
): Promise<QuantizeResult> {
|
|
120
|
+
const groupSize = opts.groupSize;
|
|
121
|
+
const mode = opts.mode ?? "affine";
|
|
122
|
+
|
|
123
|
+
// Mixed-precision path: derive a per-module bit allocation first, then write.
|
|
124
|
+
let perLayerBits: Map<string, number> | undefined;
|
|
125
|
+
let mixedMeta: MixedMeta | undefined;
|
|
126
|
+
if (opts.targetBpw !== undefined) {
|
|
127
|
+
const alloc = await computeMixedAllocation(srcDir, opts, onProgress);
|
|
128
|
+
perLayerBits = alloc.perLayerBits;
|
|
129
|
+
mixedMeta = alloc.meta;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const bits = opts.bits;
|
|
133
|
+
|
|
134
|
+
mkdirSync(outDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
const progress = (stage: string, message: string, frac: number) =>
|
|
137
|
+
onProgress?.({ stage, message, progress: frac });
|
|
138
|
+
|
|
139
|
+
progress("loading", `Reading ${srcDir}`, 0);
|
|
140
|
+
|
|
141
|
+
const config = await loadModelConfig(srcDir);
|
|
142
|
+
const srcQuant: QuantizationConfig | null = config.quantization;
|
|
143
|
+
const weights = await Weights.open(srcDir);
|
|
144
|
+
|
|
145
|
+
// Preserve the source tensor order so the output diff is mlx-lm-shaped.
|
|
146
|
+
// tensorNames is sorted; we walk modules in that order. For each module's
|
|
147
|
+
// `.weight` we decide quantize / pass-through, then emit its tensors. We
|
|
148
|
+
// skip the source `.scales`/`.biases` of already-quantized modules (they're
|
|
149
|
+
// regenerated) and pass through everything else verbatim.
|
|
150
|
+
const names = weights.tensorNames;
|
|
151
|
+
|
|
152
|
+
// Group names by module base so we can detect "already quantized" (has a
|
|
153
|
+
// sibling `.scales`) and process a module atomically.
|
|
154
|
+
const moduleSuffixes = new Map<string, Set<Suffix>>();
|
|
155
|
+
const passthroughNames: string[] = []; // non-(weight/scales/biases) tensors
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const sp = splitName(name);
|
|
158
|
+
if (!sp) {
|
|
159
|
+
passthroughNames.push(name);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
let set = moduleSuffixes.get(sp.base);
|
|
163
|
+
if (!set) {
|
|
164
|
+
set = new Set();
|
|
165
|
+
moduleSuffixes.set(sp.base, set);
|
|
166
|
+
}
|
|
167
|
+
set.add(sp.suffix);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Decide the per-module plan in source order.
|
|
171
|
+
const moduleBases = [...moduleSuffixes.keys()];
|
|
172
|
+
const totalModules = moduleBases.length;
|
|
173
|
+
|
|
174
|
+
const out: NamedTensor[] = [];
|
|
175
|
+
const perLayer = new Map<string, PerLayerEntry>();
|
|
176
|
+
let nQuantized = 0;
|
|
177
|
+
let quantizedParams = 0;
|
|
178
|
+
let quantizedBits = 0;
|
|
179
|
+
let processed = 0;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
for (const base of moduleBases) {
|
|
183
|
+
const suffixes = moduleSuffixes.get(base)!;
|
|
184
|
+
const alreadyQuant = suffixes.has("scales");
|
|
185
|
+
const hasWeight = suffixes.has("weight");
|
|
186
|
+
|
|
187
|
+
if (!hasWeight) {
|
|
188
|
+
// Module with only scales/biases and no weight — shouldn't happen, but
|
|
189
|
+
// pass any present tensors through unchanged.
|
|
190
|
+
for (const suf of suffixes) out.push(named(weights, `${base}.${suf}`));
|
|
191
|
+
bumpProgress();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Materialize the full-precision weight for this module.
|
|
196
|
+
const weightName = `${base}.weight`;
|
|
197
|
+
const shape = weights.info(weightName).shape;
|
|
198
|
+
|
|
199
|
+
// Determine the *original* (full-precision) shape: if already quantized,
|
|
200
|
+
// the packed weight's last dim is compressed — recover the real width
|
|
201
|
+
// from the scales (groups × srcGroupSize) when dequantizing.
|
|
202
|
+
let fullWeight = weights.tensor(weightName);
|
|
203
|
+
let materialized: MlxArray | null = null;
|
|
204
|
+
|
|
205
|
+
if (alreadyQuant) {
|
|
206
|
+
const srcSpec = quantFor(srcQuant, base);
|
|
207
|
+
if (!srcSpec) {
|
|
208
|
+
// Has scales but config says unquantized — fall back to passthrough.
|
|
209
|
+
for (const suf of suffixes) out.push(named(weights, `${base}.${suf}`));
|
|
210
|
+
bumpProgress();
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const scales = weights.tensor(`${base}.scales`);
|
|
214
|
+
const biases = suffixes.has("biases") ? weights.tensor(`${base}.biases`) : null;
|
|
215
|
+
materialized = dequantize(
|
|
216
|
+
fullWeight, scales, biases,
|
|
217
|
+
{ bits: srcSpec.bits, groupSize: srcSpec.groupSize, mode: srcSpec.mode },
|
|
218
|
+
cpuStream,
|
|
219
|
+
);
|
|
220
|
+
fullWeight = materialized;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Recover the full-precision 2D shape for eligibility + param counting.
|
|
224
|
+
// For a freshly-loaded bf16 weight this is just `shape`; for a
|
|
225
|
+
// re-quantized weight it is the dequantized array's shape.
|
|
226
|
+
const fullShape = alreadyQuant ? materialized!.shape : shape;
|
|
227
|
+
|
|
228
|
+
if (!isQuantizable(fullShape, groupSize)) {
|
|
229
|
+
// Not eligible: pass the full-precision weight through unchanged
|
|
230
|
+
// (re-materialized to bf16 if it had been quantized).
|
|
231
|
+
if (alreadyQuant) {
|
|
232
|
+
const bf16 = materialized!.astype(Dtype.bfloat16, cpuStream);
|
|
233
|
+
out.push({ name: weightName, array: bf16 });
|
|
234
|
+
materialized!.dispose();
|
|
235
|
+
} else {
|
|
236
|
+
out.push({ name: weightName, array: fullWeight });
|
|
237
|
+
}
|
|
238
|
+
bumpProgress();
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Resolve this module's bit-width: the allocator's per-layer choice in
|
|
243
|
+
// mixed mode, else the uniform `bits`. The allocation map is keyed by
|
|
244
|
+
// module base path (e.g. "model.layers.0.self_attn.q_proj").
|
|
245
|
+
const moduleBits = perLayerBits?.get(base) ?? bits;
|
|
246
|
+
|
|
247
|
+
// Cast to bf16 before quantizing (mx.quantize expects a float weight;
|
|
248
|
+
// bf16 matches the reference dtype and the scales/biases dtype on disk).
|
|
249
|
+
const bf16 = fullWeight.astype(Dtype.bfloat16, cpuStream);
|
|
250
|
+
if (materialized) materialized.dispose();
|
|
251
|
+
|
|
252
|
+
const q = quantize(bf16, groupSize, moduleBits, mode, cpuStream);
|
|
253
|
+
bf16.dispose();
|
|
254
|
+
|
|
255
|
+
out.push({ name: `${base}.weight`, array: q.packed });
|
|
256
|
+
out.push({ name: `${base}.scales`, array: q.scales });
|
|
257
|
+
out.push({ name: `${base}.biases`, array: q.biases });
|
|
258
|
+
|
|
259
|
+
// Account: every element of the original 2D weight is now `moduleBits`
|
|
260
|
+
// bits, plus 32 bits (bf16 scale + bf16 bias) per group of `groupSize`.
|
|
261
|
+
const params = fullShape.reduce((a, b) => a * b, 1);
|
|
262
|
+
quantizedParams += params;
|
|
263
|
+
quantizedBits += params * moduleBits + (params / groupSize) * AFFINE_GROUP_OVERHEAD_BITS;
|
|
264
|
+
perLayer.set(base, { bits: moduleBits, groupSize });
|
|
265
|
+
nQuantized++;
|
|
266
|
+
|
|
267
|
+
bumpProgress();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Pass through non-weight/scale/bias tensors (e.g. anything unusual).
|
|
271
|
+
for (const name of passthroughNames) out.push(named(weights, name));
|
|
272
|
+
|
|
273
|
+
progress("writing", `Writing ${out.length} tensors to ${outDir}`, 1);
|
|
274
|
+
const write = writeShardedSafetensors(outDir, out);
|
|
275
|
+
|
|
276
|
+
const achievedBpw = quantizedParams > 0 ? quantizedBits / quantizedParams : 0;
|
|
277
|
+
|
|
278
|
+
// Default block bits: the uniform `bits` normally; in mixed mode use the
|
|
279
|
+
// lowest allocated bit-width so the loader's per-module overrides describe
|
|
280
|
+
// every deviation above the floor.
|
|
281
|
+
let defaultBits: number = bits;
|
|
282
|
+
if (perLayerBits && perLayerBits.size > 0) {
|
|
283
|
+
defaultBits = Math.min(...perLayerBits.values());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Build + write the config block. For uniform mode, let the config writer
|
|
287
|
+
// emit its standard optiq_metadata.json sidecar. For mixed mode we write a
|
|
288
|
+
// richer OptiQ-style sidecar ourselves below (method "mixed_precision").
|
|
289
|
+
const block = buildQuantizationBlock({ bits: defaultBits, groupSize, mode }, perLayer);
|
|
290
|
+
await writeQuantizedConfig(config.raw, outDir, block, {
|
|
291
|
+
srcDir,
|
|
292
|
+
optiq: mixedMeta
|
|
293
|
+
? undefined
|
|
294
|
+
: {
|
|
295
|
+
method: "uniform_affine",
|
|
296
|
+
base_model: srcDir,
|
|
297
|
+
bits,
|
|
298
|
+
group_size: groupSize,
|
|
299
|
+
achieved_bpw: achievedBpw,
|
|
300
|
+
per_layer_count: nQuantized,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (mixedMeta) {
|
|
305
|
+
await Bun.write(
|
|
306
|
+
join(outDir, "optiq_metadata.json"),
|
|
307
|
+
JSON.stringify(
|
|
308
|
+
{
|
|
309
|
+
method: "mixed_precision",
|
|
310
|
+
base_model: srcDir,
|
|
311
|
+
bits: defaultBits,
|
|
312
|
+
group_size: groupSize,
|
|
313
|
+
target_bpw: mixedMeta.targetBpw,
|
|
314
|
+
achieved_bpw: achievedBpw,
|
|
315
|
+
candidate_bits: mixedMeta.candidateBits,
|
|
316
|
+
n_high: mixedMeta.nHigh,
|
|
317
|
+
n_low: mixedMeta.nLow,
|
|
318
|
+
per_layer_count: nQuantized,
|
|
319
|
+
per_layer: Object.fromEntries(
|
|
320
|
+
[...perLayer].map(([p, e]) => [
|
|
321
|
+
p,
|
|
322
|
+
e === false ? false : { bits: e.bits, group_size: e.groupSize },
|
|
323
|
+
]),
|
|
324
|
+
),
|
|
325
|
+
},
|
|
326
|
+
null,
|
|
327
|
+
2,
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
progress("done", `Quantized ${nQuantized} modules → ${outDir}`, 1);
|
|
333
|
+
return { outDir, achievedBpw, nQuantized, write };
|
|
334
|
+
} finally {
|
|
335
|
+
// Dispose every emitted array (their bytes are now on disk) and the source.
|
|
336
|
+
for (const t of out) {
|
|
337
|
+
try { t.array.dispose(); } catch {}
|
|
338
|
+
}
|
|
339
|
+
weights.dispose();
|
|
340
|
+
clearCache();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function bumpProgress(): void {
|
|
344
|
+
processed++;
|
|
345
|
+
if (processed % CACHE_CLEAR_EVERY === 0) clearCache();
|
|
346
|
+
progress(
|
|
347
|
+
"quantizing",
|
|
348
|
+
`Module ${processed}/${totalModules}`,
|
|
349
|
+
totalModules > 0 ? processed / totalModules : 1,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Pass-through tensor: the lazy mlx array straight from the source weights. */
|
|
355
|
+
function named(weights: Weights, name: string): NamedTensor {
|
|
356
|
+
return { name, array: weights.tensor(name) };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Summary of a mixed-precision allocation, for the sidecar + return value. */
|
|
360
|
+
interface MixedMeta {
|
|
361
|
+
targetBpw: number;
|
|
362
|
+
candidateBits: number[];
|
|
363
|
+
nHigh: number;
|
|
364
|
+
nLow: number;
|
|
365
|
+
/** Knapsack's own BPW estimate over the analyzed layers (params-weighted,
|
|
366
|
+
* no group overhead) — distinct from the writer's achievedBpw. */
|
|
367
|
+
allocBpw: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Run the OptiQ mixed-precision pipeline for `srcDir`:
|
|
372
|
+
* load model + tokenizer → calibration → per-layer KL sensitivity →
|
|
373
|
+
* greedy knapsack → per-module bit allocation.
|
|
374
|
+
*
|
|
375
|
+
* Returns a `Map<modulePath, bits>` keyed by weight-file module base path
|
|
376
|
+
* (the same key the writer loop uses) plus a metadata summary. Modules not in
|
|
377
|
+
* the map fall back to the uniform `opts.bits` at write time.
|
|
378
|
+
*/
|
|
379
|
+
async function computeMixedAllocation(
|
|
380
|
+
srcDir: string,
|
|
381
|
+
opts: QuantizeOptions,
|
|
382
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
383
|
+
): Promise<{ perLayerBits: Map<string, number>; meta: MixedMeta }> {
|
|
384
|
+
const targetBpw = opts.targetBpw!;
|
|
385
|
+
const groupSize = opts.groupSize;
|
|
386
|
+
const candidateBits = (opts.candidateBits ?? [4, 8]).slice().sort((a, b) => a - b);
|
|
387
|
+
const nCalibration = opts.nCalibration ?? 2;
|
|
388
|
+
const calibrationMix = opts.calibrationMix ?? "optiq";
|
|
389
|
+
|
|
390
|
+
const progress = (stage: string, message: string, frac: number) =>
|
|
391
|
+
onProgress?.({ stage, message, progress: frac });
|
|
392
|
+
|
|
393
|
+
progress("sensitivity", "Loading model for sensitivity analysis", 0.0);
|
|
394
|
+
|
|
395
|
+
const config = await loadModelConfig(srcDir);
|
|
396
|
+
const weights = await Weights.open(srcDir);
|
|
397
|
+
try {
|
|
398
|
+
const model = createModel(weights, config);
|
|
399
|
+
const tokenizer = await loadTokenizer(srcDir);
|
|
400
|
+
|
|
401
|
+
// Calibration: short sequences keep the per-probe forward memory modest.
|
|
402
|
+
progress("sensitivity", "Building calibration set", 0.02);
|
|
403
|
+
const calibrationFn = loadLlmCalibration(tokenizer, {
|
|
404
|
+
nSamples: nCalibration,
|
|
405
|
+
seqLen: 128,
|
|
406
|
+
mix: calibrationMix,
|
|
407
|
+
});
|
|
408
|
+
const calIds = calibrationFn();
|
|
409
|
+
|
|
410
|
+
// Quantizable linears to probe: the model's LoRA-target linears (attention
|
|
411
|
+
// + MLP projections), keyed by full module path.
|
|
412
|
+
const layers = model.loraTargets();
|
|
413
|
+
|
|
414
|
+
progress(
|
|
415
|
+
"sensitivity",
|
|
416
|
+
`Probing ${layers.size} layers × ${candidateBits.length} bit-widths`,
|
|
417
|
+
0.05,
|
|
418
|
+
);
|
|
419
|
+
const sensitivity: SensitivityResult[] = analyzeSensitivityExact(
|
|
420
|
+
model,
|
|
421
|
+
layers,
|
|
422
|
+
calIds,
|
|
423
|
+
{ candidateBits, groupSize },
|
|
424
|
+
(done, total, layerName) =>
|
|
425
|
+
progress(
|
|
426
|
+
"sensitivity",
|
|
427
|
+
`Sensitivity ${done}/${total}: ${layerName}`,
|
|
428
|
+
0.05 + 0.85 * (done / Math.max(total, 1)),
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Greedy knapsack → per-layer bit allocation.
|
|
433
|
+
progress("allocating", "Optimizing per-layer bit allocation", 0.92);
|
|
434
|
+
const opt = optimizeMixedPrecision(sensitivity, {
|
|
435
|
+
targetBpw,
|
|
436
|
+
candidateBits,
|
|
437
|
+
groupSize,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const perLayerBits = new Map<string, number>();
|
|
441
|
+
for (const c of opt.configs) perLayerBits.set(c.layerName, c.bits);
|
|
442
|
+
|
|
443
|
+
const meta: MixedMeta = {
|
|
444
|
+
targetBpw,
|
|
445
|
+
candidateBits,
|
|
446
|
+
nHigh: opt.nHighBits,
|
|
447
|
+
nLow: opt.nLowBits,
|
|
448
|
+
allocBpw: opt.achievedBpw,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
progress(
|
|
452
|
+
"allocating",
|
|
453
|
+
`Allocated ${opt.nHighBits} high / ${opt.nLowBits} low (alloc bpw ${opt.achievedBpw.toFixed(2)})`,
|
|
454
|
+
0.95,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
return { perLayerBits, meta };
|
|
458
|
+
} finally {
|
|
459
|
+
weights.dispose();
|
|
460
|
+
clearCache();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Sharded safetensors writer: takes a list of named mlx arrays and writes
|
|
2
|
+
// them to disk as one or more `model*.safetensors` files via mlx's native
|
|
3
|
+
// saver (mlx_save_safetensors), matching the on-disk layout the loader
|
|
4
|
+
// (src/weights.ts → ShardedSafetensors) reads back.
|
|
5
|
+
//
|
|
6
|
+
// Layout parity with mlx-lm / the MiniCPM5-OptiQ target:
|
|
7
|
+
// - single shard → `model.safetensors`, NO index file.
|
|
8
|
+
// - multiple shards → `model-00001-of-0000N.safetensors`, plus a
|
|
9
|
+
// `model.safetensors.index.json` { metadata:{total_size,
|
|
10
|
+
// total_parameters}, weight_map: <name → shard> }.
|
|
11
|
+
// - metadata map carries {"format":"mlx"} so the file round-trips through
|
|
12
|
+
// mlx_load_safetensors with the same provenance HF/mlx writes.
|
|
13
|
+
//
|
|
14
|
+
// Memory discipline: each array is eval'd immediately before insertion so its
|
|
15
|
+
// bytes are materialized one tensor at a time (the caller hands us already-
|
|
16
|
+
// disposed-after-write arrays), bounding peak memory to ~one tensor over the
|
|
17
|
+
// resident weight set rather than the whole model.
|
|
18
|
+
|
|
19
|
+
import { ptr } from "bun:ffi";
|
|
20
|
+
import { writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { C, takeMlxError } from "../mlx/ffi";
|
|
23
|
+
import type { MlxArray } from "../mlx/array";
|
|
24
|
+
|
|
25
|
+
const cstr = (s: string) => Buffer.from(s + "\0", "utf8");
|
|
26
|
+
|
|
27
|
+
/** Default greedy shard ceiling: 5 GiB (mlx-lm's MAX shard size). */
|
|
28
|
+
export const DEFAULT_SHARD_BYTES = 5 << 30;
|
|
29
|
+
|
|
30
|
+
export interface NamedTensor {
|
|
31
|
+
name: string;
|
|
32
|
+
array: MlxArray;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ShardInfo {
|
|
36
|
+
/** Filename written into outDir (e.g. "model.safetensors"). */
|
|
37
|
+
file: string;
|
|
38
|
+
/** Tensor names placed in this shard, in insertion order. */
|
|
39
|
+
names: string[];
|
|
40
|
+
/** Byte size of the tensors in this shard (sum of nbytes). */
|
|
41
|
+
bytes: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SafetensorsIndex {
|
|
45
|
+
metadata: { total_size: number; total_parameters: number };
|
|
46
|
+
weight_map: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WriteResult {
|
|
50
|
+
shards: ShardInfo[];
|
|
51
|
+
totalSize: number;
|
|
52
|
+
totalParams: number;
|
|
53
|
+
/** The index object (also written to disk when shards.length > 1). */
|
|
54
|
+
index: SafetensorsIndex;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WriteOpts {
|
|
58
|
+
/** Greedy shard ceiling in bytes (default DEFAULT_SHARD_BYTES). */
|
|
59
|
+
shardBytes?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Insert one array into a native string→array map (key cstring, handle u64).
|
|
63
|
+
* Evals the array first so its bytes are materialized exactly here. */
|
|
64
|
+
function insertArray(map: bigint, name: string, array: MlxArray): void {
|
|
65
|
+
array.eval();
|
|
66
|
+
if (C.mlx_map_string_to_array_insert(map, ptr(cstr(name)), array.handle) !== 0)
|
|
67
|
+
throw new Error(`map insert ${name} failed: ${takeMlxError() ?? ""}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build the {"format":"mlx"} metadata map. Caller frees it. */
|
|
71
|
+
function newMetaMap(): bigint {
|
|
72
|
+
const map = C.mlx_map_string_to_string_new();
|
|
73
|
+
if (C.mlx_map_string_to_string_insert(map, ptr(cstr("format")), ptr(cstr("mlx"))) !== 0)
|
|
74
|
+
throw new Error(`metadata insert failed: ${takeMlxError() ?? ""}`);
|
|
75
|
+
return map;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Pack the shard's tensors into a fresh native map and save to `path`. */
|
|
79
|
+
function saveShard(path: string, tensors: NamedTensor[]): void {
|
|
80
|
+
const arrMap = C.mlx_map_string_to_array_new();
|
|
81
|
+
const metaMap = newMetaMap();
|
|
82
|
+
try {
|
|
83
|
+
for (const { name, array } of tensors) insertArray(arrMap, name, array);
|
|
84
|
+
if (C.mlx_save_safetensors(ptr(cstr(path)), arrMap, metaMap) !== 0)
|
|
85
|
+
throw new Error(`mlx_save_safetensors(${path}) failed: ${takeMlxError() ?? ""}`);
|
|
86
|
+
} finally {
|
|
87
|
+
C.mlx_map_string_to_array_free(arrMap);
|
|
88
|
+
C.mlx_map_string_to_string_free(metaMap);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Shard filename for shard i of n (1-based, 5-digit, matches mlx-lm). */
|
|
93
|
+
function shardName(i: number, n: number): string {
|
|
94
|
+
if (n === 1) return "model.safetensors";
|
|
95
|
+
const pad = (x: number) => String(x).padStart(5, "0");
|
|
96
|
+
return `model-${pad(i + 1)}-of-${pad(n)}.safetensors`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Greedily bin tensors into shards no larger than `shardBytes`. A single
|
|
100
|
+
* tensor larger than the ceiling still gets its own shard (never split). */
|
|
101
|
+
function planShards(tensors: NamedTensor[], shardBytes: number): NamedTensor[][] {
|
|
102
|
+
const shards: NamedTensor[][] = [];
|
|
103
|
+
let cur: NamedTensor[] = [];
|
|
104
|
+
let curBytes = 0;
|
|
105
|
+
for (const t of tensors) {
|
|
106
|
+
const nb = t.array.nbytes;
|
|
107
|
+
if (cur.length > 0 && curBytes + nb > shardBytes) {
|
|
108
|
+
shards.push(cur);
|
|
109
|
+
cur = [];
|
|
110
|
+
curBytes = 0;
|
|
111
|
+
}
|
|
112
|
+
cur.push(t);
|
|
113
|
+
curBytes += nb;
|
|
114
|
+
}
|
|
115
|
+
if (cur.length > 0) shards.push(cur);
|
|
116
|
+
return shards.length > 0 ? shards : [[]];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Write `tensors` to `outDir` as sharded safetensors. Returns shard layout,
|
|
121
|
+
* total byte size, total parameter count, and the (single source of truth)
|
|
122
|
+
* index object. An index file is written only when there is more than one
|
|
123
|
+
* shard — matching the loader's expectation that a lone `model.safetensors`
|
|
124
|
+
* carries no sidecar index.
|
|
125
|
+
*/
|
|
126
|
+
export function writeShardedSafetensors(
|
|
127
|
+
outDir: string,
|
|
128
|
+
tensors: NamedTensor[],
|
|
129
|
+
opts: WriteOpts = {},
|
|
130
|
+
): WriteResult {
|
|
131
|
+
const shardBytes = opts.shardBytes ?? DEFAULT_SHARD_BYTES;
|
|
132
|
+
const plan = planShards(tensors, shardBytes);
|
|
133
|
+
const n = plan.length;
|
|
134
|
+
|
|
135
|
+
const shards: ShardInfo[] = [];
|
|
136
|
+
const weight_map: Record<string, string> = {};
|
|
137
|
+
let totalSize = 0;
|
|
138
|
+
let totalParams = 0;
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < n; i++) {
|
|
141
|
+
const group = plan[i]!;
|
|
142
|
+
const file = shardName(i, n);
|
|
143
|
+
let bytes = 0;
|
|
144
|
+
for (const t of group) {
|
|
145
|
+
bytes += t.array.nbytes;
|
|
146
|
+
totalParams += t.array.size;
|
|
147
|
+
weight_map[t.name] = file;
|
|
148
|
+
}
|
|
149
|
+
totalSize += bytes;
|
|
150
|
+
saveShard(join(outDir, file), group);
|
|
151
|
+
shards.push({ file, names: group.map((t) => t.name), bytes });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// weight_map sorted by name for a stable, diff-friendly index.
|
|
155
|
+
const sortedMap: Record<string, string> = {};
|
|
156
|
+
for (const name of Object.keys(weight_map).sort()) sortedMap[name] = weight_map[name]!;
|
|
157
|
+
|
|
158
|
+
const index: SafetensorsIndex = {
|
|
159
|
+
metadata: { total_size: totalSize, total_parameters: totalParams },
|
|
160
|
+
weight_map: sortedMap,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (n > 1) {
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(outDir, "model.safetensors.index.json"),
|
|
166
|
+
JSON.stringify(index, null, 2),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { shards, totalSize, totalParams, index };
|
|
171
|
+
}
|