neurarch 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/bin/neurarch.mjs +75 -6
- package/engine.bundle.mjs +422 -16
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ npx neurarch rightsize jobs.csv # each past job's request next to what
|
|
|
9
9
|
npx neurarch Qwen/Qwen3-8B --batch 8 # the training peak for one model
|
|
10
10
|
npx neurarch oom error.txt --model Qwen/Qwen3-8B --batch 8 # why it ran out of memory, and the least invasive fix
|
|
11
11
|
npx neurarch Qwen/Qwen3-8B --inference --batch 4 --seq-len 4096 --gen 512 # weights + KV cache for a generation job
|
|
12
|
+
npx neurarch bert-base-uncased --seq-len 1024 --tokenizer-size 30524 # does the data fit what the config accepts
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
The memory number is calibrated against measured runs, and every run is public
|
|
@@ -117,6 +118,34 @@ prices. Nothing was trained and nothing was measured, which is why the verb is
|
|
|
117
118
|
"quoted": what it is good for is deciding whether to start the run, not
|
|
118
119
|
predicting what comes out of it.
|
|
119
120
|
|
|
121
|
+
## Does the data fit what the config accepts
|
|
122
|
+
|
|
123
|
+
Some jobs never reach their peak: they are admitted, wait for the card, load the
|
|
124
|
+
weights and then index past a table on the first batch. Each of these is
|
|
125
|
+
readable from the config before anything is reserved.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
$ npx neurarch bert-base-uncased --seq-len 1024 --tokenizer-size 30524
|
|
129
|
+
|
|
130
|
+
data fit checked: sequence length, tokenizer size
|
|
131
|
+
block Sequences of 1024 tokens are longer than the 512-row position table
|
|
132
|
+
fix: Truncate or chunk to 512 tokens, or pick a model whose context covers 1024.
|
|
133
|
+
block The tokenizer has 30524 ids and the embedding has 30522 rows
|
|
134
|
+
fix: Call model.resize_token_embeddings(len(tokenizer)) before training, and budget for the new rows.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| flag | checked against | when it blocks |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `--seq-len N` | the position scheme | past a learned position table (a rotary context only warns: it runs) |
|
|
140
|
+
| `--tokenizer-size N` | rows in the token embedding | more tokenizer ids than rows |
|
|
141
|
+
| `--classes N` | a `*ForSequenceClassification`-style head in the config, or a graph's final Linear | more labels than the head emits (fewer only warns) |
|
|
142
|
+
| `--image-size N` | the patch position table | any other size |
|
|
143
|
+
| `--channels N` | the first layer's input channels | any other count |
|
|
144
|
+
|
|
145
|
+
A field you do not state is not checked, and the output lists what could not be
|
|
146
|
+
compared and why, so a clean result never stands in for one nobody ran. A block
|
|
147
|
+
sets the exit code to 1. The same check is `data` on `POST /api/v1/check`.
|
|
148
|
+
|
|
120
149
|
## Which modules a LoRA can sit on
|
|
121
150
|
|
|
122
151
|
```
|
|
@@ -188,7 +217,20 @@ gpt2-pretrain,openai-community/gpt2,4xA100-80,16,24,1024,
|
|
|
188
217
|
`--prices prices.json` overrides the default list prices per GPU-hour
|
|
189
218
|
(`{"H100": 2.49, "A100-80": 1.79}`); cards it does not name keep theirs, and the
|
|
190
219
|
report says which were overridden. `--json` returns every row with the plan's
|
|
191
|
-
own `peakGb`, `smallestFitGpu`, `maxBatch` and `twoCardSplit
|
|
220
|
+
own `peakGb`, `smallestFitGpu`, `maxBatch` and `twoCardSplit`, plus `upperNeedGb` and
|
|
221
|
+
`fit`: a row whose capacity holds the estimate but not the conservative peak (the
|
|
222
|
+
estimate x 1.43, at or above the measured peak on 95% of held-out runs) is
|
|
223
|
+
`borderline`, says "measure once", and claims as not needed only what the
|
|
224
|
+
conservative peak would free.
|
|
225
|
+
|
|
226
|
+
**Three answers per card.** The training block prints each card green (the
|
|
227
|
+
conservative peak with 1.15x headroom fits), yellow `borderline` (the estimate
|
|
228
|
+
fits, the conservative peak does not: measure one step first) or red (the estimate
|
|
229
|
+
alone is past the card), then the upper bound, the largest batch it fits, and how
|
|
230
|
+
many copies of it fit each card and the smallest MIG slice on A100 / H100 (memory
|
|
231
|
+
only: compute and bandwidth are not considered). `--json` carries the same as
|
|
232
|
+
`cost.peakGbUpper`, `cost.upperBound`, `cost.gpuVerdict`, `cost.maxBatchSafe` and
|
|
233
|
+
`cost.shareable`; `cost.gpuFits` is unchanged.
|
|
192
234
|
|
|
193
235
|
**The memory model.** The peak is `buildPlan`'s training peak under AdamW with
|
|
194
236
|
bf16 autocast: weights, gradients and optimizer state plus activations (every
|
package/bin/neurarch.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* npx neurarch Qwen/Qwen3-8B --batch 8 --seq-len 8192 # training peak at that length, no price
|
|
11
11
|
* npx neurarch Qwen/Qwen3-8B --inference --batch 4 --seq-len 4096 --gen 512 # weights + KV cache, which cards hold it
|
|
12
12
|
* npx neurarch Qwen/Qwen3-8B --lora-targets # which modules a LoRA can sit on
|
|
13
|
+
* npx neurarch bert-base-uncased --seq-len 1024 --tokenizer-size 30524 # will this data crash it after it gets the GPU
|
|
13
14
|
* npx neurarch rightsize jobs.csv # requested vs needed GPU over past jobs
|
|
14
15
|
*
|
|
15
16
|
* The number this exists to print is the second one: parameters derived from
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
*
|
|
20
21
|
* Zero dependencies; the engine is bundled. Set HF_TOKEN for gated repos.
|
|
21
22
|
*/
|
|
22
|
-
import { inspectHFModel, keyFor, AGREE_TOLERANCE, priceHFModel, parseHumanSize, PREFLIGHT_GPUS, specFromFlags, renderLoraText, rightsize, renderMarkdown, headline, applyPrices, DEFAULT_CARDS, oomReport, renderOomText, readAttention, attentionPhrase, readSeqLen, DEFAULT_SEQ_LEN, inferHFModel, readInferencePrecision, INFERENCE_PRECISIONS, DEFAULT_GEN_TOKENS, DEFAULT_INFERENCE_BATCH, CHANGE_HEADROOM } from '../engine.bundle.mjs';
|
|
23
|
+
import { inspectHFModel, keyFor, AGREE_TOLERANCE, priceHFModel, parseHumanSize, PREFLIGHT_GPUS, specFromFlags, renderLoraText, rightsize, renderMarkdown, headline, applyPrices, DEFAULT_CARDS, oomReport, renderOomText, readAttention, attentionPhrase, readSeqLen, DEFAULT_SEQ_LEN, inferHFModel, readInferencePrecision, INFERENCE_PRECISIONS, DEFAULT_GEN_TOKENS, DEFAULT_INFERENCE_BATCH, CHANGE_HEADROOM, storedTwiceSentence, dataFitHFModel } from '../engine.bundle.mjs';
|
|
23
24
|
/** The channel tag on every link this program prints. `docs/DISTRIBUTION.md`
|
|
24
25
|
* has asked for one since the first release: without it a visit from a tool
|
|
25
26
|
* and a visit from a search result are the same row, and "which channel brings
|
|
@@ -197,7 +198,7 @@ for gated repos); nothing is uploaded.
|
|
|
197
198
|
|
|
198
199
|
// `--data 3M` and `--gpu a100` take a value, so their value must not fall
|
|
199
200
|
// through to `positional` and be read as a second model id.
|
|
200
|
-
const VALUED = new Set(['--data', '--gpu', '--lora', '--quant', '--fit', '--serve', '--batch', '--attention', '--seq-len', '--gen', '--precision']);
|
|
201
|
+
const VALUED = new Set(['--data', '--gpu', '--lora', '--quant', '--fit', '--serve', '--batch', '--attention', '--seq-len', '--gen', '--precision', '--tokenizer-size', '--classes', '--image-size', '--channels']);
|
|
201
202
|
const flags = new Set();
|
|
202
203
|
const opts = {};
|
|
203
204
|
const positional = [];
|
|
@@ -234,6 +235,11 @@ if (flags.has('-h') || flags.has('--help') || positional.length === 0) {
|
|
|
234
235
|
the change record: what may move on this base, and whether the gate approves it
|
|
235
236
|
npx neurarch <model-id> --lora-targets every Linear a LoRA can target, and PEFT target_modules
|
|
236
237
|
for the attention and all-linear presets with r=8 / r=16 totals
|
|
238
|
+
npx neurarch <model-id> --seq-len 1024 --tokenizer-size 30524 --classes 5 [--image-size 384 --channels 1]
|
|
239
|
+
does YOUR data fit what the config accepts: sequence vs the
|
|
240
|
+
position table, tokenizer vs the embedding, labels vs the head,
|
|
241
|
+
image size and channels vs the first layer. These crash on the
|
|
242
|
+
first batch, after the reservation; this says so before it.
|
|
237
243
|
|
|
238
244
|
--data takes the size of YOUR training set (3M, 500k, 2B). Without it there is
|
|
239
245
|
no price, because a cost with no workload behind it is a cost for somebody
|
|
@@ -303,7 +309,21 @@ if (opts.gen != null) {
|
|
|
303
309
|
const inferOpts = { batch: batchSize, promptTokens: seqLen, genTokens, precision };
|
|
304
310
|
/** The training-peak block runs on any of these; `--inference` replaces it. */
|
|
305
311
|
const trainingSizing = !inference && (opts.data != null || batchSize != null || seqLen != null || opts.attention != null);
|
|
306
|
-
|
|
312
|
+
// The data the caller describes. `--seq-len` is the same number the peak is
|
|
313
|
+
// sized at, so it is checked against the position scheme whenever it is given.
|
|
314
|
+
const DATA_FLAGS = [['seq-len', 'seqLen'], ['tokenizer-size', 'tokenizerSize'], ['classes', 'numClasses'], ['image-size', 'imageSize'], ['channels', 'channels']];
|
|
315
|
+
const dataSpec = {};
|
|
316
|
+
for (const [flag, key] of DATA_FLAGS) {
|
|
317
|
+
if (opts[flag] == null) continue;
|
|
318
|
+
const n = Number(opts[flag]);
|
|
319
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
320
|
+
process.stderr.write(`\n ${red('cannot read')} --${flag} ${opts[flag]}. Use a positive integer.\n`);
|
|
321
|
+
process.exit(2);
|
|
322
|
+
}
|
|
323
|
+
dataSpec[key] = n;
|
|
324
|
+
}
|
|
325
|
+
const dataAsked = Object.keys(dataSpec).length > 0;
|
|
326
|
+
const sizingAsked = inference || trainingSizing || opts.lora != null || dataAsked;
|
|
307
327
|
|
|
308
328
|
const modelId = positional[0];
|
|
309
329
|
const result = await inspectHFModel(modelId);
|
|
@@ -332,6 +352,7 @@ if (flags.has('--json')) {
|
|
|
332
352
|
const spec = specFromFlags({ lora: opts.lora, quant: opts.quant, fit: opts.fit, serve: opts.serve });
|
|
333
353
|
if (spec) out.change = priceHFModel(f, { samples: 50_000, source: 'assumed' }, runFrom(gpuFromFlag(opts.gpu)), spec, attention, seqLen).plan.change;
|
|
334
354
|
if (flags.has('--lora-targets')) out.lora = priceHFModel(f, { samples: 50_000, source: 'assumed' }).plan.lora;
|
|
355
|
+
if (dataAsked) out.dataFit = dataFitHFModel(f, dataSpec);
|
|
335
356
|
return out;
|
|
336
357
|
};
|
|
337
358
|
const payload = result.ok
|
|
@@ -341,6 +362,7 @@ if (flags.has('--json')) {
|
|
|
341
362
|
compare: `https://neurarch.com/compare?a=${encodeURIComponent(modelId)}&b=&utm_source=${SOURCE}`,
|
|
342
363
|
publishedParams: result.hub.publishedParams,
|
|
343
364
|
derivedParams: result.facts.derivedParams,
|
|
365
|
+
storedTwice: result.facts.storedTwice,
|
|
344
366
|
ratio: result.facts.ratio,
|
|
345
367
|
absDelta: result.facts.absDelta,
|
|
346
368
|
tolerance: AGREE_TOLERANCE,
|
|
@@ -364,7 +386,7 @@ if (flags.has('--json')) {
|
|
|
364
386
|
}
|
|
365
387
|
: { ok: false, modelId, reason: result.reason };
|
|
366
388
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
367
|
-
process.exit(payload.ok ? 0 : 1);
|
|
389
|
+
process.exit(payload.ok && !(payload.dataFit?.findings ?? []).some((f) => f.severity === 'block') ? 0 : 1);
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
if (!result.ok && !sizingOnly) {
|
|
@@ -404,6 +426,12 @@ if (sizingOnly) {
|
|
|
404
426
|
// The two numbers, first, because nothing below matters if they disagree.
|
|
405
427
|
process.stdout.write(` ${dim('published (safetensors.total)')} ${bold(humanParams(hub.publishedParams))} ${dim(hub.publishedParams.toLocaleString())}\n`);
|
|
406
428
|
process.stdout.write(` ${dim('derived (config.json alone) ')} ${bold(humanParams(facts.derivedParams))} ${dim(facts.derivedParams.toLocaleString())}\n`);
|
|
429
|
+
// A tied matrix the file writes twice: read from the safetensors header,
|
|
430
|
+
// and the delta below already counts it. Same sentence the /m/ page prints.
|
|
431
|
+
if (facts.storedTwice) {
|
|
432
|
+
process.stdout.write(` ${dim('stored twice in the file ')} ${bold(`+${humanParams(facts.storedTwice.extra)}`)} ${dim(facts.storedTwice.extra.toLocaleString())}\n`);
|
|
433
|
+
process.stdout.write(` ${yellow('!')} ${storedTwiceSentence(facts.storedTwice)}\n`);
|
|
434
|
+
}
|
|
407
435
|
|
|
408
436
|
const deltaLine = `${sign}${pct.toFixed(2)}%`;
|
|
409
437
|
if (facts.comparability !== 'comparable') {
|
|
@@ -435,6 +463,24 @@ if (sizingOnly) {
|
|
|
435
463
|
}
|
|
436
464
|
}
|
|
437
465
|
|
|
466
|
+
// ── Does the data fit what the config accepts ───────────────────────────────
|
|
467
|
+
//
|
|
468
|
+
// Before memory, because a job that indexes past a table on its first batch
|
|
469
|
+
// does not get far enough for its peak to matter.
|
|
470
|
+
if (dataAsked) {
|
|
471
|
+
const fit = dataFitHFModel(facts, dataSpec);
|
|
472
|
+
const blocks = fit.findings.filter((f) => f.severity === 'block');
|
|
473
|
+
process.stdout.write(`\n ${bold('data fit')} ${dim(`checked: ${fit.checked.join(', ') || 'nothing'}`)}\n`);
|
|
474
|
+
if (!fit.findings.length && fit.checked.length) process.stdout.write(` ${green('fits')} ${dim('every stated field is within what the config accepts')}\n`);
|
|
475
|
+
for (const f of fit.findings) {
|
|
476
|
+
process.stdout.write(` ${f.severity === 'block' ? red('block') : yellow('warn ')} ${f.title}\n`);
|
|
477
|
+
process.stdout.write(` ${dim(f.detail)}\n`);
|
|
478
|
+
process.stdout.write(` ${dim(`fix: ${f.fix}`)}\n`);
|
|
479
|
+
}
|
|
480
|
+
for (const s of fit.skipped) process.stdout.write(` ${dim(`not checked: ${s.check} (${s.reason})`)}\n`);
|
|
481
|
+
for (const b of blocks) blockers.push({ title: b.title });
|
|
482
|
+
}
|
|
483
|
+
|
|
438
484
|
// ── What an inference job needs ────────────────────────────────────────────
|
|
439
485
|
//
|
|
440
486
|
// Weights at the stated precision plus the KV cache at the last generated
|
|
@@ -503,14 +549,37 @@ if (trainingSizing) {
|
|
|
503
549
|
} else {
|
|
504
550
|
process.stdout.write(`\n ${bold('training memory')} ${dim('(no price without --data)')}\n`);
|
|
505
551
|
}
|
|
506
|
-
|
|
507
|
-
|
|
552
|
+
// Three answers per card: green fits on the conservative peak, yellow
|
|
553
|
+
// borderline (the estimate fits, the conservative peak does not: measure
|
|
554
|
+
// once), red does not fit. An older engine without `gpuVerdict` falls back
|
|
555
|
+
// to the point-estimate boolean.
|
|
556
|
+
const fits = Object.entries(cost.gpuVerdict ?? cost.gpuFits)
|
|
557
|
+
.map(([card, v]) => (v === null ? dim(`${card} n/a`) : v === 'fits' || v === true ? green(`${card} yes`) : v === 'borderline' ? yellow(`${card} borderline`) : red(`${card} no`)))
|
|
508
558
|
.join(' ');
|
|
509
559
|
const batchShown = /at batch (\d+)/.exec(cost.assumptions)?.[1] ?? '32';
|
|
510
560
|
// The sequence length is printed because it moves the peak more than the
|
|
511
561
|
// batch does: a config's context window is not a training length.
|
|
512
562
|
const tokens = cost.seqLen != null ? `, ${cost.seqLen} tokens` : '';
|
|
513
563
|
process.stdout.write(` ${dim('fits ')} ${fits} ${dim(`(${opts.lora != null ? 'full fine-tune ' : ''}training peak ~${cost.peakGb.toFixed(1)} GB at batch ${batchShown}${tokens}${cost.activationGb == null ? ', activations unknown' : ''}, 1.15x headroom)`)}\n`);
|
|
564
|
+
if (cost.peakGbUpper > 0) {
|
|
565
|
+
const u = cost.upperBound;
|
|
566
|
+
process.stdout.write(` ${dim('upper bound ')} ${cost.peakGbUpper.toFixed(1)} GB ${dim(`(the estimate x ${u.multiplier}: held out by architecture, ${Math.round(u.coverage * 100)}% of ${u.runs} measured runs peaked at or under it)`)}\n`);
|
|
567
|
+
if (Object.values(cost.gpuVerdict ?? {}).includes('borderline')) {
|
|
568
|
+
process.stdout.write(` ${dim('borderline ')} ${yellow('the estimate fits, the upper bound with 1.15x headroom does not: measure once (one step at this batch) before reserving')}\n`);
|
|
569
|
+
}
|
|
570
|
+
const mbs = cost.maxBatchSafe;
|
|
571
|
+
if (mbs && (mbs.T4 != null || mbs.A100_40 != null || mbs.H100_80 != null)) {
|
|
572
|
+
const v = (x) => (x == null ? 'n/a' : x.toLocaleString('en-US'));
|
|
573
|
+
process.stdout.write(` ${dim('safe batch ')} T4 ${v(mbs.T4)} A100_40 ${v(mbs.A100_40)} H100_80 ${v(mbs.H100_80)} ${dim('(largest batch the upper bound fits)')}\n`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (cost.shareable) {
|
|
577
|
+
const sh = cost.shareable;
|
|
578
|
+
const pick = sh.cards.filter((c) => ['T4 (16GB)', 'A100 (40GB)', 'H100 (80GB)'].includes(c.gpu)).map((c) => `${c.copies} on ${gpuSlug(c.gpu)}`).join(', ');
|
|
579
|
+
const mig = [['A100-40', sh.mig.A100_40], ['A100-80', sh.mig.A100_80], ['H100', sh.mig.H100_80]].filter(([, p]) => p).map(([g, p]) => `${g} ${p}`).join(', ');
|
|
580
|
+
const anyCopy = sh.cards.some((c) => c.copies > 0);
|
|
581
|
+
process.stdout.write(` ${dim('shareable ')} ${anyCopy ? pick : 'no listed card holds one copy'}${mig ? `; smallest MIG slice ${mig}` : ''} ${dim(`(${sh.perCopyGb.toFixed(1)} GB a copy; memory only, compute and bandwidth not considered)`)}\n`);
|
|
582
|
+
}
|
|
514
583
|
if (opts.lora != null) process.stdout.write(` ${dim('adapter ')} ${dim('the change record below sizes the LoRA run: frozen bf16 base, adapters with AdamW state, the same activations')}\n`);
|
|
515
584
|
if (cost.seqLen != null && seqLen == null) {
|
|
516
585
|
process.stdout.write(` ${dim('tokens ')} ${dim(`${cost.seqLen} assumed (default ${DEFAULT_SEQ_LEN}, or the context if shorter); --seq-len to change`)}\n`);
|
package/engine.bundle.mjs
CHANGED
|
@@ -32737,16 +32737,16 @@ async function fetchDiffusersConfig(modelId) {
|
|
|
32737
32737
|
}
|
|
32738
32738
|
async function fetchHFModelConfig(modelId) {
|
|
32739
32739
|
try {
|
|
32740
|
-
const
|
|
32740
|
+
const authHeaders2 = hfAuthHeaders();
|
|
32741
32741
|
const configResponse = await fetch(
|
|
32742
32742
|
`https://huggingface.co/${modelId}/resolve/main/config.json`,
|
|
32743
|
-
{ headers:
|
|
32743
|
+
{ headers: authHeaders2 }
|
|
32744
32744
|
);
|
|
32745
32745
|
if (configResponse.ok) {
|
|
32746
32746
|
const raw = await configResponse.json();
|
|
32747
32747
|
const config2 = normalizeAndAugmentConfig(raw, modelId);
|
|
32748
32748
|
config2._configSource = "config";
|
|
32749
|
-
config2._realParamCount = await fetchHFRealParamCount(modelId,
|
|
32749
|
+
config2._realParamCount = await fetchHFRealParamCount(modelId, authHeaders2);
|
|
32750
32750
|
return config2;
|
|
32751
32751
|
}
|
|
32752
32752
|
const diffusion = await fetchDiffusersConfig(modelId);
|
|
@@ -32754,7 +32754,7 @@ async function fetchHFModelConfig(modelId) {
|
|
|
32754
32754
|
diffusion._configSource = diffusion._configSource ?? "diffusers";
|
|
32755
32755
|
return diffusion;
|
|
32756
32756
|
}
|
|
32757
|
-
const apiResponse = await fetch(`https://huggingface.co/api/models/${modelId}`, { headers:
|
|
32757
|
+
const apiResponse = await fetch(`https://huggingface.co/api/models/${modelId}`, { headers: authHeaders2 });
|
|
32758
32758
|
const modelInfo = apiResponse.ok ? await apiResponse.json() : {};
|
|
32759
32759
|
const modelIdLower = modelId.toLowerCase();
|
|
32760
32760
|
const pipelineTag = modelInfo.pipeline_tag ?? "";
|
|
@@ -41719,8 +41719,50 @@ var MEMORY_CALIBRATION = {
|
|
|
41719
41719
|
noFit: 131,
|
|
41720
41720
|
/** Runs measured after the factors were fixed, never in any fit, and the median miss on the ones that fit, percent. */
|
|
41721
41721
|
afterFitRuns: 25,
|
|
41722
|
-
afterFitMedianPct: 14
|
|
41722
|
+
afterFitMedianPct: 14,
|
|
41723
|
+
/**
|
|
41724
|
+
* The upper multiplier U: held out one architecture at a time, measured peak
|
|
41725
|
+
* / predicted peak over the fitted runs of 1 GB or more, at the 90th and
|
|
41726
|
+
* 95th percentile (nearest rank), rounded up to two decimals. Predicted x U
|
|
41727
|
+
* is at or above the measured peak on that share of held-out runs.
|
|
41728
|
+
*/
|
|
41729
|
+
upperK90: 1.17,
|
|
41730
|
+
upperK95: 1.43,
|
|
41731
|
+
/** Held-out runs (1 GB or more, measured peak) the multipliers were read from. */
|
|
41732
|
+
upperRuns: 91
|
|
41723
41733
|
};
|
|
41734
|
+
var UPPER_K = MEMORY_CALIBRATION.upperK95;
|
|
41735
|
+
var UPPER_COVERAGE = 0.95;
|
|
41736
|
+
var UPPER_FLOOR_GB = 1;
|
|
41737
|
+
function upperPeakGb(peakGb) {
|
|
41738
|
+
if (!(peakGb > 0)) return 0;
|
|
41739
|
+
return Math.max(peakGb * UPPER_K, UPPER_FLOOR_GB);
|
|
41740
|
+
}
|
|
41741
|
+
function fitVerdict(peakGb, params, gb2) {
|
|
41742
|
+
if (params <= 0) return null;
|
|
41743
|
+
if (upperPeakGb(peakGb) * FIT_HEADROOM <= gb2) return "fits";
|
|
41744
|
+
return peakGb <= gb2 ? "borderline" : "does_not_fit";
|
|
41745
|
+
}
|
|
41746
|
+
var MIG_PROFILES = {
|
|
41747
|
+
A100_40: [{ name: "1g.5gb", gb: 5 }, { name: "1g.10gb", gb: 10 }, { name: "2g.10gb", gb: 10 }, { name: "3g.20gb", gb: 20 }, { name: "4g.20gb", gb: 20 }, { name: "7g.40gb", gb: 40 }],
|
|
41748
|
+
A100_80: [{ name: "1g.10gb", gb: 10 }, { name: "1g.20gb", gb: 20 }, { name: "2g.20gb", gb: 20 }, { name: "3g.40gb", gb: 40 }, { name: "4g.40gb", gb: 40 }, { name: "7g.80gb", gb: 80 }],
|
|
41749
|
+
H100_80: [{ name: "1g.10gb", gb: 10 }, { name: "1g.20gb", gb: 20 }, { name: "2g.20gb", gb: 20 }, { name: "3g.40gb", gb: 40 }, { name: "4g.40gb", gb: 40 }, { name: "7g.80gb", gb: 80 }]
|
|
41750
|
+
};
|
|
41751
|
+
var MIG_SOURCE = "NVIDIA Multi-Instance GPU User Guide, Supported MIG Profiles";
|
|
41752
|
+
var SHAREABLE_NOTE = "Memory only: copies of the conservative peak (upper bound x 1.15 headroom each) that fit the card's memory. Compute, memory bandwidth and the per-process CUDA context are not considered.";
|
|
41753
|
+
function shareableFor(peakGbUpper) {
|
|
41754
|
+
if (!(peakGbUpper > 0)) return null;
|
|
41755
|
+
const per = peakGbUpper * FIT_HEADROOM;
|
|
41756
|
+
const mig = (k) => MIG_PROFILES[k].find((p) => per <= p.gb)?.name ?? null;
|
|
41757
|
+
return {
|
|
41758
|
+
perCopyGb: per,
|
|
41759
|
+
cards: PREFLIGHT_GPUS.map((g) => ({ gpu: g.name, gb: g.gb, copies: Math.floor(g.gb / per) })),
|
|
41760
|
+
mig: { A100_40: mig("A100_40"), A100_80: mig("A100_80"), H100_80: mig("H100_80") },
|
|
41761
|
+
migSource: MIG_SOURCE,
|
|
41762
|
+
memoryOnly: true,
|
|
41763
|
+
note: SHAREABLE_NOTE
|
|
41764
|
+
};
|
|
41765
|
+
}
|
|
41724
41766
|
var ACTIVATION_BYTES_PER_ELEMENT = 2;
|
|
41725
41767
|
var ATTENTION_TYPES2 = /attention|transformerEncoder|transformerDecoder|^mla$|ditBlock|swin/i;
|
|
41726
41768
|
var ALWAYS_STORES_SCORES = /^hstuBlock$/;
|
|
@@ -41938,6 +41980,10 @@ function fitsCard(peakGb, params, gb2) {
|
|
|
41938
41980
|
if (params <= 0) return null;
|
|
41939
41981
|
return peakGb * FIT_HEADROOM <= gb2;
|
|
41940
41982
|
}
|
|
41983
|
+
function smallestSafeCardFor(peakGb, params) {
|
|
41984
|
+
return PREFLIGHT_GPUS.find((g) => fitVerdict(peakGb, params, g.gb) === "fits")?.name ?? null;
|
|
41985
|
+
}
|
|
41986
|
+
var UPPER_BOUND = { multiplier: UPPER_K, coverage: UPPER_COVERAGE, runs: MEMORY_CALIBRATION.upperRuns, multiplier90: MEMORY_CALIBRATION.upperK90 };
|
|
41941
41987
|
var ATTENTION_LABEL = { sdpa: "SDPA", flash: "flash attention", eager: "eager" };
|
|
41942
41988
|
function attentionPhrase(attention, scoresGb) {
|
|
41943
41989
|
if (scoresGb == null || !(scoresGb > 0.05)) return "";
|
|
@@ -42034,7 +42080,11 @@ function summaryFor(f, plan, diff, withheld) {
|
|
|
42034
42080
|
parts.push(f.warnings.length ? `will run, ${f.warnings.length} warning${f.warnings.length === 1 ? "" : "s"}` : "will run");
|
|
42035
42081
|
const fits = plan.cost.gpuFits;
|
|
42036
42082
|
const smallest = plan.cost.smallestFitGpu ? `${/^[AEIOU]/i.test(plan.cost.smallestFitGpu) ? "an" : "a"} ${plan.cost.smallestFitGpu}` : fits.T4 ? "a T4" : fits.A100_40 ? "an A100 40GB" : fits.H100_80 ? "an H100 80GB" : null;
|
|
42037
|
-
|
|
42083
|
+
const article = (g) => `${/^[AEIOU]/i.test(g) ? "an" : "a"} ${g}`;
|
|
42084
|
+
const safe = plan.cost.smallestSafeGpu;
|
|
42085
|
+
if (smallest && plan.cost.smallestFitGpu && safe !== plan.cost.smallestFitGpu) {
|
|
42086
|
+
parts.push(`fits ${smallest} at batch ${batchOf(plan)}, borderline there (measure once; ${safe ? `${article(safe)} holds the upper bound` : "no listed card holds the upper bound"})`);
|
|
42087
|
+
} else if (smallest) parts.push(`fits ${smallest} at batch ${batchOf(plan)}`);
|
|
42038
42088
|
else if (fits.T4 === false) parts.push("does not fit a single 80GB card");
|
|
42039
42089
|
if (f.estCostUsd != null) parts.push(`about ${fmtUsd(f.estCostUsd)} to train`);
|
|
42040
42090
|
}
|
|
@@ -42096,6 +42146,12 @@ function unreadablePlan(blockers) {
|
|
|
42096
42146
|
peakGb: 0,
|
|
42097
42147
|
activationGb: null,
|
|
42098
42148
|
smallestFitGpu: null,
|
|
42149
|
+
peakGbUpper: 0,
|
|
42150
|
+
upperBound: UPPER_BOUND,
|
|
42151
|
+
gpuVerdict: { T4: null, A100_40: null, H100_80: null },
|
|
42152
|
+
smallestSafeGpu: null,
|
|
42153
|
+
maxBatchSafe: { T4: null, A100_40: null, H100_80: null },
|
|
42154
|
+
shareable: null,
|
|
42099
42155
|
maxBatch: { T4: null, A100_40: null, H100_80: null },
|
|
42100
42156
|
twoCardSplit: null,
|
|
42101
42157
|
attention: DEFAULT_ATTENTION,
|
|
@@ -42155,6 +42211,22 @@ function buildPlanUnchecked(model, base, opts) {
|
|
|
42155
42211
|
A100_40: maxBatchFor(batchFacts, gpuGb("A100 (40GB)", 40)),
|
|
42156
42212
|
H100_80: maxBatchFor(batchFacts, gpuGb("H100", 80))
|
|
42157
42213
|
} : { T4: null, A100_40: null, H100_80: null };
|
|
42214
|
+
const peakGbUpper = f.params > 0 ? upperPeakGb(f.peakGb) : 0;
|
|
42215
|
+
const gpuVerdict = {
|
|
42216
|
+
T4: fitVerdict(f.peakGb, f.params, gpuGb("T4", 16)),
|
|
42217
|
+
A100_40: fitVerdict(f.peakGb, f.params, gpuGb("A100 (40GB)", 40)),
|
|
42218
|
+
H100_80: fitVerdict(f.peakGb, f.params, gpuGb("H100", 80))
|
|
42219
|
+
};
|
|
42220
|
+
const safeFacts = {
|
|
42221
|
+
footprintGb: f.footprintGb * UPPER_K,
|
|
42222
|
+
activationPerSampleGb: elems == null ? null : activationGbAt(elems, 1) * UPPER_K,
|
|
42223
|
+
activationGbAt: elems == null ? void 0 : (b) => activationGbAt(elems, b) * UPPER_K
|
|
42224
|
+
};
|
|
42225
|
+
const maxBatchSafe = f.params > 0 ? {
|
|
42226
|
+
T4: maxBatchFor(safeFacts, gpuGb("T4", 16)),
|
|
42227
|
+
A100_40: maxBatchFor(safeFacts, gpuGb("A100 (40GB)", 40)),
|
|
42228
|
+
H100_80: maxBatchFor(safeFacts, gpuGb("H100", 80))
|
|
42229
|
+
} : { T4: null, A100_40: null, H100_80: null };
|
|
42158
42230
|
const cost = {
|
|
42159
42231
|
estCostUsd: f.estCostUsd,
|
|
42160
42232
|
estTrainSec: f.estTrainSec,
|
|
@@ -42164,6 +42236,12 @@ function buildPlanUnchecked(model, base, opts) {
|
|
|
42164
42236
|
fitsGpu: f.fitsGpu,
|
|
42165
42237
|
gpuFits,
|
|
42166
42238
|
peakGb: f.peakGb,
|
|
42239
|
+
peakGbUpper,
|
|
42240
|
+
upperBound: UPPER_BOUND,
|
|
42241
|
+
gpuVerdict,
|
|
42242
|
+
smallestSafeGpu: f.params > 0 ? smallestSafeCardFor(f.peakGb, f.params) : null,
|
|
42243
|
+
maxBatchSafe,
|
|
42244
|
+
shareable: f.params > 0 ? shareableFor(peakGbUpper) : null,
|
|
42167
42245
|
activationGb: f.activationGb,
|
|
42168
42246
|
smallestFitGpu: f.smallestFitGpu,
|
|
42169
42247
|
maxBatch,
|
|
@@ -43530,6 +43608,124 @@ init_toastStore();
|
|
|
43530
43608
|
// scripts/architecture-pages.ts
|
|
43531
43609
|
var STRUCTURAL_CHECKS = ADVISOR_RULE_IDS.length + GUARDRAIL_GATE_IDS.length;
|
|
43532
43610
|
|
|
43611
|
+
// scripts/storedHead.ts
|
|
43612
|
+
var TIE_DEFAULT_FALSE = /* @__PURE__ */ new Set([
|
|
43613
|
+
"llama",
|
|
43614
|
+
"mistral",
|
|
43615
|
+
"mixtral",
|
|
43616
|
+
"qwen2",
|
|
43617
|
+
"qwen2_moe",
|
|
43618
|
+
"qwen3",
|
|
43619
|
+
"qwen3_moe",
|
|
43620
|
+
"phi",
|
|
43621
|
+
"phi3",
|
|
43622
|
+
"olmo",
|
|
43623
|
+
"olmo2",
|
|
43624
|
+
"granite",
|
|
43625
|
+
"stablelm",
|
|
43626
|
+
"deepseek_v3",
|
|
43627
|
+
"gpt_neox"
|
|
43628
|
+
]);
|
|
43629
|
+
var LM_SUBCONFIG = /^(text|llm|language|language_model|thinker|decoder)_config$/;
|
|
43630
|
+
function lmSubConfigs(config, depth = 0) {
|
|
43631
|
+
if (depth > 3 || !config || typeof config !== "object") return [];
|
|
43632
|
+
const out = [];
|
|
43633
|
+
for (const [k, v] of Object.entries(config)) {
|
|
43634
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
43635
|
+
if (LM_SUBCONFIG.test(k)) out.push(v);
|
|
43636
|
+
out.push(...lmSubConfigs(v, depth + 1));
|
|
43637
|
+
}
|
|
43638
|
+
return out;
|
|
43639
|
+
}
|
|
43640
|
+
function isTied(config) {
|
|
43641
|
+
if (typeof config.tie_word_embeddings === "boolean") return config.tie_word_embeddings;
|
|
43642
|
+
const subs = lmSubConfigs(config);
|
|
43643
|
+
const nested = subs.map((s) => s.tie_word_embeddings).filter((v) => typeof v === "boolean");
|
|
43644
|
+
if (nested.length) return nested.every(Boolean);
|
|
43645
|
+
const types = [config, ...subs].map((c) => String(c.model_type ?? ""));
|
|
43646
|
+
return !types.some((t) => TIE_DEFAULT_FALSE.has(t));
|
|
43647
|
+
}
|
|
43648
|
+
var HEAD = /(^|\.)(lm_head|embed_out)\.weight$/;
|
|
43649
|
+
var EMBED = /(^|\.)(embed_tokens|wte|word_embeddings|tok_embeddings|embed_in)\.weight$/;
|
|
43650
|
+
function isCandidate(name) {
|
|
43651
|
+
return HEAD.test(name) || EMBED.test(name);
|
|
43652
|
+
}
|
|
43653
|
+
function storedTwice(config, tensors) {
|
|
43654
|
+
if (!isTied(config)) return null;
|
|
43655
|
+
const heads = tensors.filter((t) => HEAD.test(t.name));
|
|
43656
|
+
const embeds = tensors.filter((t) => EMBED.test(t.name));
|
|
43657
|
+
for (const h of heads) {
|
|
43658
|
+
const e = embeds.find((x) => x.shape.length === h.shape.length && x.shape.every((d, i) => d === h.shape[i]));
|
|
43659
|
+
if (e) {
|
|
43660
|
+
return { extra: h.shape.reduce((a, b) => a * b, 1), head: h.name, embedding: e.name, shape: [...h.shape] };
|
|
43661
|
+
}
|
|
43662
|
+
}
|
|
43663
|
+
return null;
|
|
43664
|
+
}
|
|
43665
|
+
var HF = "https://huggingface.co";
|
|
43666
|
+
function authHeaders() {
|
|
43667
|
+
const token = typeof process !== "undefined" ? process.env?.HF_TOKEN?.trim() : void 0;
|
|
43668
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
43669
|
+
}
|
|
43670
|
+
async function readSafetensorsHeader(url, fetchImpl = fetch) {
|
|
43671
|
+
try {
|
|
43672
|
+
const r1 = await fetchImpl(url, { headers: { ...authHeaders(), Range: "bytes=0-7" } });
|
|
43673
|
+
if (!r1.ok) return null;
|
|
43674
|
+
const lenBuf = new Uint8Array(await r1.arrayBuffer()).slice(0, 8);
|
|
43675
|
+
if (lenBuf.length < 8) return null;
|
|
43676
|
+
const n2 = Number(new DataView(lenBuf.buffer).getBigUint64(0, true));
|
|
43677
|
+
if (!(n2 > 0 && n2 < 1e8)) return null;
|
|
43678
|
+
const r2 = await fetchImpl(url, { headers: { ...authHeaders(), Range: `bytes=8-${8 + n2 - 1}` } });
|
|
43679
|
+
if (!r2.ok) return null;
|
|
43680
|
+
const header = JSON.parse(new TextDecoder().decode(await r2.arrayBuffer()));
|
|
43681
|
+
return Object.entries(header).filter(([k]) => k !== "__metadata__").map(([name, v]) => ({ name, shape: v?.shape ?? [] }));
|
|
43682
|
+
} catch {
|
|
43683
|
+
return null;
|
|
43684
|
+
}
|
|
43685
|
+
}
|
|
43686
|
+
async function readStoredHead(modelId, fetchImpl = fetch) {
|
|
43687
|
+
let info;
|
|
43688
|
+
try {
|
|
43689
|
+
const r = await fetchImpl(`${HF}/api/models/${modelId}`, { headers: authHeaders() });
|
|
43690
|
+
if (!r.ok) return { error: `Hub API answered ${r.status}` };
|
|
43691
|
+
info = await r.json();
|
|
43692
|
+
} catch (e) {
|
|
43693
|
+
return { error: `Hub API unreachable (${e.message})` };
|
|
43694
|
+
}
|
|
43695
|
+
const revision = String(info?.sha ?? "main");
|
|
43696
|
+
const names = (info?.siblings ?? []).map((s) => String(s.rfilename));
|
|
43697
|
+
const base = `${HF}/${modelId}/resolve/${revision}`;
|
|
43698
|
+
let files;
|
|
43699
|
+
if (names.includes("model.safetensors.index.json")) {
|
|
43700
|
+
try {
|
|
43701
|
+
const r = await fetchImpl(`${base}/model.safetensors.index.json`, { headers: authHeaders() });
|
|
43702
|
+
if (!r.ok) return { error: `index answered ${r.status}` };
|
|
43703
|
+
const idx = await r.json();
|
|
43704
|
+
files = [...new Set(Object.entries(idx.weight_map ?? {}).filter(([k]) => isCandidate(k)).map(([, f]) => f))];
|
|
43705
|
+
} catch (e) {
|
|
43706
|
+
return { error: `index unreadable (${e.message})` };
|
|
43707
|
+
}
|
|
43708
|
+
} else {
|
|
43709
|
+
files = names.filter((n2) => n2.endsWith(".safetensors") && !n2.includes("/"));
|
|
43710
|
+
}
|
|
43711
|
+
if (!files.length) return { error: "no root-level safetensors file" };
|
|
43712
|
+
const tensors = [];
|
|
43713
|
+
for (const f of files.sort()) {
|
|
43714
|
+
const t = await readSafetensorsHeader(`${base}/${f}`, fetchImpl);
|
|
43715
|
+
if (!t) return { error: `header of ${f} unreadable` };
|
|
43716
|
+
tensors.push(...t.filter((x) => isCandidate(x.name)));
|
|
43717
|
+
}
|
|
43718
|
+
tensors.sort((a, b) => a.name.localeCompare(b.name));
|
|
43719
|
+
return {
|
|
43720
|
+
modelId,
|
|
43721
|
+
revision,
|
|
43722
|
+
files: files.sort(),
|
|
43723
|
+
tensors,
|
|
43724
|
+
source: `${HF}/${modelId}/tree/${revision}`,
|
|
43725
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
43726
|
+
};
|
|
43727
|
+
}
|
|
43728
|
+
|
|
43533
43729
|
// scripts/model-pages.ts
|
|
43534
43730
|
var AGREE_TOLERANCE = 0.02;
|
|
43535
43731
|
var SIZING_CARDS = [
|
|
@@ -43559,7 +43755,8 @@ function sizingFor(model, plan) {
|
|
|
43559
43755
|
const train = (fixed, gb2) => {
|
|
43560
43756
|
if (fixed == null || perSample == null) return { kind: "unknown" };
|
|
43561
43757
|
const b = maxBatchFor({ footprintGb: fixed, activationPerSampleGb: perSample }, gb2);
|
|
43562
|
-
|
|
43758
|
+
const upper = maxBatchFor({ footprintGb: fixed * UPPER_K, activationPerSampleGb: perSample * UPPER_K }, gb2);
|
|
43759
|
+
return b == null ? { kind: "none" } : { kind: "batch", batch: b, upperBatch: upper };
|
|
43563
43760
|
};
|
|
43564
43761
|
const cards = SIZING_CARDS.map(({ label, gb: gb2 }) => {
|
|
43565
43762
|
let full = train(fullFixedGb, gb2);
|
|
@@ -43661,7 +43858,8 @@ function factsFor2(key, snap) {
|
|
|
43661
43858
|
const derivedParams = plan.model.params;
|
|
43662
43859
|
if (!derivedParams) return { skipped: `${key}: the reconstruction carries no parameters` };
|
|
43663
43860
|
const { kind, caveat } = classify(snap.config);
|
|
43664
|
-
const
|
|
43861
|
+
const twice = snap.storedTensors ? storedTwice(snap.config, snap.storedTensors.tensors) : null;
|
|
43862
|
+
const ratio = (derivedParams + (twice?.extra ?? 0)) / snap.publishedParams;
|
|
43665
43863
|
const absDelta = Math.abs(ratio - 1);
|
|
43666
43864
|
const prop = propagateShapes(model);
|
|
43667
43865
|
const structureSeqLen = tokenSeqLenOf(model);
|
|
@@ -43676,6 +43874,7 @@ function factsFor2(key, snap) {
|
|
|
43676
43874
|
snap,
|
|
43677
43875
|
plan,
|
|
43678
43876
|
derivedParams,
|
|
43877
|
+
storedTwice: twice,
|
|
43679
43878
|
ratio,
|
|
43680
43879
|
absDelta,
|
|
43681
43880
|
comparability: kind,
|
|
@@ -43688,6 +43887,169 @@ function factsFor2(key, snap) {
|
|
|
43688
43887
|
sizing: sizingFor(model, plan)
|
|
43689
43888
|
};
|
|
43690
43889
|
}
|
|
43890
|
+
function storedTwiceSentence(t) {
|
|
43891
|
+
const n2 = t.extra >= 1e9 ? `${(t.extra / 1e9).toFixed(2)}B` : `${(t.extra / 1e6).toFixed(1)}M`;
|
|
43892
|
+
return `The checkpoint stores the tied embedding twice: +${n2} (\`${t.head}\` and \`${t.embedding}\`, ${t.shape.join(" x ")}). The model has one such matrix and the count above has it once; the delta compares that count plus the second copy against the file.`;
|
|
43893
|
+
}
|
|
43894
|
+
|
|
43895
|
+
// src/utils/configDataFit.ts
|
|
43896
|
+
var CLASSIFIER_ARCH = /For(Sequence|Token|Image|Audio|MultipleChoice)Classification$/;
|
|
43897
|
+
var posInt2 = (v) => {
|
|
43898
|
+
const n2 = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
43899
|
+
return typeof n2 === "number" && Number.isInteger(n2) && n2 > 0 ? n2 : null;
|
|
43900
|
+
};
|
|
43901
|
+
function inputContractOf(model, config) {
|
|
43902
|
+
const comps = model.components ?? [];
|
|
43903
|
+
const first = (t) => comps.find((c) => c.type === t);
|
|
43904
|
+
const source = {};
|
|
43905
|
+
const emb = first("embedding");
|
|
43906
|
+
const vocabSize = posInt2(emb?.params?.vocabSize);
|
|
43907
|
+
if (vocabSize != null) source.vocabSize = "the token embedding";
|
|
43908
|
+
const patch = first("patchEmbed");
|
|
43909
|
+
const learned = first("learnedPositionalEmbedding");
|
|
43910
|
+
let maxPositions = null;
|
|
43911
|
+
let positionKind = null;
|
|
43912
|
+
if (!patch) {
|
|
43913
|
+
if (learned && posInt2(learned.params?.maxLen) != null) {
|
|
43914
|
+
maxPositions = posInt2(learned.params?.maxLen);
|
|
43915
|
+
positionKind = "learned";
|
|
43916
|
+
source.maxPositions = "the learned position table";
|
|
43917
|
+
} else if (posInt2(emb?.params?.maxSeqLen) != null) {
|
|
43918
|
+
maxPositions = posInt2(emb?.params?.maxSeqLen);
|
|
43919
|
+
positionKind = comps.some((c) => c.type === "rope") ? "rotary" : "declared";
|
|
43920
|
+
source.maxPositions = positionKind === "rotary" ? "max_position_embeddings (rotary)" : "the declared maximum length";
|
|
43921
|
+
}
|
|
43922
|
+
}
|
|
43923
|
+
let imageSize = null;
|
|
43924
|
+
let channels = null;
|
|
43925
|
+
if (patch) {
|
|
43926
|
+
imageSize = posInt2(patch.params?.imgSize);
|
|
43927
|
+
channels = posInt2(patch.params?.inChans);
|
|
43928
|
+
if (imageSize != null) source.imageSize = "the patch embedding";
|
|
43929
|
+
if (channels != null) source.channels = "the patch embedding";
|
|
43930
|
+
} else if (!emb) {
|
|
43931
|
+
const shape = first("input")?.params?.shape ?? null;
|
|
43932
|
+
if (Array.isArray(shape) && shape.length === 3 && shape.every((x) => posInt2(x) != null)) {
|
|
43933
|
+
channels = shape[0];
|
|
43934
|
+
source.channels = "the input shape";
|
|
43935
|
+
}
|
|
43936
|
+
}
|
|
43937
|
+
let numLabels = null;
|
|
43938
|
+
const archs = Array.isArray(config?.architectures) ? config.architectures.map(String) : [];
|
|
43939
|
+
if (archs.some((a) => CLASSIFIER_ARCH.test(a))) {
|
|
43940
|
+
const id2label = config?.id2label;
|
|
43941
|
+
numLabels = id2label && typeof id2label === "object" ? Object.keys(id2label).length || null : posInt2(config?.num_labels);
|
|
43942
|
+
if (numLabels != null) source.numLabels = `the ${archs.find((a) => CLASSIFIER_ARCH.test(a))} head`;
|
|
43943
|
+
} else if (!emb) {
|
|
43944
|
+
const tail = [...comps].reverse().find((c) => !["output", "softmax", "dropout", "flatten", "activation", "relu"].includes(c.type));
|
|
43945
|
+
if (tail?.type === "linear" && posInt2(tail.params?.outFeatures) != null) {
|
|
43946
|
+
numLabels = posInt2(tail.params?.outFeatures);
|
|
43947
|
+
source.numLabels = "the final Linear";
|
|
43948
|
+
}
|
|
43949
|
+
}
|
|
43950
|
+
return { vocabSize, maxPositions, positionKind, numLabels, imageSize, channels, source };
|
|
43951
|
+
}
|
|
43952
|
+
function checkDataFit(contract, data) {
|
|
43953
|
+
const checked = [];
|
|
43954
|
+
const skipped = [];
|
|
43955
|
+
const findings = [];
|
|
43956
|
+
const c = contract;
|
|
43957
|
+
if (data.seqLen != null) {
|
|
43958
|
+
if (c.maxPositions == null) {
|
|
43959
|
+
skipped.push({ check: "sequence length", reason: c.imageSize != null ? "a vision model: its position table is checked against the image size" : "the model declares no maximum length" });
|
|
43960
|
+
} else {
|
|
43961
|
+
checked.push("sequence length");
|
|
43962
|
+
if (data.seqLen > c.maxPositions) {
|
|
43963
|
+
if (c.positionKind === "learned") {
|
|
43964
|
+
findings.push({
|
|
43965
|
+
rule: "seq-over-positions",
|
|
43966
|
+
severity: "block",
|
|
43967
|
+
title: `Sequences of ${data.seqLen} tokens are longer than the ${c.maxPositions}-row position table`,
|
|
43968
|
+
detail: `Position ids past ${c.maxPositions - 1} index outside a learned table, so the first batch that long raises an IndexError (a device-side assert on GPU) after the reservation is taken.`,
|
|
43969
|
+
fix: `Truncate or chunk to ${c.maxPositions} tokens, or pick a model whose context covers ${data.seqLen}.`
|
|
43970
|
+
});
|
|
43971
|
+
} else {
|
|
43972
|
+
findings.push({
|
|
43973
|
+
rule: "seq-over-context",
|
|
43974
|
+
severity: "warn",
|
|
43975
|
+
title: `Sequences of ${data.seqLen} tokens are past the model's ${c.maxPositions}-token context`,
|
|
43976
|
+
detail: c.positionKind === "rotary" ? "Rotary positions are computed, so the job runs; quality past the trained length is not something the config can vouch for, and attention memory grows with the length." : "The config declares this as the longest input; past it, behaviour depends on the implementation.",
|
|
43977
|
+
fix: c.positionKind === "rotary" ? "Set rope_scaling for the longer context, or truncate to the trained length." : `Truncate to ${c.maxPositions} tokens.`
|
|
43978
|
+
});
|
|
43979
|
+
}
|
|
43980
|
+
}
|
|
43981
|
+
}
|
|
43982
|
+
}
|
|
43983
|
+
if (data.tokenizerSize != null) {
|
|
43984
|
+
if (c.vocabSize == null) skipped.push({ check: "tokenizer size", reason: "the model has no token embedding" });
|
|
43985
|
+
else {
|
|
43986
|
+
checked.push("tokenizer size");
|
|
43987
|
+
if (data.tokenizerSize > c.vocabSize) {
|
|
43988
|
+
findings.push({
|
|
43989
|
+
rule: "tokenizer-over-vocab",
|
|
43990
|
+
severity: "block",
|
|
43991
|
+
title: `The tokenizer has ${data.tokenizerSize} ids and the embedding has ${c.vocabSize} rows`,
|
|
43992
|
+
detail: `Any of the ${data.tokenizerSize - c.vocabSize} ids past ${c.vocabSize - 1} indexes outside the embedding. The usual cause is special tokens added to the tokenizer without resizing the model.`,
|
|
43993
|
+
fix: "Call model.resize_token_embeddings(len(tokenizer)) before training, and budget for the new rows."
|
|
43994
|
+
});
|
|
43995
|
+
}
|
|
43996
|
+
}
|
|
43997
|
+
}
|
|
43998
|
+
if (data.numClasses != null) {
|
|
43999
|
+
if (c.numLabels == null) skipped.push({ check: "label count", reason: "no classification head is visible in the config or the graph" });
|
|
44000
|
+
else {
|
|
44001
|
+
checked.push("label count");
|
|
44002
|
+
if (data.numClasses > c.numLabels) {
|
|
44003
|
+
findings.push({
|
|
44004
|
+
rule: "labels-over-head",
|
|
44005
|
+
severity: "block",
|
|
44006
|
+
title: `The data has ${data.numClasses} labels and ${c.source.numLabels ?? "the head"} emits ${c.numLabels}`,
|
|
44007
|
+
detail: `A label id of ${c.numLabels} or more is out of range for the loss, which on GPU is a device-side assert with no line number.`,
|
|
44008
|
+
fix: `Load with num_labels=${data.numClasses} (and ignore_mismatched_sizes=True for a pretrained head).`
|
|
44009
|
+
});
|
|
44010
|
+
} else if (data.numClasses < c.numLabels) {
|
|
44011
|
+
findings.push({
|
|
44012
|
+
rule: "labels-under-head",
|
|
44013
|
+
severity: "warn",
|
|
44014
|
+
title: `The data has ${data.numClasses} labels and the head emits ${c.numLabels}`,
|
|
44015
|
+
detail: "It runs; the extra outputs are never a target, which is usually a head left over from a different task.",
|
|
44016
|
+
fix: `Load with num_labels=${data.numClasses} if the head is meant for this data.`
|
|
44017
|
+
});
|
|
44018
|
+
}
|
|
44019
|
+
}
|
|
44020
|
+
}
|
|
44021
|
+
if (data.imageSize != null) {
|
|
44022
|
+
if (c.imageSize == null) skipped.push({ check: "image size", reason: "the model has no fixed image size (no patch position table)" });
|
|
44023
|
+
else {
|
|
44024
|
+
checked.push("image size");
|
|
44025
|
+
if (data.imageSize !== c.imageSize) {
|
|
44026
|
+
findings.push({
|
|
44027
|
+
rule: "image-size-mismatch",
|
|
44028
|
+
severity: "block",
|
|
44029
|
+
title: `Images are ${data.imageSize}px and the position table was built for ${c.imageSize}px`,
|
|
44030
|
+
detail: "The patch count no longer matches the learned position table, so the forward pass raises on the first batch.",
|
|
44031
|
+
fix: `Resize to ${c.imageSize}px, or pass interpolate_pos_encoding=True where the model supports it.`
|
|
44032
|
+
});
|
|
44033
|
+
}
|
|
44034
|
+
}
|
|
44035
|
+
}
|
|
44036
|
+
if (data.channels != null) {
|
|
44037
|
+
if (c.channels == null) skipped.push({ check: "channels", reason: "the model states no input channels" });
|
|
44038
|
+
else {
|
|
44039
|
+
checked.push("channels");
|
|
44040
|
+
if (data.channels !== c.channels) {
|
|
44041
|
+
findings.push({
|
|
44042
|
+
rule: "channels-mismatch",
|
|
44043
|
+
severity: "block",
|
|
44044
|
+
title: `Images have ${data.channels} channel${data.channels === 1 ? "" : "s"} and the first layer expects ${c.channels}`,
|
|
44045
|
+
detail: "The first convolution or patch projection has a fixed input-channel count; a mismatch raises on the first batch.",
|
|
44046
|
+
fix: data.channels === 1 && c.channels === 3 ? "Repeat the grey channel to three in the transform." : `Convert the images to ${c.channels} channels.`
|
|
44047
|
+
});
|
|
44048
|
+
}
|
|
44049
|
+
}
|
|
44050
|
+
}
|
|
44051
|
+
return { checked, skipped, findings, contract };
|
|
44052
|
+
}
|
|
43691
44053
|
|
|
43692
44054
|
// src/utils/trainCost.ts
|
|
43693
44055
|
init_paramEstimator();
|
|
@@ -43939,7 +44301,7 @@ async function rightsize(csv, opts = {}) {
|
|
|
43939
44301
|
const cache = /* @__PURE__ */ new Map();
|
|
43940
44302
|
const results = [];
|
|
43941
44303
|
for (const j of parsed.jobs) {
|
|
43942
|
-
const empty = { batch: null, hours: null, seqLen: null, loraRank: null, attention: null, peakGb: null, needGb: null, smallestCard: null, rightSized: null, spentUsd: 0, savedUsd: 0, plan: null, params: null };
|
|
44304
|
+
const empty = { batch: null, hours: null, seqLen: null, loraRank: null, attention: null, peakGb: null, needGb: null, smallestCard: null, upperNeedGb: null, fit: null, rightSized: null, spentUsd: 0, savedUsd: 0, plan: null, params: null };
|
|
43943
44305
|
if ("error" in j) {
|
|
43944
44306
|
results.push({ jobId: j.jobId, model: j.model, requested: null, ...empty, verdict: "unknown", reason: j.error });
|
|
43945
44307
|
continue;
|
|
@@ -43967,17 +44329,24 @@ async function rightsize(csv, opts = {}) {
|
|
|
43967
44329
|
continue;
|
|
43968
44330
|
}
|
|
43969
44331
|
const needGb = sized.peakGb * HEADROOM2;
|
|
44332
|
+
const upperNeedGb = upperPeakGb(sized.peakGb) * HEADROOM2;
|
|
43970
44333
|
const c = classify2(needGb, req, cards);
|
|
43971
|
-
const
|
|
44334
|
+
const restsOn = c.verdict === "over" ? c.rightSized : c.verdict === "right-sized" ? req : null;
|
|
44335
|
+
const fit = restsOn == null ? null : upperNeedGb <= restsOn.gb ? "fits" : "borderline";
|
|
44336
|
+
const claim = fit === "borderline" && c.verdict === "over" ? classify2(upperNeedGb, req, cards) : c;
|
|
44337
|
+
const savedUsd2 = claim.verdict === "over" && claim.rightSized ? Math.max(0, (req.hourly - claim.rightSized.hourly) * j.hours) : 0;
|
|
43972
44338
|
const p = sized.plan.cost;
|
|
44339
|
+
const measure = `borderline: the upper bound (${upperNeedGb.toFixed(1)} GB with headroom, ${Math.round(UPPER_COVERAGE * 100)}% of held-out runs) does not fit ${restsOn?.gb ?? 0} GB; measure once`;
|
|
43973
44340
|
results.push({
|
|
43974
44341
|
...base,
|
|
43975
44342
|
seqLen,
|
|
43976
44343
|
verdict: c.verdict,
|
|
43977
|
-
reason: c.verdict === "under" ? `predicted peak ${sized.peakGb.toFixed(1)} GB x ${HEADROOM2} does not fit ${req.gb} GB: the job OOMed or relied on something the graph does not declare` : c.verdict === "right-sized" ? null : `${c.rightSized.gb} GB holds the ${needGb.toFixed(1)} GB the job needed` + (c.rightSized.count < req.count ? `; ${req.count} cards to ${c.rightSized.count}: the same work on fewer cards takes longer, so read the figure as capacity reserved and not needed` : ""),
|
|
44344
|
+
reason: c.verdict === "under" ? `predicted peak ${sized.peakGb.toFixed(1)} GB x ${HEADROOM2} does not fit ${req.gb} GB: the job OOMed or relied on something the graph does not declare` : c.verdict === "right-sized" ? fit === "borderline" ? measure : null : `${c.rightSized.gb} GB holds the ${needGb.toFixed(1)} GB the job needed` + (c.rightSized.count < req.count ? `; ${req.count} cards to ${c.rightSized.count}: the same work on fewer cards takes longer, so read the figure as capacity reserved and not needed` : "") + (fit === "borderline" ? `; ${measure}, so the not-needed figure is claimed only down to ${claim.verdict === "over" && claim.rightSized ? labelOf(claim.rightSized) : "the request itself"}, which holds the upper bound` : ""),
|
|
43978
44345
|
peakGb: sized.peakGb,
|
|
43979
44346
|
needGb,
|
|
43980
44347
|
smallestCard: c.smallestCard,
|
|
44348
|
+
upperNeedGb,
|
|
44349
|
+
fit,
|
|
43981
44350
|
rightSized: c.rightSized ? { ...c.rightSized, label: labelOf(c.rightSized) } : null,
|
|
43982
44351
|
savedUsd: savedUsd2,
|
|
43983
44352
|
plan: { peakGb: p.peakGb, activationGb: p.activationGb, smallestFitGpu: p.smallestFitGpu, maxBatch: p.maxBatch, twoCardSplit: p.twoCardSplit },
|
|
@@ -44001,7 +44370,7 @@ async function rightsize(csv, opts = {}) {
|
|
|
44001
44370
|
prices: { cards, overridden: opts.overridden ?? [], source: PRICE_SOURCE }
|
|
44002
44371
|
};
|
|
44003
44372
|
}
|
|
44004
|
-
var METHOD = `Needed memory is the training peak \`buildPlan\` predicts from the graph: weights, gradients and optimizer state (${OPTIMIZER_K} x 16 bytes per parameter) plus activations (fitted factors on every layer's output, the intermediates inside each layer, recurrent gate states, the largest tensor's backward copy, and stored attention scores), with ${HEADROOM2}x headroom for allocator fragmentation and the CUDA context. It estimates the training peak under AdamW with bf16 autocast; inference jobs and other optimizers will differ. ${calibrationSentence()} (every run: https://www.neurarch.com/memory-calibration.html). The score term counts only the attention score matrices eager attention stores for backward; PyTorch SDPA (the Hugging Face default) and flash attention do not keep them, so the plan, like every other plan, leaves that term out unless the CSV's attention column says eager. Token models are sized at the CSV's seq_len, or ${DEFAULT_SEQ_LEN} tokens (the model's context if shorter) when it is blank. A job with lora_rank is sized with the base frozen in bf16 (2 bytes per parameter) and adapters on every Linear but the head carrying AdamW state. A multi-card job is sized as one model sharded across its cards (FSDP / ZeRO-3 style): the peak at the global batch (per-device batch x cards) against the summed capacity. Plain DDP keeps a full copy on every card, so for a DDP job read the single-card peak at the per-device batch instead. A request is right-sized when it is the smallest capacity that holds the peak or one size up; the right-sized request is the cheapest single card that holds it, and several cards only when no single card does. The not-needed figure assumes the same wall hours: a slower card can lengthen a run, so the figure is reservation capacity paid for and not needed, not a promise about a cheaper run.`;
|
|
44373
|
+
var METHOD = `Needed memory is the training peak \`buildPlan\` predicts from the graph: weights, gradients and optimizer state (${OPTIMIZER_K} x 16 bytes per parameter) plus activations (fitted factors on every layer's output, the intermediates inside each layer, recurrent gate states, the largest tensor's backward copy, and stored attention scores), with ${HEADROOM2}x headroom for allocator fragmentation and the CUDA context. It estimates the training peak under AdamW with bf16 autocast; inference jobs and other optimizers will differ. ${calibrationSentence()} (every run: https://www.neurarch.com/memory-calibration.html). The score term counts only the attention score matrices eager attention stores for backward; PyTorch SDPA (the Hugging Face default) and flash attention do not keep them, so the plan, like every other plan, leaves that term out unless the CSV's attention column says eager. Token models are sized at the CSV's seq_len, or ${DEFAULT_SEQ_LEN} tokens (the model's context if shorter) when it is blank. A job with lora_rank is sized with the base frozen in bf16 (2 bytes per parameter) and adapters on every Linear but the head carrying AdamW state. A multi-card job is sized as one model sharded across its cards (FSDP / ZeRO-3 style): the peak at the global batch (per-device batch x cards) against the summed capacity. Plain DDP keeps a full copy on every card, so for a DDP job read the single-card peak at the per-device batch instead. A request is right-sized when it is the smallest capacity that holds the peak or one size up; the right-sized request is the cheapest single card that holds it, and several cards only when no single card does. The not-needed figure assumes the same wall hours: a slower card can lengthen a run, so the figure is reservation capacity paid for and not needed, not a promise about a cheaper run. Every verdict is also checked against the conservative peak, the estimate x ${UPPER_K} (held out by architecture, ${Math.round(UPPER_COVERAGE * 100)}% of measured runs of 1 GB or more peaked at or under it): a row whose capacity holds the estimate and not the conservative peak is marked borderline ("measure once"), and an over row that is borderline claims as not needed only what the conservative peak would free.`;
|
|
44005
44374
|
var DOES_NOT_KNOW = [
|
|
44006
44375
|
"Data loader memory: pinned host buffers, prefetch queues and worker processes on the GPU host.",
|
|
44007
44376
|
"Custom kernels beyond the attention column: fused optimizers, fused losses and other custom ops can add or remove memory it does not see.",
|
|
@@ -44034,7 +44403,7 @@ function renderMarkdown(r) {
|
|
|
44034
44403
|
j.attention === "eager" ? "eager attention" : ""
|
|
44035
44404
|
].filter(Boolean).join(", ");
|
|
44036
44405
|
const note = [extras, j.reason ?? ""].filter(Boolean).join("; ");
|
|
44037
|
-
L.push(`| ${md(j.jobId)} | ${md(j.model)} | ${j.requested?.label ?? ""} | ${j.batch ?? ""} | ${j.hours ?? ""} | ${gb(j.needGb)} | ${j.smallestCard ?? (j.needGb != null ? "none single" : "")} | ${j.verdict} | ${j.verdict === "over" ? j.rightSized?.label ?? "" : j.verdict === "right-sized" ? "as requested" : ""} | ${usd(j.spentUsd)} | ${j.savedUsd > 0 ? usd(j.savedUsd) : "-"} | ${md(note)} |`);
|
|
44406
|
+
L.push(`| ${md(j.jobId)} | ${md(j.model)} | ${j.requested?.label ?? ""} | ${j.batch ?? ""} | ${j.hours ?? ""} | ${gb(j.needGb)} | ${j.smallestCard ?? (j.needGb != null ? "none single" : "")} | ${j.verdict}${j.fit === "borderline" ? " (borderline)" : ""} | ${j.verdict === "over" ? j.rightSized?.label ?? "" : j.verdict === "right-sized" ? "as requested" : ""} | ${usd(j.spentUsd)} | ${j.savedUsd > 0 ? usd(j.savedUsd) : "-"} | ${md(note)} |`);
|
|
44038
44407
|
}
|
|
44039
44408
|
L.push("", "## Method", "", METHOD, "");
|
|
44040
44409
|
L.push("## Prices", "", `Default: ${r.prices.source}.` + (r.prices.overridden.length ? ` Overridden by --prices: ${r.prices.overridden.join(", ")}.` : " Pass --prices to use your own rates."), "");
|
|
@@ -44107,10 +44476,10 @@ var GIANT_REQUEST_SHARE = 0.2;
|
|
|
44107
44476
|
var HEADROOM3 = 1.15;
|
|
44108
44477
|
var LATE_STEP = 5;
|
|
44109
44478
|
var f2 = (x) => x >= 10 ? x.toFixed(1) : x.toFixed(2);
|
|
44110
|
-
function batchThatFits(facts, cardGb) {
|
|
44479
|
+
function batchThatFits(facts, cardGb, k = 1) {
|
|
44111
44480
|
const { footprintGb, activationPerSampleGb } = facts;
|
|
44112
44481
|
if (footprintGb == null || activationPerSampleGb == null || !(activationPerSampleGb > 0)) return null;
|
|
44113
|
-
const b = Math.floor((cardGb / HEADROOM3 - footprintGb) / activationPerSampleGb);
|
|
44482
|
+
const b = Math.floor((cardGb / HEADROOM3 - footprintGb * k) / (activationPerSampleGb * k));
|
|
44114
44483
|
return b >= 1 ? b : null;
|
|
44115
44484
|
}
|
|
44116
44485
|
function inferenceSizeFixes(facts, cardGb) {
|
|
@@ -44246,6 +44615,33 @@ function diagnoseOom(text, facts = {}) {
|
|
|
44246
44615
|
]
|
|
44247
44616
|
);
|
|
44248
44617
|
}
|
|
44618
|
+
if (!inference && card != null && facts.predictedPeakGb != null && facts.predictedUpperGb != null && facts.predictedUpperGb * HEADROOM3 > card && !(facts.step != null && facts.step > LATE_STEP)) {
|
|
44619
|
+
const k = facts.upperK ?? facts.predictedUpperGb / facts.predictedPeakGb;
|
|
44620
|
+
const cov = facts.upperCoverage != null ? `${Math.round(facts.upperCoverage * 100)}% of held-out measured runs peaked at or under it` : "the conservative estimate";
|
|
44621
|
+
evidence.push(`the conservative peak is ${f2(facts.predictedUpperGb)} GB (${f2(facts.predictedUpperGb * HEADROOM3)} GB with headroom): ${cov}`);
|
|
44622
|
+
const fixes = [];
|
|
44623
|
+
const b = batchThatFits(facts, card, k);
|
|
44624
|
+
if (b != null && facts.batch != null && b < facts.batch) {
|
|
44625
|
+
let micro = b;
|
|
44626
|
+
while (micro > 1 && facts.batch % micro !== 0) micro--;
|
|
44627
|
+
const accum = facts.batch / micro;
|
|
44628
|
+
fixes.push({
|
|
44629
|
+
action: `batch ${micro} with gradient accumulation of ${accum} steps`,
|
|
44630
|
+
because: `${b} is the largest batch whose conservative peak fits ${f2(card)} GB with ${Math.round((HEADROOM3 - 1) * 100)}% headroom; ${micro} x ${accum} keeps the effective batch at ${facts.batch}`,
|
|
44631
|
+
changesTraining: false
|
|
44632
|
+
});
|
|
44633
|
+
}
|
|
44634
|
+
fixes.push({
|
|
44635
|
+
action: "measure one step before the full run: log torch.cuda.max_memory_allocated() after the first optimizer step",
|
|
44636
|
+
because: "at the edge of a card the estimate is a fit check, not a precise number; one measured step settles it",
|
|
44637
|
+
changesTraining: false
|
|
44638
|
+
});
|
|
44639
|
+
return result(
|
|
44640
|
+
"borderline",
|
|
44641
|
+
`The job was borderline on this card: the predicted peak of ${f2(facts.predictedPeakGb)} GB fits ${f2(card)} GB with headroom, but the conservative peak of ${f2(facts.predictedUpperGb)} GB does not. This out-of-memory is inside the estimate's own error, not evidence of memory the model does not declare.`,
|
|
44642
|
+
fixes
|
|
44643
|
+
);
|
|
44644
|
+
}
|
|
44249
44645
|
if (card != null && facts.predictedPeakGb != null && facts.predictedPeakGb <= card * FITS_WITH_ROOM) {
|
|
44250
44646
|
const late = facts.step != null && facts.step > LATE_STEP;
|
|
44251
44647
|
return result(
|
|
@@ -44295,6 +44691,9 @@ function factsFor3(model, batch, seqLen, attention, step) {
|
|
|
44295
44691
|
const act = plan.cost.activationGb;
|
|
44296
44692
|
return {
|
|
44297
44693
|
predictedPeakGb: plan.cost.peakGb,
|
|
44694
|
+
predictedUpperGb: plan.cost.peakGbUpper,
|
|
44695
|
+
upperK: plan.cost.upperBound.multiplier,
|
|
44696
|
+
upperCoverage: plan.cost.upperBound.coverage,
|
|
44298
44697
|
footprintGb: act != null ? plan.cost.peakGb - act : null,
|
|
44299
44698
|
activationPerSampleGb: act != null && batch > 0 ? act / batch : null,
|
|
44300
44699
|
batch,
|
|
@@ -44374,6 +44773,9 @@ function graphOfFacts(facts) {
|
|
|
44374
44773
|
updatedAt: ""
|
|
44375
44774
|
};
|
|
44376
44775
|
}
|
|
44776
|
+
function dataFitHFModel(facts, data) {
|
|
44777
|
+
return checkDataFit(inputContractOf(graphOfFacts(facts), facts.snap.config), data);
|
|
44778
|
+
}
|
|
44377
44779
|
function inferHFModel(facts, opts) {
|
|
44378
44780
|
const model = graphOfFacts(facts);
|
|
44379
44781
|
const prompt = opts.promptTokens ?? withSeqLen(model, null).seqLen ?? DEFAULT_SEQ_LEN;
|
|
@@ -44441,6 +44843,8 @@ async function inspectHFModel(modelId, fetchImpl = fetch) {
|
|
|
44441
44843
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44442
44844
|
config
|
|
44443
44845
|
};
|
|
44846
|
+
const stored = await readStoredHead(modelId, fetchImpl);
|
|
44847
|
+
snap.storedTensors = "error" in stored ? null : { tensors: stored.tensors, source: stored.source, fetchedAt: stored.fetchedAt };
|
|
44444
44848
|
const facts = factsFor2(keyFor(modelId), snap);
|
|
44445
44849
|
if ("skipped" in facts) return { ok: false, hub, reason: facts.skipped };
|
|
44446
44850
|
return { ok: true, hub, facts };
|
|
@@ -44458,6 +44862,7 @@ export {
|
|
|
44458
44862
|
PRICE_SOURCE,
|
|
44459
44863
|
applyPrices,
|
|
44460
44864
|
attentionPhrase,
|
|
44865
|
+
dataFitHFModel,
|
|
44461
44866
|
graphOfFacts,
|
|
44462
44867
|
headline,
|
|
44463
44868
|
inferHFModel,
|
|
@@ -44473,7 +44878,8 @@ export {
|
|
|
44473
44878
|
renderMarkdown,
|
|
44474
44879
|
renderOomText,
|
|
44475
44880
|
rightsize,
|
|
44476
|
-
specFromFlags
|
|
44881
|
+
specFromFlags,
|
|
44882
|
+
storedTwiceSentence
|
|
44477
44883
|
};
|
|
44478
44884
|
/*! Bundled license information:
|
|
44479
44885
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neurarch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Know what a GPU job needs before it takes a reservation: training peak memory, the smallest card that holds it and what a run costs, read from a Hugging Face config or a traced graph. `neurarch rightsize jobs.csv` puts each past job's request next to what it needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"homepage": "https://neurarch.com/memory-calibration.html",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/neurarch-ai/neurarch"
|
|
11
|
+
"url": "git+https://github.com/neurarch-ai/neurarch.git"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"neurarch": "
|
|
14
|
+
"neurarch": "bin/neurarch.mjs"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|