neurarch 0.1.0 → 0.1.1

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 CHANGED
@@ -188,7 +188,20 @@ gpt2-pretrain,openai-community/gpt2,4xA100-80,16,24,1024,
188
188
  `--prices prices.json` overrides the default list prices per GPU-hour
189
189
  (`{"H100": 2.49, "A100-80": 1.79}`); cards it does not name keep theirs, and the
190
190
  report says which were overridden. `--json` returns every row with the plan's
191
- own `peakGb`, `smallestFitGpu`, `maxBatch` and `twoCardSplit`.
191
+ own `peakGb`, `smallestFitGpu`, `maxBatch` and `twoCardSplit`, plus `upperNeedGb` and
192
+ `fit`: a row whose capacity holds the estimate but not the conservative peak (the
193
+ estimate x 1.43, at or above the measured peak on 95% of held-out runs) is
194
+ `borderline`, says "measure once", and claims as not needed only what the
195
+ conservative peak would free.
196
+
197
+ **Three answers per card.** The training block prints each card green (the
198
+ conservative peak with 1.15x headroom fits), yellow `borderline` (the estimate
199
+ fits, the conservative peak does not: measure one step first) or red (the estimate
200
+ alone is past the card), then the upper bound, the largest batch it fits, and how
201
+ many copies of it fit each card and the smallest MIG slice on A100 / H100 (memory
202
+ only: compute and bandwidth are not considered). `--json` carries the same as
203
+ `cost.peakGbUpper`, `cost.upperBound`, `cost.gpuVerdict`, `cost.maxBatchSafe` and
204
+ `cost.shareable`; `cost.gpuFits` is unchanged.
192
205
 
193
206
  **The memory model.** The peak is `buildPlan`'s training peak under AdamW with
194
207
  bf16 autocast: weights, gradients and optimizer state plus activations (every
package/bin/neurarch.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * Zero dependencies; the engine is bundled. Set HF_TOKEN for gated repos.
21
21
  */
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';
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, storedTwiceSentence } from '../engine.bundle.mjs';
23
23
  /** The channel tag on every link this program prints. `docs/DISTRIBUTION.md`
24
24
  * has asked for one since the first release: without it a visit from a tool
25
25
  * and a visit from a search result are the same row, and "which channel brings
@@ -341,6 +341,7 @@ if (flags.has('--json')) {
341
341
  compare: `https://neurarch.com/compare?a=${encodeURIComponent(modelId)}&b=&utm_source=${SOURCE}`,
342
342
  publishedParams: result.hub.publishedParams,
343
343
  derivedParams: result.facts.derivedParams,
344
+ storedTwice: result.facts.storedTwice,
344
345
  ratio: result.facts.ratio,
345
346
  absDelta: result.facts.absDelta,
346
347
  tolerance: AGREE_TOLERANCE,
@@ -404,6 +405,12 @@ if (sizingOnly) {
404
405
  // The two numbers, first, because nothing below matters if they disagree.
405
406
  process.stdout.write(` ${dim('published (safetensors.total)')} ${bold(humanParams(hub.publishedParams))} ${dim(hub.publishedParams.toLocaleString())}\n`);
406
407
  process.stdout.write(` ${dim('derived (config.json alone) ')} ${bold(humanParams(facts.derivedParams))} ${dim(facts.derivedParams.toLocaleString())}\n`);
408
+ // A tied matrix the file writes twice: read from the safetensors header,
409
+ // and the delta below already counts it. Same sentence the /m/ page prints.
410
+ if (facts.storedTwice) {
411
+ process.stdout.write(` ${dim('stored twice in the file ')} ${bold(`+${humanParams(facts.storedTwice.extra)}`)} ${dim(facts.storedTwice.extra.toLocaleString())}\n`);
412
+ process.stdout.write(` ${yellow('!')} ${storedTwiceSentence(facts.storedTwice)}\n`);
413
+ }
407
414
 
408
415
  const deltaLine = `${sign}${pct.toFixed(2)}%`;
409
416
  if (facts.comparability !== 'comparable') {
@@ -503,14 +510,37 @@ if (trainingSizing) {
503
510
  } else {
504
511
  process.stdout.write(`\n ${bold('training memory')} ${dim('(no price without --data)')}\n`);
505
512
  }
506
- const fits = Object.entries(cost.gpuFits)
507
- .map(([card, ok]) => `${ok === null ? dim(card) : ok ? green(card) : red(card)}`)
513
+ // Three answers per card: green fits on the conservative peak, yellow
514
+ // borderline (the estimate fits, the conservative peak does not: measure
515
+ // once), red does not fit. An older engine without `gpuVerdict` falls back
516
+ // to the point-estimate boolean.
517
+ const fits = Object.entries(cost.gpuVerdict ?? cost.gpuFits)
518
+ .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
519
  .join(' ');
509
520
  const batchShown = /at batch (\d+)/.exec(cost.assumptions)?.[1] ?? '32';
510
521
  // The sequence length is printed because it moves the peak more than the
511
522
  // batch does: a config's context window is not a training length.
512
523
  const tokens = cost.seqLen != null ? `, ${cost.seqLen} tokens` : '';
513
524
  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`);
525
+ if (cost.peakGbUpper > 0) {
526
+ const u = cost.upperBound;
527
+ 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`);
528
+ if (Object.values(cost.gpuVerdict ?? {}).includes('borderline')) {
529
+ 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`);
530
+ }
531
+ const mbs = cost.maxBatchSafe;
532
+ if (mbs && (mbs.T4 != null || mbs.A100_40 != null || mbs.H100_80 != null)) {
533
+ const v = (x) => (x == null ? 'n/a' : x.toLocaleString('en-US'));
534
+ 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`);
535
+ }
536
+ }
537
+ if (cost.shareable) {
538
+ const sh = cost.shareable;
539
+ const pick = sh.cards.filter((c) => ['T4 (16GB)', 'A100 (40GB)', 'H100 (80GB)'].includes(c.gpu)).map((c) => `${c.copies} on ${gpuSlug(c.gpu)}`).join(', ');
540
+ 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(', ');
541
+ const anyCopy = sh.cards.some((c) => c.copies > 0);
542
+ 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`);
543
+ }
514
544
  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
545
  if (cost.seqLen != null && seqLen == null) {
516
546
  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 authHeaders = hfAuthHeaders();
32740
+ const authHeaders2 = hfAuthHeaders();
32741
32741
  const configResponse = await fetch(
32742
32742
  `https://huggingface.co/${modelId}/resolve/main/config.json`,
32743
- { headers: authHeaders }
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, authHeaders);
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: authHeaders });
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
- if (smallest) parts.push(`fits ${smallest} at batch ${batchOf(plan)}`);
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
- return b == null ? { kind: "none" } : { kind: "batch", batch: b };
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 ratio = derivedParams / snap.publishedParams;
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,10 @@ 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
+ }
43691
43894
 
43692
43895
  // src/utils/trainCost.ts
43693
43896
  init_paramEstimator();
@@ -43939,7 +44142,7 @@ async function rightsize(csv, opts = {}) {
43939
44142
  const cache = /* @__PURE__ */ new Map();
43940
44143
  const results = [];
43941
44144
  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 };
44145
+ 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
44146
  if ("error" in j) {
43944
44147
  results.push({ jobId: j.jobId, model: j.model, requested: null, ...empty, verdict: "unknown", reason: j.error });
43945
44148
  continue;
@@ -43967,17 +44170,24 @@ async function rightsize(csv, opts = {}) {
43967
44170
  continue;
43968
44171
  }
43969
44172
  const needGb = sized.peakGb * HEADROOM2;
44173
+ const upperNeedGb = upperPeakGb(sized.peakGb) * HEADROOM2;
43970
44174
  const c = classify2(needGb, req, cards);
43971
- const savedUsd2 = c.verdict === "over" && c.rightSized ? Math.max(0, (req.hourly - c.rightSized.hourly) * j.hours) : 0;
44175
+ const restsOn = c.verdict === "over" ? c.rightSized : c.verdict === "right-sized" ? req : null;
44176
+ const fit = restsOn == null ? null : upperNeedGb <= restsOn.gb ? "fits" : "borderline";
44177
+ const claim = fit === "borderline" && c.verdict === "over" ? classify2(upperNeedGb, req, cards) : c;
44178
+ const savedUsd2 = claim.verdict === "over" && claim.rightSized ? Math.max(0, (req.hourly - claim.rightSized.hourly) * j.hours) : 0;
43972
44179
  const p = sized.plan.cost;
44180
+ 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
44181
  results.push({
43974
44182
  ...base,
43975
44183
  seqLen,
43976
44184
  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` : ""),
44185
+ 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
44186
  peakGb: sized.peakGb,
43979
44187
  needGb,
43980
44188
  smallestCard: c.smallestCard,
44189
+ upperNeedGb,
44190
+ fit,
43981
44191
  rightSized: c.rightSized ? { ...c.rightSized, label: labelOf(c.rightSized) } : null,
43982
44192
  savedUsd: savedUsd2,
43983
44193
  plan: { peakGb: p.peakGb, activationGb: p.activationGb, smallestFitGpu: p.smallestFitGpu, maxBatch: p.maxBatch, twoCardSplit: p.twoCardSplit },
@@ -44001,7 +44211,7 @@ async function rightsize(csv, opts = {}) {
44001
44211
  prices: { cards, overridden: opts.overridden ?? [], source: PRICE_SOURCE }
44002
44212
  };
44003
44213
  }
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.`;
44214
+ 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
44215
  var DOES_NOT_KNOW = [
44006
44216
  "Data loader memory: pinned host buffers, prefetch queues and worker processes on the GPU host.",
44007
44217
  "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 +44244,7 @@ function renderMarkdown(r) {
44034
44244
  j.attention === "eager" ? "eager attention" : ""
44035
44245
  ].filter(Boolean).join(", ");
44036
44246
  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)} |`);
44247
+ 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
44248
  }
44039
44249
  L.push("", "## Method", "", METHOD, "");
44040
44250
  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 +44317,10 @@ var GIANT_REQUEST_SHARE = 0.2;
44107
44317
  var HEADROOM3 = 1.15;
44108
44318
  var LATE_STEP = 5;
44109
44319
  var f2 = (x) => x >= 10 ? x.toFixed(1) : x.toFixed(2);
44110
- function batchThatFits(facts, cardGb) {
44320
+ function batchThatFits(facts, cardGb, k = 1) {
44111
44321
  const { footprintGb, activationPerSampleGb } = facts;
44112
44322
  if (footprintGb == null || activationPerSampleGb == null || !(activationPerSampleGb > 0)) return null;
44113
- const b = Math.floor((cardGb / HEADROOM3 - footprintGb) / activationPerSampleGb);
44323
+ const b = Math.floor((cardGb / HEADROOM3 - footprintGb * k) / (activationPerSampleGb * k));
44114
44324
  return b >= 1 ? b : null;
44115
44325
  }
44116
44326
  function inferenceSizeFixes(facts, cardGb) {
@@ -44246,6 +44456,33 @@ function diagnoseOom(text, facts = {}) {
44246
44456
  ]
44247
44457
  );
44248
44458
  }
44459
+ if (!inference && card != null && facts.predictedPeakGb != null && facts.predictedUpperGb != null && facts.predictedUpperGb * HEADROOM3 > card && !(facts.step != null && facts.step > LATE_STEP)) {
44460
+ const k = facts.upperK ?? facts.predictedUpperGb / facts.predictedPeakGb;
44461
+ const cov = facts.upperCoverage != null ? `${Math.round(facts.upperCoverage * 100)}% of held-out measured runs peaked at or under it` : "the conservative estimate";
44462
+ evidence.push(`the conservative peak is ${f2(facts.predictedUpperGb)} GB (${f2(facts.predictedUpperGb * HEADROOM3)} GB with headroom): ${cov}`);
44463
+ const fixes = [];
44464
+ const b = batchThatFits(facts, card, k);
44465
+ if (b != null && facts.batch != null && b < facts.batch) {
44466
+ let micro = b;
44467
+ while (micro > 1 && facts.batch % micro !== 0) micro--;
44468
+ const accum = facts.batch / micro;
44469
+ fixes.push({
44470
+ action: `batch ${micro} with gradient accumulation of ${accum} steps`,
44471
+ 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}`,
44472
+ changesTraining: false
44473
+ });
44474
+ }
44475
+ fixes.push({
44476
+ action: "measure one step before the full run: log torch.cuda.max_memory_allocated() after the first optimizer step",
44477
+ because: "at the edge of a card the estimate is a fit check, not a precise number; one measured step settles it",
44478
+ changesTraining: false
44479
+ });
44480
+ return result(
44481
+ "borderline",
44482
+ `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.`,
44483
+ fixes
44484
+ );
44485
+ }
44249
44486
  if (card != null && facts.predictedPeakGb != null && facts.predictedPeakGb <= card * FITS_WITH_ROOM) {
44250
44487
  const late = facts.step != null && facts.step > LATE_STEP;
44251
44488
  return result(
@@ -44295,6 +44532,9 @@ function factsFor3(model, batch, seqLen, attention, step) {
44295
44532
  const act = plan.cost.activationGb;
44296
44533
  return {
44297
44534
  predictedPeakGb: plan.cost.peakGb,
44535
+ predictedUpperGb: plan.cost.peakGbUpper,
44536
+ upperK: plan.cost.upperBound.multiplier,
44537
+ upperCoverage: plan.cost.upperBound.coverage,
44298
44538
  footprintGb: act != null ? plan.cost.peakGb - act : null,
44299
44539
  activationPerSampleGb: act != null && batch > 0 ? act / batch : null,
44300
44540
  batch,
@@ -44441,6 +44681,8 @@ async function inspectHFModel(modelId, fetchImpl = fetch) {
44441
44681
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
44442
44682
  config
44443
44683
  };
44684
+ const stored = await readStoredHead(modelId, fetchImpl);
44685
+ snap.storedTensors = "error" in stored ? null : { tensors: stored.tensors, source: stored.source, fetchedAt: stored.fetchedAt };
44444
44686
  const facts = factsFor2(keyFor(modelId), snap);
44445
44687
  if ("skipped" in facts) return { ok: false, hub, reason: facts.skipped };
44446
44688
  return { ok: true, hub, facts };
@@ -44473,7 +44715,8 @@ export {
44473
44715
  renderMarkdown,
44474
44716
  renderOomText,
44475
44717
  rightsize,
44476
- specFromFlags
44718
+ specFromFlags,
44719
+ storedTwiceSentence
44477
44720
  };
44478
44721
  /*! Bundled license information:
44479
44722
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neurarch",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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": "./bin/neurarch.mjs"
14
+ "neurarch": "bin/neurarch.mjs"
15
15
  },
16
16
  "files": [
17
17
  "bin",