mlx-bun 0.0.7 → 0.0.9
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 +112 -20
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/diffusion-gemma-port.md +602 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +40 -9
- package/docs/reference/training.md +25 -11
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1029 -86
- package/src/config.ts +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/ffi.ts +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +45 -7
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +90 -18
- package/src/server.ts +643 -50
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/diffusion-lora.ts +216 -0
- package/src/train/flash-cce.ts +70 -15
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/config.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface TextConfig {
|
|
|
55
55
|
tieWordEmbeddings: boolean;
|
|
56
56
|
bosTokenId: number;
|
|
57
57
|
eosTokenId: number | number[];
|
|
58
|
+
/** DiffusionGemma: fixed denoising-canvas length (256). Undefined elsewhere. */
|
|
59
|
+
canvasLength?: number;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface QuantSpec {
|
|
@@ -132,13 +134,61 @@ export function quantFor(q: QuantizationConfig | null, modulePath: string): Quan
|
|
|
132
134
|
return spec.mode === "none" ? null : spec;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
/** DiffusionGemma's config.json ships only token ids + canvas_length + the
|
|
138
|
+
* quant map — the architecture dims live in optiq's `config.py` TextConfig
|
|
139
|
+
* defaults (and layer_types / rope_parameters are computed in __post_init__).
|
|
140
|
+
* We reproduce those defaults in snake_case so the generic parser below picks
|
|
141
|
+
* them up; any field actually present in config.json still overrides. Source:
|
|
142
|
+
* optiq/vlm/_mlxvlm/models/diffusion_gemma/config.py. */
|
|
143
|
+
function diffusionGemmaRawDefaults(): Record<string, any> {
|
|
144
|
+
const numLayers = 30;
|
|
145
|
+
const pattern = ["sliding_attention", "sliding_attention", "sliding_attention",
|
|
146
|
+
"sliding_attention", "sliding_attention", "full_attention"];
|
|
147
|
+
const layer_types = Array.from({ length: numLayers }, (_, i) => pattern[i % pattern.length]);
|
|
148
|
+
layer_types[numLayers - 1] = "full_attention"; // last forced full
|
|
149
|
+
return {
|
|
150
|
+
hidden_size: 2816,
|
|
151
|
+
num_hidden_layers: numLayers,
|
|
152
|
+
num_attention_heads: 16,
|
|
153
|
+
num_key_value_heads: 8,
|
|
154
|
+
num_global_key_value_heads: 2,
|
|
155
|
+
head_dim: 256,
|
|
156
|
+
global_head_dim: 512,
|
|
157
|
+
intermediate_size: 2112,
|
|
158
|
+
moe_intermediate_size: 704,
|
|
159
|
+
hidden_activation: "gelu_pytorch_tanh",
|
|
160
|
+
rms_norm_eps: 1e-6,
|
|
161
|
+
vocab_size: 262144,
|
|
162
|
+
max_position_embeddings: 262144,
|
|
163
|
+
sliding_window: 1024,
|
|
164
|
+
layer_types,
|
|
165
|
+
enable_moe_block: true,
|
|
166
|
+
num_experts: 128,
|
|
167
|
+
top_k_experts: 8,
|
|
168
|
+
final_logit_softcapping: 30.0,
|
|
169
|
+
tie_word_embeddings: true,
|
|
170
|
+
bos_token_id: 2,
|
|
171
|
+
eos_token_id: 1,
|
|
172
|
+
rope_parameters: {
|
|
173
|
+
sliding_attention: { rope_type: "default", rope_theta: 10000.0 },
|
|
174
|
+
full_attention: { rope_type: "proportional", partial_rotary_factor: 0.25, rope_theta: 1000000.0 },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
135
179
|
export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
136
180
|
const raw = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
|
|
137
|
-
// Gemma 4 unified nests the LM config; plain text models keep it at top level.
|
|
138
|
-
const t = (raw.text_config ?? raw) as Record<string, any>;
|
|
139
181
|
const modelType = raw.model_type;
|
|
182
|
+
const isDiffusion = modelType === "diffusion_gemma";
|
|
183
|
+
// Gemma 4 unified nests the LM config; plain text models keep it at top level.
|
|
184
|
+
// DiffusionGemma keeps it flat too, but omits all dims — backfill from defaults.
|
|
185
|
+
const baseT = (raw.text_config ?? raw) as Record<string, any>;
|
|
186
|
+
const t = isDiffusion ? { ...diffusionGemmaRawDefaults(), ...baseT } : baseT;
|
|
140
187
|
const isLlama = modelType === "llama";
|
|
141
188
|
const isQwen35 = typeof modelType === "string" && modelType.startsWith("qwen3_5");
|
|
189
|
+
// Plain Qwen3 (Qwen3ForCausalLM, e.g. Qwen3-Embedding): a flat HF config with
|
|
190
|
+
// a scalar rope_theta and no rope_parameters map — handled like llama below.
|
|
191
|
+
const isQwen3 = modelType === "qwen3";
|
|
142
192
|
// Qwen3.5 rope_parameters is a flat dict ({type, rope_theta, mrope_section,
|
|
143
193
|
// partial_rotary_factor}), not the gemma per-attention-type map — and
|
|
144
194
|
// type "default" means plain partial nn.RoPE (mrope_section ignored for text).
|
|
@@ -187,7 +237,7 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
187
237
|
}
|
|
188
238
|
: t.rope_parameters
|
|
189
239
|
? parseRope(t.rope_parameters)
|
|
190
|
-
: isLlama
|
|
240
|
+
: isLlama || isQwen3
|
|
191
241
|
? {
|
|
192
242
|
full_attention: {
|
|
193
243
|
ropeTheta: t.rope_theta ?? 10000,
|
|
@@ -201,6 +251,7 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
201
251
|
tieWordEmbeddings: t.tie_word_embeddings ?? raw.tie_word_embeddings ?? false,
|
|
202
252
|
bosTokenId: t.bos_token_id ?? raw.bos_token_id,
|
|
203
253
|
eosTokenId: t.eos_token_id ?? raw.eos_token_id,
|
|
254
|
+
canvasLength: raw.canvas_length ?? t.canvas_length,
|
|
204
255
|
};
|
|
205
256
|
|
|
206
257
|
let kvQuant: KvQuantSpec[] | null = null;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// DiffusionGemma denoising generation engine (port D2). Block-diffusion, NOT
|
|
2
|
+
// autoregressive: prefill the prompt once, then for each canvas block fill a
|
|
3
|
+
// fixed-length canvas with uniform-random token ids and un-mask it over <=48
|
|
4
|
+
// denoising steps, feeding the previous step's soft embeddings back in
|
|
5
|
+
// (self-conditioning). Ported verbatim from
|
|
6
|
+
// optiq/vlm/_mlxvlm/generate/diffusion.py (stream_diffusion_generate).
|
|
7
|
+
//
|
|
8
|
+
// RNG parity: canvas init + per-step re-noise call ops.randint with the GLOBAL
|
|
9
|
+
// mlx key (ops.randomSeed). Calling them in the same order as the reference
|
|
10
|
+
// reproduces its draws bit-for-bit → token-for-token parity on a fixed seed.
|
|
11
|
+
//
|
|
12
|
+
// Implements the public-API default sampler (confidence-threshold) AND the
|
|
13
|
+
// model-default (entropy-bound). temperature 0 (greedy) is the default and the
|
|
14
|
+
// only RNG consumers then are the canvas draws.
|
|
15
|
+
|
|
16
|
+
import type { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
17
|
+
import { MlxArray } from "../mlx/array";
|
|
18
|
+
import { Dtype } from "../mlx/ffi";
|
|
19
|
+
import * as ops from "../mlx/ops";
|
|
20
|
+
import { disposing } from "../model/gemma4-base";
|
|
21
|
+
|
|
22
|
+
export interface DiffusionGenOptions {
|
|
23
|
+
maxTokens: number;
|
|
24
|
+
maxDenoisingSteps?: number; // default 48
|
|
25
|
+
minCanvasLength?: number; // default 64
|
|
26
|
+
maxCanvasLength?: number; // default model canvas (256)
|
|
27
|
+
fullCanvas?: boolean; // force full model canvas each block
|
|
28
|
+
sampler?: "confidence-threshold" | "entropy-bound"; // default confidence-threshold
|
|
29
|
+
threshold?: number; // confidence accept threshold (default 0.9)
|
|
30
|
+
entropyBound?: number; // entropy sampler bound (default 0.1)
|
|
31
|
+
temperature?: number; // default 0 (greedy)
|
|
32
|
+
tMin?: number; // schedule (default 0.4)
|
|
33
|
+
tMax?: number; // schedule (default 0.8)
|
|
34
|
+
// stable+confident early stop. The shipped checkpoint loads
|
|
35
|
+
// generation_config=None in optiq, so this is OFF in the oracle (entropy runs
|
|
36
|
+
// all steps). Only enabled when BOTH are provided (mirrors stopping_config).
|
|
37
|
+
stabilityThreshold?: number;
|
|
38
|
+
confidenceThreshold?: number;
|
|
39
|
+
eosTokenIds?: number[]; // default [1, 106] (the tokenizer stopping set; NOT 50)
|
|
40
|
+
seed?: bigint; // global RNG seed (default 0)
|
|
41
|
+
/** Image-text-to-text: when set, `promptIds` are the SPLICED ids (with the
|
|
42
|
+
* <|image|> run) and the encoder prefills with the merged vision features +
|
|
43
|
+
* bidirectional overlay instead of the plain text path. Channel-first
|
|
44
|
+
* [1,3,H,W]. Caller keeps ownership. */
|
|
45
|
+
visionPixels?: MlxArray;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiffusionGenResult {
|
|
49
|
+
tokens: number[]; // emitted tokens (EOS-trimmed, EOS excluded)
|
|
50
|
+
blocks: number[][]; // per-block final argmax canvases (full, pre-trim)
|
|
51
|
+
steps: number; // total denoising steps run
|
|
52
|
+
finishReason: "stop" | "length";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const I32 = Dtype.int32;
|
|
56
|
+
|
|
57
|
+
/** logits / schedule_temperature (the diffusion linear temperature schedule). */
|
|
58
|
+
function linearTemp(curStep: number, maxSteps: number, tMin: number, tMax: number): number {
|
|
59
|
+
return tMin + (tMax - tMin) * (curStep / maxSteps);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** confidence = exp(token_logit - logsumexp(logits)) in fp32 (per position). */
|
|
63
|
+
function tokenProbability(processed: MlxArray, tokenIds: MlxArray): MlxArray {
|
|
64
|
+
const lf = processed.astype(Dtype.float32);
|
|
65
|
+
const tokExp = ops.expandDims(tokenIds, -1); // [1, L, 1]
|
|
66
|
+
const gathered = ops.takeAlongAxis(lf, tokExp, -1); // [1, L, 1]
|
|
67
|
+
tokExp.dispose();
|
|
68
|
+
const [b, l] = gathered.shape as [number, number, number];
|
|
69
|
+
const tokLogits = ops.reshape(gathered, [b, l]); // squeeze(-1)
|
|
70
|
+
gathered.dispose();
|
|
71
|
+
const lse = ops.logsumexpAxis(lf, -1, false); // [1, L]
|
|
72
|
+
lf.dispose();
|
|
73
|
+
const diff = ops.sub(tokLogits, lse);
|
|
74
|
+
tokLogits.dispose();
|
|
75
|
+
lse.dispose();
|
|
76
|
+
const conf = ops.exp(diff);
|
|
77
|
+
diff.dispose();
|
|
78
|
+
return conf;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** entropy chain (reference _diffusion_entropy_probs_chain): returns
|
|
82
|
+
* { probs, entropy } in fp32. probs = exp(logits - logsumexp). */
|
|
83
|
+
function entropyProbsChain(processed: MlxArray): { probs: MlxArray; entropy: MlxArray } {
|
|
84
|
+
const lf = processed.astype(Dtype.float32);
|
|
85
|
+
const lse = ops.logsumexpAxis(lf, -1, true); // [1, L, 1]
|
|
86
|
+
const logProbs = ops.sub(lf, lse);
|
|
87
|
+
lf.dispose();
|
|
88
|
+
lse.dispose();
|
|
89
|
+
const probs = ops.exp(logProbs);
|
|
90
|
+
const prod = ops.mul(probs, logProbs);
|
|
91
|
+
logProbs.dispose();
|
|
92
|
+
const summed = ops.sumAxis(prod, -1, false); // [1, L]
|
|
93
|
+
prod.dispose();
|
|
94
|
+
const entropy = ops.mulScalar(summed, -1);
|
|
95
|
+
summed.dispose();
|
|
96
|
+
return { probs, entropy };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** per-position entropy -sum(p*logp) over vocab, fp32. */
|
|
100
|
+
function tokenEntropy(processed: MlxArray): MlxArray {
|
|
101
|
+
const { probs, entropy } = entropyProbsChain(processed);
|
|
102
|
+
probs.dispose();
|
|
103
|
+
return entropy;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** soft embeddings from precomputed probs (entropy sampler path):
|
|
107
|
+
* (probs.astype(w) @ w).astype(w) * embed_scale. */
|
|
108
|
+
function softEmbeddingsFromProbs(probs: MlxArray, weight: MlxArray, embedScale: number): MlxArray {
|
|
109
|
+
const pc = probs.astype(weight.dtype);
|
|
110
|
+
const m = ops.matmul(pc, weight);
|
|
111
|
+
pc.dispose();
|
|
112
|
+
const mc = m.astype(weight.dtype);
|
|
113
|
+
m.dispose();
|
|
114
|
+
const out = ops.mulScalar(mc, embedScale);
|
|
115
|
+
mc.dispose();
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** confidence-threshold acceptance mask (reference _diffusion_confidence_transfer_mask). */
|
|
120
|
+
function confidenceTransferMask(
|
|
121
|
+
confidence: MlxArray,
|
|
122
|
+
unrevealed: MlxArray,
|
|
123
|
+
threshold: number,
|
|
124
|
+
forceAll: boolean,
|
|
125
|
+
): MlxArray {
|
|
126
|
+
if (forceAll) return ops.logicalNot(ops.logicalNot(unrevealed)); // copy of unrevealed
|
|
127
|
+
const thr = ops.scalarLike(threshold, confidence);
|
|
128
|
+
const ge = ops.greaterEqual(confidence, thr);
|
|
129
|
+
thr.dispose();
|
|
130
|
+
const transfer = ops.logicalAnd(unrevealed, ge);
|
|
131
|
+
ge.dispose();
|
|
132
|
+
const hasUnrevealed = ops.anyAxis(unrevealed, -1, true); // [1, 1]
|
|
133
|
+
const hasTransfer = ops.anyAxis(transfer, -1, true); // [1, 1]
|
|
134
|
+
const notTransfer = ops.logicalNot(hasTransfer);
|
|
135
|
+
hasTransfer.dispose();
|
|
136
|
+
const needsForce = ops.logicalAnd(hasUnrevealed, notTransfer); // [1, 1]
|
|
137
|
+
hasUnrevealed.dispose();
|
|
138
|
+
notTransfer.dispose();
|
|
139
|
+
const negInf = ops.scalarLike(-Infinity, confidence);
|
|
140
|
+
const maskedConf = ops.where(unrevealed, confidence, negInf); // [1, L]
|
|
141
|
+
negInf.dispose();
|
|
142
|
+
const best = ops.argmaxAxis(maskedConf, -1); // [1]
|
|
143
|
+
maskedConf.dispose();
|
|
144
|
+
const L = confidence.shape[confidence.shape.length - 1]!;
|
|
145
|
+
const positions = ops.arange(0, L, 1, I32); // [L]
|
|
146
|
+
const posRow = ops.reshape(positions, [1, L]);
|
|
147
|
+
positions.dispose();
|
|
148
|
+
const bestCol = ops.reshape(best, [1, 1]);
|
|
149
|
+
best.dispose();
|
|
150
|
+
const eq = ops.equal(posRow, bestCol); // [1, L]
|
|
151
|
+
posRow.dispose();
|
|
152
|
+
bestCol.dispose();
|
|
153
|
+
const forced = ops.logicalAnd(eq, needsForce); // broadcast [1, L]
|
|
154
|
+
eq.dispose();
|
|
155
|
+
needsForce.dispose();
|
|
156
|
+
const out = ops.logicalOr(transfer, forced);
|
|
157
|
+
transfer.dispose();
|
|
158
|
+
forced.dispose();
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** entropy-bound acceptance mask (reference _diffusion_entropy_transfer_mask):
|
|
163
|
+
* sort positions by ascending entropy; accept the lowest-entropy prefix while
|
|
164
|
+
* cumsum - cummax <= bound; unsort. */
|
|
165
|
+
function entropyTransferMask(entropy: MlxArray, bound: number): MlxArray {
|
|
166
|
+
const sortedIdx = ops.argsortAxis(entropy, -1); // [1, L]
|
|
167
|
+
const sortedEnt = ops.takeAlongAxis(entropy, sortedIdx, -1);
|
|
168
|
+
const cum = ops.cumsum(sortedEnt, -1);
|
|
169
|
+
const cmax = ops.cummax(sortedEnt, -1);
|
|
170
|
+
sortedEnt.dispose();
|
|
171
|
+
const delta = ops.sub(cum, cmax);
|
|
172
|
+
cum.dispose();
|
|
173
|
+
cmax.dispose();
|
|
174
|
+
const boundArr = ops.scalarLike(bound, delta);
|
|
175
|
+
const sortedSel = ops.lessEqual(delta, boundArr); // [1, L] bool
|
|
176
|
+
delta.dispose();
|
|
177
|
+
boundArr.dispose();
|
|
178
|
+
// put_along_axis: scatter sortedSel back via sortedIdx into a zero mask.
|
|
179
|
+
const zeros = ops.zeros(sortedSel.shape, sortedSel.dtype);
|
|
180
|
+
const out = ops.putAlongAxis(zeros, sortedIdx, sortedSel, -1);
|
|
181
|
+
zeros.dispose();
|
|
182
|
+
sortedIdx.dispose();
|
|
183
|
+
sortedSel.dispose();
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function diffusionGenerate(
|
|
188
|
+
model: DiffusionGemmaModel,
|
|
189
|
+
promptIds: number[],
|
|
190
|
+
opts: DiffusionGenOptions,
|
|
191
|
+
): DiffusionGenResult {
|
|
192
|
+
const t = model.config.text;
|
|
193
|
+
const vocab = t.vocabSize;
|
|
194
|
+
const modelCanvas = model.canvasLength;
|
|
195
|
+
const maxSteps = opts.maxDenoisingSteps ?? 48;
|
|
196
|
+
const fullCanvas = opts.fullCanvas ?? false;
|
|
197
|
+
const maxCanvas = fullCanvas ? modelCanvas : Math.min(modelCanvas, opts.maxCanvasLength ?? modelCanvas);
|
|
198
|
+
const minCanvas = Math.min(maxCanvas, opts.minCanvasLength ?? 64);
|
|
199
|
+
const sampler = opts.sampler ?? "confidence-threshold";
|
|
200
|
+
const threshold = opts.threshold ?? 0.9;
|
|
201
|
+
const entropyBound = opts.entropyBound ?? 0.1;
|
|
202
|
+
const temperature = opts.temperature ?? 0;
|
|
203
|
+
const tMin = opts.tMin ?? 0.4;
|
|
204
|
+
const tMax = opts.tMax ?? 0.8;
|
|
205
|
+
// Stable-stop is OFF unless both thresholds are explicitly provided (the
|
|
206
|
+
// oracle ships generation_config=None → stopping_config None → never stops).
|
|
207
|
+
const stableStop = opts.stabilityThreshold !== undefined && opts.confidenceThreshold !== undefined;
|
|
208
|
+
const stabilityThreshold = opts.stabilityThreshold ?? 1;
|
|
209
|
+
const stopConfidence = opts.confidenceThreshold ?? 0.005;
|
|
210
|
+
const eosIds = new Set(opts.eosTokenIds ?? [1, 106]);
|
|
211
|
+
const maxNewTokens = opts.maxTokens;
|
|
212
|
+
|
|
213
|
+
if (temperature > 0) throw new Error("diffusion temperature>0 (categorical sampling) not wired yet");
|
|
214
|
+
|
|
215
|
+
ops.randomSeed(opts.seed ?? 0n);
|
|
216
|
+
|
|
217
|
+
const cache = opts.visionPixels
|
|
218
|
+
? model.prefillVision(promptIds, opts.visionPixels)
|
|
219
|
+
: model.prefill(promptIds);
|
|
220
|
+
const dequantWeight = model.dequantEmbedWeight();
|
|
221
|
+
|
|
222
|
+
const emitted: number[] = [];
|
|
223
|
+
const blocks: number[][] = [];
|
|
224
|
+
let totalSteps = 0;
|
|
225
|
+
let generated = 0;
|
|
226
|
+
let finishReason: "stop" | "length" = "length";
|
|
227
|
+
let stopped = false;
|
|
228
|
+
let isFirstBlock = true;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
while (generated < maxNewTokens) {
|
|
232
|
+
const remaining = maxNewTokens - generated;
|
|
233
|
+
const canvasLength = fullCanvas
|
|
234
|
+
? modelCanvas
|
|
235
|
+
: Math.min(maxCanvas, Math.max(remaining, minCanvas));
|
|
236
|
+
|
|
237
|
+
if (!isFirstBlock) {
|
|
238
|
+
// Continuation: prefill the encoder over the previous accepted block.
|
|
239
|
+
const prev = blocks[blocks.length - 1]!;
|
|
240
|
+
const prevArr = MlxArray.fromInt32(Int32Array.from(prev), [1, prev.length]);
|
|
241
|
+
model.extendPrefill(prevArr, cache);
|
|
242
|
+
prevArr.dispose();
|
|
243
|
+
}
|
|
244
|
+
isFirstBlock = false;
|
|
245
|
+
|
|
246
|
+
let currentCanvas = ops.randint(0, vocab, [1, canvasLength], I32); // RNG: init
|
|
247
|
+
let draftReveal = ops.zeros([1, canvasLength], Dtype.bool);
|
|
248
|
+
let draftCanvas: MlxArray = ops.logicalNot(ops.logicalNot(draftReveal)); // placeholder, replaced below
|
|
249
|
+
draftCanvas.dispose();
|
|
250
|
+
draftCanvas = ops.reshape(currentCanvas, [1, canvasLength]); // alias copy
|
|
251
|
+
let argmaxCanvas = ops.reshape(currentCanvas, [1, canvasLength]);
|
|
252
|
+
let scEmbeddings: MlxArray | null = null;
|
|
253
|
+
const history: MlxArray[] = [];
|
|
254
|
+
let stepsThisCanvas = 0;
|
|
255
|
+
|
|
256
|
+
for (let curStep = maxSteps; curStep >= 1; curStep--) {
|
|
257
|
+
stepsThisCanvas++;
|
|
258
|
+
const logits = model.decoderLogits(currentCanvas, cache, scEmbeddings);
|
|
259
|
+
const schedT = linearTemp(curStep, maxSteps, tMin, tMax);
|
|
260
|
+
// Match the reference EXACTLY: divide (not reciprocal-multiply) — the
|
|
261
|
+
// f32 rounding differs and the confidence threshold is a hard cutoff
|
|
262
|
+
// that flips acceptance on a 1-ULP difference, diverging the trajectory.
|
|
263
|
+
const schedArr = ops.scalarLike(schedT, logits);
|
|
264
|
+
const processed = ops.div(logits, schedArr);
|
|
265
|
+
schedArr.dispose();
|
|
266
|
+
logits.dispose();
|
|
267
|
+
|
|
268
|
+
argmaxCanvas.dispose();
|
|
269
|
+
argmaxCanvas = ops.argmaxAxis(processed, -1).astype(I32); // [1, L]
|
|
270
|
+
|
|
271
|
+
if (curStep === 1) {
|
|
272
|
+
processed.dispose();
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// temperature 0 → denoiser canvas is the argmax.
|
|
277
|
+
const denoiser = argmaxCanvas;
|
|
278
|
+
|
|
279
|
+
let acceptance: MlxArray;
|
|
280
|
+
let nextSc: MlxArray | null = null;
|
|
281
|
+
if (sampler === "entropy-bound") {
|
|
282
|
+
// cur_step > 1 always here (cur_step==1 breaks before the sampler):
|
|
283
|
+
// entropy AND the next soft embeddings come from the SAME entropy
|
|
284
|
+
// chain (reference _diffusion_entropy_and_soft_embeddings) — NOT the
|
|
285
|
+
// softmax-precise path the confidence sampler uses.
|
|
286
|
+
const { probs, entropy } = entropyProbsChain(processed);
|
|
287
|
+
acceptance = entropyTransferMask(entropy, entropyBound);
|
|
288
|
+
entropy.dispose();
|
|
289
|
+
nextSc = softEmbeddingsFromProbs(probs, dequantWeight, model.embedScale);
|
|
290
|
+
probs.dispose();
|
|
291
|
+
// accepted/current update (entropy variant)
|
|
292
|
+
const accepted = ops.where(acceptance, denoiser, currentCanvas);
|
|
293
|
+
const noise = ops.randint(0, vocab, [1, canvasLength], I32); // RNG: re-noise
|
|
294
|
+
const newCurrent = ops.where(acceptance, accepted, noise);
|
|
295
|
+
noise.dispose();
|
|
296
|
+
currentCanvas.dispose();
|
|
297
|
+
currentCanvas = newCurrent;
|
|
298
|
+
draftReveal.dispose();
|
|
299
|
+
draftReveal = ops.logicalNot(ops.logicalNot(acceptance)); // = acceptance
|
|
300
|
+
draftCanvas.dispose();
|
|
301
|
+
draftCanvas = ops.reshape(argmaxCanvas, [1, canvasLength]);
|
|
302
|
+
accepted.dispose();
|
|
303
|
+
} else {
|
|
304
|
+
const unrevealed = ops.logicalNot(draftReveal);
|
|
305
|
+
const confidence = tokenProbability(processed, denoiser);
|
|
306
|
+
acceptance = confidenceTransferMask(confidence, unrevealed, threshold, false);
|
|
307
|
+
confidence.dispose();
|
|
308
|
+
const accepted = ops.where(acceptance, denoiser, draftCanvas);
|
|
309
|
+
const revealOrAccept = ops.logicalOr(draftReveal, acceptance);
|
|
310
|
+
const noise = ops.randint(0, vocab, [1, canvasLength], I32); // RNG: re-noise
|
|
311
|
+
const newCurrent = ops.where(revealOrAccept, accepted, noise);
|
|
312
|
+
noise.dispose();
|
|
313
|
+
currentCanvas.dispose();
|
|
314
|
+
currentCanvas = newCurrent;
|
|
315
|
+
const newReveal = ops.logicalOr(draftReveal, acceptance);
|
|
316
|
+
draftReveal.dispose();
|
|
317
|
+
draftReveal = newReveal;
|
|
318
|
+
const newDraft = ops.where(acceptance, accepted, draftCanvas);
|
|
319
|
+
draftCanvas.dispose();
|
|
320
|
+
draftCanvas = newDraft;
|
|
321
|
+
unrevealed.dispose();
|
|
322
|
+
revealOrAccept.dispose();
|
|
323
|
+
accepted.dispose();
|
|
324
|
+
}
|
|
325
|
+
acceptance.dispose();
|
|
326
|
+
|
|
327
|
+
// Early stop 1 (confidence sampler): all positions revealed.
|
|
328
|
+
if (sampler === "confidence-threshold") {
|
|
329
|
+
const allRevealed = ops.allReduce(draftReveal);
|
|
330
|
+
const done = ops.itemBool(allRevealed);
|
|
331
|
+
allRevealed.dispose();
|
|
332
|
+
if (done) {
|
|
333
|
+
processed.dispose();
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Early stop 2: stable + confident (only when explicitly configured;
|
|
339
|
+
// the as-loaded oracle has stopping_config None and never stops here).
|
|
340
|
+
if (
|
|
341
|
+
stableStop &&
|
|
342
|
+
stableAndConfident(argmaxCanvas, processed, history, stabilityThreshold, stopConfidence)
|
|
343
|
+
) {
|
|
344
|
+
processed.dispose();
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Self-conditioning feedback for the next step.
|
|
349
|
+
if (curStep > 1) {
|
|
350
|
+
if (nextSc === null) nextSc = model.softEmbeddings(processed, dequantWeight);
|
|
351
|
+
if (scEmbeddings) scEmbeddings.dispose();
|
|
352
|
+
scEmbeddings = nextSc;
|
|
353
|
+
}
|
|
354
|
+
processed.dispose();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (scEmbeddings) scEmbeddings.dispose();
|
|
358
|
+
for (const hh of history) hh.dispose();
|
|
359
|
+
currentCanvas.dispose();
|
|
360
|
+
draftReveal.dispose();
|
|
361
|
+
draftCanvas.dispose();
|
|
362
|
+
|
|
363
|
+
// Emit from the final argmax canvas.
|
|
364
|
+
const blockTokens = [...argmaxCanvas.astype(Dtype.float32).toFloat32()].map((x) =>
|
|
365
|
+
Math.round(x),
|
|
366
|
+
);
|
|
367
|
+
argmaxCanvas.dispose();
|
|
368
|
+
blocks.push(blockTokens);
|
|
369
|
+
totalSteps += stepsThisCanvas;
|
|
370
|
+
|
|
371
|
+
for (const tok of blockTokens) {
|
|
372
|
+
generated++;
|
|
373
|
+
if (eosIds.has(tok)) {
|
|
374
|
+
stopped = true;
|
|
375
|
+
finishReason = "stop";
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
emitted.push(tok);
|
|
379
|
+
if (generated >= maxNewTokens) {
|
|
380
|
+
stopped = true;
|
|
381
|
+
finishReason = "length";
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (stopped) break;
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
dequantWeight.dispose();
|
|
389
|
+
for (const c of cache) c.dispose();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { tokens: emitted, blocks, steps: totalSteps, finishReason };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** stable (last `stabilityThreshold` argmax canvases identical) AND confident
|
|
396
|
+
* (mean token entropy < bound). Mutates `history` like the reference. */
|
|
397
|
+
function stableAndConfident(
|
|
398
|
+
argmaxCanvas: MlxArray,
|
|
399
|
+
processed: MlxArray,
|
|
400
|
+
history: MlxArray[],
|
|
401
|
+
stabilityThreshold: number,
|
|
402
|
+
confidenceThreshold: number,
|
|
403
|
+
): boolean {
|
|
404
|
+
let stable: boolean;
|
|
405
|
+
if (history.length === stabilityThreshold) {
|
|
406
|
+
stable = history.every((canvas) => {
|
|
407
|
+
const eq = ops.equal(argmaxCanvas, canvas);
|
|
408
|
+
const all = ops.allReduce(eq);
|
|
409
|
+
eq.dispose();
|
|
410
|
+
const v = ops.itemBool(all);
|
|
411
|
+
all.dispose();
|
|
412
|
+
return v;
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
stable = false;
|
|
416
|
+
}
|
|
417
|
+
// Push an INDEPENDENT copy (add-zero forces a fresh buffer): a reshape/view
|
|
418
|
+
// would alias argmaxCanvas and read freed/overwritten data once the caller
|
|
419
|
+
// disposes argmaxCanvas next step → false "stable" → premature stop.
|
|
420
|
+
const zero = ops.scalarLike(0, argmaxCanvas);
|
|
421
|
+
history.push(ops.add(argmaxCanvas, zero));
|
|
422
|
+
zero.dispose();
|
|
423
|
+
if (history.length > stabilityThreshold) history.shift()!.dispose();
|
|
424
|
+
if (!stable) return false;
|
|
425
|
+
|
|
426
|
+
const entropy = tokenEntropy(processed);
|
|
427
|
+
const meanEnt = ops.meanAll(entropy, false);
|
|
428
|
+
entropy.dispose();
|
|
429
|
+
const thr = ops.scalarLike(confidenceThreshold, meanEnt);
|
|
430
|
+
const lt = ops.less(meanEnt, thr);
|
|
431
|
+
meanEnt.dispose();
|
|
432
|
+
thr.dispose();
|
|
433
|
+
const confident = ops.itemBool(lt);
|
|
434
|
+
lt.dispose();
|
|
435
|
+
return confident;
|
|
436
|
+
}
|
package/src/download.ts
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
// Files download sequentially (decode is bandwidth-bound, the network
|
|
16
16
|
// rarely is; resumability matters more than parallelism here).
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
mkdirSync, existsSync, statSync, createWriteStream, renameSync, rmSync,
|
|
20
|
+
symlinkSync, readFileSync, openSync, closeSync, readSync, writeSync,
|
|
21
|
+
} from "node:fs";
|
|
19
22
|
import { dirname, join } from "node:path";
|
|
20
23
|
import { DEFAULT_HUB } from "./registry";
|
|
21
24
|
|
|
@@ -54,6 +57,63 @@ export function hfToken(): string | null {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/** 401/403 almost always means a gated/private repo, not a broken network —
|
|
61
|
+
* say so instead of leaving the user staring at a bare status code. */
|
|
62
|
+
function authHint(status: number): string {
|
|
63
|
+
return status === 401 || status === 403
|
|
64
|
+
? " — gated repo? run `hf auth login` or set HF_TOKEN"
|
|
65
|
+
: "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- download lockfile -----------------------------------------------------
|
|
69
|
+
// Two writers on one blob (a foreground `mlx-bun get` racing the server's
|
|
70
|
+
// background auto-download) both append to the same .incomplete file and
|
|
71
|
+
// corrupt it. An O_EXCL .lock beside the .incomplete serializes them; a lock
|
|
72
|
+
// older than LOCK_STALE_MS (crashed owner) or whose pid is gone is stolen.
|
|
73
|
+
|
|
74
|
+
const LOCK_STALE_MS = 60 * 60 * 1000; // ~1 h: longer than any sane blob fetch
|
|
75
|
+
|
|
76
|
+
function lockIsStale(lockPath: string): boolean {
|
|
77
|
+
try {
|
|
78
|
+
const st = statSync(lockPath);
|
|
79
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
80
|
+
const pid = Number(readFileSync(lockPath, "utf8").split("\n")[0]);
|
|
81
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
82
|
+
try { process.kill(pid, 0); } catch { return true; } // owner is gone
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
} catch {
|
|
86
|
+
return true; // unreadable/vanished — treat as stale, O_EXCL still arbitrates
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Take `<blobPath>.lock` (O_EXCL). Returns a release fn, or throws the
|
|
91
|
+
* friendly "another download in progress" error when live-held. */
|
|
92
|
+
function acquireLock(blobPath: string, name: string): () => void {
|
|
93
|
+
const lockPath = `${blobPath}.lock`;
|
|
94
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
const fd = openSync(lockPath, "wx");
|
|
97
|
+
writeSync(fd, `${process.pid}\n${new Date().toISOString()}\n`);
|
|
98
|
+
closeSync(fd);
|
|
99
|
+
return () => { try { rmSync(lockPath, { force: true }); } catch {} };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
|
|
102
|
+
if (lockIsStale(lockPath)) {
|
|
103
|
+
try { rmSync(lockPath, { force: true }); } catch {}
|
|
104
|
+
continue; // one retry after stealing
|
|
105
|
+
}
|
|
106
|
+
let holder = "";
|
|
107
|
+
try { holder = ` (pid ${readFileSync(lockPath, "utf8").split("\n")[0]})`; } catch {}
|
|
108
|
+
throw new Error(
|
|
109
|
+
`another download of ${name} is in progress${holder} — ` +
|
|
110
|
+
`wait for it to finish, or delete ${lockPath} if you're sure it isn't`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`could not acquire ${lockPath}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
57
117
|
/** git blob identity: sha1("blob <size>\0" + content) — what blobId is. */
|
|
58
118
|
export function gitBlobSha1(bytes: Uint8Array): string {
|
|
59
119
|
const hasher = new Bun.CryptoHasher("sha1");
|
|
@@ -73,7 +133,10 @@ export async function listRepoFiles(
|
|
|
73
133
|
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
74
134
|
});
|
|
75
135
|
if (!res.ok)
|
|
76
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
`HF API ${res.status} for ${repoId}@${revision}: ${(await res.text()).slice(0, 200)}` +
|
|
138
|
+
authHint(res.status),
|
|
139
|
+
);
|
|
77
140
|
const body = (await res.json()) as {
|
|
78
141
|
sha: string;
|
|
79
142
|
siblings: { rfilename: string; size?: number; blobId?: string; lfs?: { oid: string; size: number } }[];
|
|
@@ -115,6 +178,21 @@ export async function downloadOne(
|
|
|
115
178
|
url: string, token: string | null, blobPath: string,
|
|
116
179
|
expected: { size: number; sha256?: string; sha1?: string; name: string },
|
|
117
180
|
onProgress?: DownloadOptions["onProgress"],
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
// Serialize concurrent writers (foreground get vs background auto-download)
|
|
183
|
+
// on this blob — both appending to one .incomplete corrupts it.
|
|
184
|
+
const release = acquireLock(blobPath, expected.name);
|
|
185
|
+
try {
|
|
186
|
+
await downloadOneLocked(url, token, blobPath, expected, onProgress);
|
|
187
|
+
} finally {
|
|
188
|
+
release();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function downloadOneLocked(
|
|
193
|
+
url: string, token: string | null, blobPath: string,
|
|
194
|
+
expected: { size: number; sha256?: string; sha1?: string; name: string },
|
|
195
|
+
onProgress?: DownloadOptions["onProgress"],
|
|
118
196
|
): Promise<void> {
|
|
119
197
|
const partPath = `${blobPath}.incomplete`;
|
|
120
198
|
let offset = existsSync(partPath) ? statSync(partPath).size : 0;
|
|
@@ -124,14 +202,27 @@ export async function downloadOne(
|
|
|
124
202
|
}
|
|
125
203
|
|
|
126
204
|
// Streaming verification — resume re-hashes the existing prefix so the
|
|
127
|
-
// final digest always covers every byte on disk.
|
|
205
|
+
// final digest always covers every byte on disk. Chunked reads: a resumed
|
|
206
|
+
// multi-GB blob must not be materialized as one allocation.
|
|
128
207
|
const sha256 = new Bun.CryptoHasher("sha256");
|
|
129
208
|
const sha1 = new Bun.CryptoHasher("sha1");
|
|
130
209
|
sha1.update(`blob ${expected.size}\0`);
|
|
131
210
|
if (offset > 0) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
211
|
+
const fd = openSync(partPath, "r");
|
|
212
|
+
try {
|
|
213
|
+
const buf = Buffer.alloc(8 * 2 ** 20);
|
|
214
|
+
let pos = 0;
|
|
215
|
+
while (pos < offset) {
|
|
216
|
+
const n = readSync(fd, buf, 0, Math.min(buf.length, offset - pos), pos);
|
|
217
|
+
if (n <= 0) break;
|
|
218
|
+
const chunk = buf.subarray(0, n);
|
|
219
|
+
sha256.update(chunk);
|
|
220
|
+
sha1.update(chunk);
|
|
221
|
+
pos += n;
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
closeSync(fd);
|
|
225
|
+
}
|
|
135
226
|
}
|
|
136
227
|
|
|
137
228
|
if (offset < expected.size) {
|
|
@@ -139,10 +230,10 @@ export async function downloadOne(
|
|
|
139
230
|
if (res.status === 200 && offset > 0) {
|
|
140
231
|
// server ignored Range — start the file (and hashes) over
|
|
141
232
|
rmSync(partPath);
|
|
142
|
-
return
|
|
233
|
+
return downloadOneLocked(url, token, blobPath, expected, onProgress);
|
|
143
234
|
}
|
|
144
235
|
if (res.status !== 200 && res.status !== 206)
|
|
145
|
-
throw new Error(`download failed (${res.status}) for ${expected.name}`);
|
|
236
|
+
throw new Error(`download failed (${res.status}) for ${expected.name}${authHint(res.status)}`);
|
|
146
237
|
const out = createWriteStream(partPath, { flags: offset > 0 ? "a" : "w" });
|
|
147
238
|
try {
|
|
148
239
|
for await (const chunk of res.body as ReadableStream<Uint8Array>) {
|