Skip to content

Commit ac5d9ac

Browse files
Serve the GLM-5.3 Flash TrellisMX release on b12x's in-tree P8 runtime
Problem brandonmusic/glm-5.3-flash-tr3-fp8 is a TrellisMX (procedural NPK) release of GLM-5.3 Flash, quantized directly from zai-org/GLM-5.3-Flash-BF16. It overlays per-layer, per-rank routed-expert sidecars (layers 3 to 45, one per TP4 rank) on a carrier/ directory that holds every other tensor as stored (BF16 projections and head, FP32 scalars), with a manifest and the encoder transform. The GLM recipe reads only the NVFP4 checkpoint, so Flare could not resolve, plan, verify or prepare this release. Serving it is a maintainer request. Resulting behavior - A glm-5.3-flash-trellismx checkpoint pins the release at 80cbe535, whose carrier chat template is byte-identical to the NVFP4 checkpoint's, so the recipe's insertion patch annotates it and preparation checks both the source and the result digest. Checkpoint gains tensor_parallel (sizes its storage fixes; this release plans and lists TP4 only) and overlay (the manifest schema it is published with). Local release directories resolve to it. cached and post-download verification require the canonical inventory: one sidecar per allocated layer and rank at its declared size, plus the encoder transform. - Detection checks the manifest (schema, checkpoint, allocation, MTP layer, carrier pin), the carrier index and config, geometry and the SwiGLU clamp of 10, the quantization config compared whole, and every sidecar header through a strict safetensors parse against the coupled contract and the transform digest. It reads the manifest once and hashes those bytes. Before loading, all ranks compare manifest and transform digests. - Loading reads each sidecar once, checks its size and SHA-256 against the manifest, and builds the tensors from those same bytes. Carrier tensors load as stored, MTP layer 45 takes the same path, and the draft keeps the target's BF16 head. - b12x's TrellisMX source format prepares each layer's experts; the plan must be SiLU with the clamp before SiLU at 10. On CUDA each layer and rank runs b12x's in-tree P8 runtime, built with b12x's declared GLM-5.3 Flash serving configuration (GLM53_SERVING_OPTIONS), which the recipe consumes rather than restates. Attach requires that the runtime resolved that configuration and uses in_tree_p8_backend(). The launch geometry is not a recipe setting: each capacity's plan races b12x's P8 launch configurations and fixes the winner before capture, and the plan declares the arena every P8 call carves its buffers from, which preparation sizes in the shared scratch arena. No runtime call runs at attach: the runtime declares every program a call of 1 to its capacity in rows reaches, and b12x preparation compiles those programs and installs them in the runtime before it freezes kernel resolution, so no capture compiles or allocates. No environment variable selects P8 behavior, and the mHC prefill route is left to b12x's autotuner. Compile workers receive attached bundles as b12x's inert descriptor, and only the preparing process runs the runtime. - The routed-expert record names the TrellisMX release that supplied its experts, and the recipe's TrellisMX geometry checks read that typed origin directly. - Each rank records the backend identity, b12x's serving configuration, and the attachment identities and resolved settings of its runtimes. Building that record fails if any TrellisMX layer lacks a live runtime. - The checkpoint declares the resident bytes preparation keeps: 10,046,638,288 bytes of expansion and 4,457,303,696 draft bytes. - scripts/qualify_glm53_trellismx.py runs the release's two test modules with the GPU and release cases enabled and fails unless every collected test ran and passed. - No Protocol Buffer schema or Helios code changes, so packages exported before this change load as before. Validation performed (NVIDIA main 6fa01a4, 2026-10-31) - CPU, CUDA_VISIBLE_DEVICES="", with the companion FlashInfer changes (c37d2d9ee and 9be301a77) installed: the full tests/ directory gives 64 failed, 4,326 passed and 2,515 skipped on this branch against 64 failed, 4,180 passed and 2,466 skipped on unmodified 6fa01a4 in the same environment, with identical failing ids. - GPU, one RTX PRO 6000 Blackwell Max-Q: scripts/qualify_glm53_trellismx.py against the release at 80cbe535 collected 192 tests and passed all 192, none skipped, in 26 minutes; its release cases detect, load and verify the published sidecars. - TP4 end to end on four RTX PRO 6000 Blackwell GPUs (two Max-Q, two Workstation Edition), with this change on NVIDIA main a161b17 before it was carried onto 6fa01a4, FlashInfer f5f5341d7 plus the companion changes (TrellisMX c37d2d9ee, mHC precision 9be301a77) and no mHC route pin. Apart from the mHC fix, that run's FlashInfer runtime differed from c37d2d9ee in comments, one validation error message and a trailing-padding clear span this geometry does not have, its FlashInfer tests predate the final tail test, and its Flare tree carried this change before a test-only adjustment. The build also carried changes this commit does not contain: the mixed Max-Q/Workstation tuning agreement (#150), retained warmup mapping reuse with its follow-up fixes (#154), the reasoning close-token output fix, and two local-only changes for this host (an override of torch's compile-package GPU-name comparison in ignition/preparation/aot.py for the two card names, and acceptance of this host's DeepSeek checkpoint's 2-D slab scale planes in ignition/components/weights.py and ignition/preparation/csf_checkpoint.py). - Export from an empty kernel cache passed in 41 minutes, with all 1,287 prepared requests and 122 serving graphs; every rank verified its sources and the package was signed. - Boot healthy in 104 s. Smoke NLL 1.5618 over 12 of 16 scored positions, equal to the previous export of this release. The smoke's greedy chat check spent its 512-token budget inside reasoning and returned no content. - Decode tok/s (llm-inference-bench v0.4.29) at context 0 and 8K: C1 189.2 / 218.4, C2 281.0 / 283.5, C4 377.2 / 383.7, C8 471.9 / 462.8, C16 594.5 / 590.3, C32 751.9 / 697.8, no errors; speculative acceptance over the bench was 0.68 (112,117 of 163,792 drafts, from the server's counters). Sieve coding peak median 229.7 tok/s (5 of 5). - Reliability at the model card's sampling: 12 bursts of 32 or 28 requests that restore one published prompt and 13 prompts of 99,208 tokens sent one at a time, 373 requests. 351 correct, 8 with an adjacent record number, 4 with a wrong record, and 10 the fixture grades corrupt (6 that spent the 1,024-token budget in reasoning, 3 that ended with the answer inside reasoning and no content, and 1 whose content repeats the right answer twice); 11 of the 13 long prompts were correct. None of the 373 replies carried a think tag in its content. Three bursts of 32 distinct prompts: 92 of 96 correct. There were no request or device errors. These figures measure the whole local system, not this change alone. Material limitations - Preparing and serving the release requires FlashInfer with the TrellisMX source format and the in-tree P8 runtime; the figures above also used the mHC TF32 projection precision fix, which keeps the mixing coefficients at FP32 accuracy on b12x's TF32 route. pyproject.toml and uv.lock keep upstream's LIL FlashInfer pin to main (f5f5341d7), this change adds no pin, and the release cannot be prepared until the TrellisMX change is on main. - GLM-5.3 Flash can generate its close token after reasoning has ended; without the separate change "Drop reasoning close delimiter tokens from visible content", that token appears in message.content. - Validated on SM120 (RTX PRO 6000 Blackwell) only; SM103, SM121 and Windows were not run. - The manifest lists no carrier digests, so carrier files get header checks only, as every other checkpoint does. Design-Signed-Off-By: Claude Opus 5.5 (Claude Code); design-review=1c01 Signed-off-by: Brandon M. Music <266531604+brandonmmusic-max@users.noreply.github.com>
1 parent 6fa01a4 commit ac5d9ac

21 files changed

Lines changed: 3267 additions & 30 deletions

‎docs/model-preparation.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,25 @@ captured capacities from 64 through 32,768 patches. Each feature slot holds an
456456
advance. `--image-concurrency` defaults to the smaller of four and text
457457
concurrency. `--language-model-only` omits image weights and graphs.
458458

459+
The `glm-5.3-flash-trellismx` alias selects `brandonmusic/glm-5.3-flash-tr3-fp8`,
460+
a TrellisMX release quantized from `zai-org/GLM-5.3-Flash-BF16`. It overlays
461+
per-layer, per-rank routed-expert sidecars on a `carrier/` directory that holds
462+
every other tensor as stored, BF16 projections and head included. The sidecars
463+
partition the experts for TP4, so only four GPUs can load it.
464+
Preparation reads each sidecar once, checks its size and SHA-256 against the
465+
release manifest, and builds its tensors from those bytes. Its routed experts
466+
run on b12x's in-tree P8 runtime, which preparation builds with b12x's declared
467+
GLM-5.3 Flash serving configuration (the runtime reads no environment variables)
468+
and attaches under b12x's in-tree backend declaration. The launch geometry is a
469+
prepared choice: each capacity's plan races the launch configurations b12x
470+
compiles and fixes the winner before capture, and the plan declares the arena
471+
every P8 call carves its buffers out of. Qualify a release with every GPU and
472+
release test run, none skipped:
473+
474+
```sh
475+
CUDA_VISIBLE_DEVICES=<assigned GPU> .venv/bin/python scripts/qualify_glm53_trellismx.py --release <release root>
476+
```
477+
459478
## DeepSeek V4.1 Flash
460479

461480
The `deepseek-v4.1-flash` alias selects `deepseek-ai/DeepSeek-V4.1-Flash-1017`

‎ignition/cli/help.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def recipe_help(model=None):
1717
lines = ["Available model recipes:",
1818
f" {'MODEL':<26}{'GPUs':<14}Drafts (default/max)"]
1919
for recipe, checkpoint in catalog:
20-
gpus = ",".join(map(str, recipe.tensor_parallel))
20+
gpus = ",".join(map(str, recipe.checkpoint_tensor_parallel(checkpoint)))
2121
drafts = ("none" if recipe.speculation is None else
2222
f"{recipe.speculation.draft_tokens}/{recipe.max_draft_tokens}")
2323
lines.append(f" {checkpoint.alias:<26}{gpus:<14}{drafts}")
@@ -29,7 +29,7 @@ def recipe_help(model=None):
2929

3030
recipe, checkpoint = selected
3131
lines = [f"Model recipe: {checkpoint.alias}", f" Checkpoint: {checkpoint.repository}",
32-
f" --gpus: select {', '.join(map(str, recipe.tensor_parallel))} GPUs",
32+
f" --gpus: select {', '.join(map(str, recipe.checkpoint_tensor_parallel(checkpoint)))} GPUs",
3333
f" --max-concurrency: 1..{recipe.max_concurrency}; recipe default {recipe.concurrency}",
3434
f" --prefill-chunk-size: 64..{recipe.context}, multiple of 64; default {recipe.prefill_chunk}"]
3535
if recipe.max_images:

‎ignition/cli/serve.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ def _publisher(configuration, args, model=None, hub_cache=None):
103103
return signer
104104
from ignition.preparation.model_card import model_license
105105
from ignition.preparation.checkpoint import inspect_indexed_checkpoint
106+
from ignition.preparation.trellismx_release import trellismx_assets_root
106107
try:
107-
metadata = inspect_indexed_checkpoint(model.snapshot(hub_cache)).metadata_path
108+
# A TrellisMX release overlays a carrier: its index and model card live under carrier/.
109+
metadata = inspect_indexed_checkpoint(trellismx_assets_root(model.snapshot(hub_cache))).metadata_path
108110
license = model_license(metadata,
109111
None if model.directory is not None else model.checkpoint.repository,
110112
model.checkpoint.revision)

‎ignition/planning.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ def plan(recipe: Recipe, checkpoint: Checkpoint, gpus: tuple[Gpu, ...], *, selec
521521
if chunk % 64 or not 64 <= chunk <= recipe.context:
522522
raise UsageError(f"--prefill-chunk-size for {checkpoint.alias} must be a multiple of 64"
523523
f" from 64 through {recipe.context:,}")
524-
world_sizes = recipe.tensor_parallel
524+
# A checkpoint whose storage fixes its tensor-parallel sizes plans only those.
525+
world_sizes = recipe.checkpoint_tensor_parallel(checkpoint)
525526
if speculation is not None and speculation.vocabulary_parallel_sampling:
526527
if recipe.name != "deepseek41":
527528
raise UsageError("--vocabulary-parallel-sampling requires DeepSeek V4.1")
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 Flare community contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Locate a checkpoint's assets when a TrellisMX release overlays a carrier.
5+
6+
A TrellisMX release is a routed-expert overlay: its root holds the manifest, the
7+
per-layer sidecars and the design record, while the safetensors index, tokenizer,
8+
chat template and generation config stay in ``carrier/`` with every tensor the
9+
sidecars do not replace. Shared code that resolves or inspects a checkpoint asks
10+
this module where those files are, without importing a recipe.
11+
12+
Only the manifest is read, and no payload. This module does not validate a release;
13+
the recipe that serves one owns every gate. A root without this manifest is treated
14+
exactly as before so no other checkpoint's behaviour moves, and a manifest that
15+
cannot be read raises instead of letting its root pass for a plain checkpoint.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import json
21+
from pathlib import Path
22+
23+
MANIFEST_NAME = "trellismx-manifest.json"
24+
MANIFEST_SCHEMA = "trellismx.hf-overlay-release.v1"
25+
CARRIER_DIRECTORY = "carrier"
26+
SIDECAR_DIRECTORY = "sidecars"
27+
# The encoder transform the sidecars pin, which preparation reads beside them.
28+
TRANSFORM_PATH = "design/transform.json"
29+
30+
31+
def sidecar_path(layer: int, rank: int, world_size: int) -> str:
32+
"""The release-relative path of one layer and tensor-parallel rank's sidecar."""
33+
return f"{SIDECAR_DIRECTORY}/p8-layer-{layer:03d}-tp{world_size}-rank-{rank}.safetensors"
34+
35+
36+
def _manifest(root: Path) -> dict | None:
37+
"""``root``'s overlay manifest when it declares this schema, else None.
38+
39+
Raises ``ValueError`` when the manifest exists but is not a readable JSON object.
40+
"""
41+
manifest_path = root / MANIFEST_NAME
42+
if not manifest_path.is_file():
43+
return None
44+
try:
45+
manifest = json.loads(manifest_path.read_text())
46+
except (OSError, ValueError) as error:
47+
raise ValueError(f"{manifest_path} is unreadable: {error}") from error
48+
if not isinstance(manifest, dict):
49+
raise ValueError(f"{manifest_path} is not a JSON object")
50+
return manifest if manifest.get("schema") == MANIFEST_SCHEMA else None
51+
52+
53+
def _carrier_index(root: Path) -> Path:
54+
return root / CARRIER_DIRECTORY / "model.safetensors.index.json"
55+
56+
57+
def trellismx_assets_root(root: str | Path) -> Path:
58+
"""The directory holding ``root``'s index and frontend assets.
59+
60+
The carrier directory when ``root`` declares this overlay schema and carries a
61+
carrier index, and ``root`` itself otherwise.
62+
"""
63+
root = Path(root).expanduser()
64+
return root / CARRIER_DIRECTORY if _manifest(root) is not None and _carrier_index(root).is_file() else root
65+
66+
67+
def overlay_schema(root: str | Path) -> str | None:
68+
"""The overlay manifest schema ``root`` is packaged with, or None for any other checkpoint."""
69+
return MANIFEST_SCHEMA if _manifest(Path(root).expanduser()) is not None else None
70+
71+
72+
def _allocation(manifest: dict) -> dict[int, int]:
73+
"""The manifest's allocation: each layer index and the tensor-parallel size it is sliced for."""
74+
allocation = manifest.get("allocation")
75+
if not isinstance(allocation, dict) or not allocation:
76+
raise ValueError("TrellisMX manifest allocation must be a nonempty object of layer indices")
77+
layers = {}
78+
for key, world_size in allocation.items():
79+
try:
80+
layer = int(key)
81+
except ValueError:
82+
layer = None
83+
if layer is None or key != str(layer) or layer < 0 or type(world_size) is not int or world_size <= 0:
84+
raise ValueError(f"TrellisMX manifest allocation entry {key!r}: {world_size!r} is invalid")
85+
layers[layer] = world_size
86+
return layers
87+
88+
89+
def _manifest_files(manifest: dict) -> list[tuple[str, int]]:
90+
"""Every file the manifest lists, which must be exactly the release's canonical inventory.
91+
92+
That inventory is one rank-named sidecar for every layer and rank the allocation
93+
declares, so a list that omits, repeats, renames or adds an entry is rejected
94+
instead of making an incomplete download look complete.
95+
"""
96+
files = manifest.get("files")
97+
if not isinstance(files, list) or not files:
98+
raise ValueError("TrellisMX manifest lists no files")
99+
entries = []
100+
for entry in files:
101+
path = entry.get("path") if isinstance(entry, dict) else None
102+
size = entry.get("bytes") if isinstance(entry, dict) else None
103+
if not isinstance(path, str) or not path or Path(path).is_absolute() or ".." in Path(path).parts \
104+
or type(size) is not int or size <= 0:
105+
raise ValueError(f"TrellisMX manifest file entry {entry!r} is invalid")
106+
entries.append((path, size))
107+
inventory = {sidecar_path(layer, rank, world_size)
108+
for layer, world_size in _allocation(manifest).items() for rank in range(world_size)}
109+
listed = [path for path, _ in entries]
110+
if len(listed) != len(set(listed)) or set(listed) != inventory:
111+
missing, unexpected = sorted(inventory - set(listed)), sorted(set(listed) - inventory)
112+
repeated = sorted({path for path in listed if listed.count(path) > 1})
113+
raise ValueError("TrellisMX manifest files must be one sidecar per allocated layer and rank:"
114+
f" missing {missing}, unexpected {unexpected}, repeated {repeated}")
115+
return entries
116+
117+
118+
def trellismx_files_present(root: str | Path) -> bool:
119+
"""Whether every file ``root``'s release needs beside its carrier is present at its declared size.
120+
121+
Those files are the canonical inventory (one sidecar per allocated layer and rank)
122+
and the encoder transform. True for a root that is not a release, which needs
123+
nothing more; a manifest whose file list is not that inventory raises
124+
``ValueError``. Only sizes are compared; preparation verifies each sidecar's
125+
digest when it loads it.
126+
"""
127+
root = Path(root).expanduser()
128+
manifest = _manifest(root)
129+
if manifest is None:
130+
return True
131+
return _carrier_index(root).is_file() and (root / TRANSFORM_PATH).is_file() and all(
132+
(root / path).is_file() and (root / path).stat().st_size == size for path, size in _manifest_files(manifest))
133+
134+
135+
def trellismx_stored_bytes(root: str | Path) -> int | None:
136+
"""Stored tensor bytes of ``root``'s release: the carrier's payload plus every manifest file.
137+
138+
None for a root that is not a release.
139+
"""
140+
root = Path(root).expanduser()
141+
manifest = _manifest(root)
142+
if manifest is None:
143+
return None
144+
try:
145+
index = json.loads(_carrier_index(root).read_text())
146+
except OSError as error:
147+
raise ValueError(f"{_carrier_index(root)} is unreadable: {error}") from error
148+
carrier = index.get("metadata", {}).get("total_size") if isinstance(index, dict) else None
149+
if type(carrier) is not int or carrier <= 0:
150+
raise ValueError(f"{root / CARRIER_DIRECTORY} has an invalid safetensors total_size: {carrier!r}")
151+
return carrier + sum(size for _, size in _manifest_files(manifest))
152+
153+
154+
__all__ = [
155+
"CARRIER_DIRECTORY", "MANIFEST_NAME", "MANIFEST_SCHEMA", "SIDECAR_DIRECTORY", "TRANSFORM_PATH", "overlay_schema",
156+
"sidecar_path", "trellismx_assets_root", "trellismx_files_present", "trellismx_stored_bytes",
157+
]

‎ignition/recipes/__init__.py‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ def _csf_stored_bytes(container, snapshot):
7272
def _snapshot_cached(snapshot: Path) -> bool:
7373
"""Report whether the index and every tensor file it names are present."""
7474
try:
75+
from ignition.preparation.trellismx_release import trellismx_assets_root, trellismx_files_present
76+
77+
# A TrellisMX release overlays a carrier: its index and tokenizer live under carrier/, and
78+
# it is complete only with every sidecar its manifest names.
79+
if not trellismx_files_present(snapshot):
80+
return False
81+
snapshot = trellismx_assets_root(snapshot)
7582
if _csf_container(snapshot) is not None:
7683
from ignition.preparation.checkpoint import inspect_indexed_checkpoint
7784

@@ -116,6 +123,18 @@ def snapshot(self, hub_cache: Path) -> Path:
116123
def verify(self, hub_cache: Path) -> None:
117124
"""Check index metadata; recipes validate tensor storage and geometry."""
118125
snapshot = self.snapshot(hub_cache)
126+
from ignition.preparation.trellismx_release import (overlay_schema, trellismx_files_present,
127+
trellismx_stored_bytes)
128+
129+
try:
130+
if overlay_schema(snapshot) is not None:
131+
# An overlay release keeps its index under carrier/ and needs every file its manifest names.
132+
trellismx_stored_bytes(snapshot)
133+
if not trellismx_files_present(snapshot):
134+
raise UsageError(f"{snapshot} lacks files its TrellisMX release manifest names")
135+
return
136+
except ValueError as error:
137+
raise UsageError(f"{snapshot} has an unreadable TrellisMX release: {error}") from error
119138
try:
120139
container = _csf_container(snapshot)
121140
if container is not None:
@@ -148,16 +167,23 @@ def _unsupported(reason: str) -> UsageError:
148167
return UsageError(f"{reason}; supported models: {', '.join(aliases())}")
149168

150169

151-
def _supporting(config_path: Path, source: str) -> tuple[Recipe, Checkpoint]:
170+
def _supporting(config_path: Path, source: str, overlay: str | None = None) -> tuple[Recipe, Checkpoint]:
171+
"""The first declared checkpoint of the named architecture packaged like ``source``.
172+
173+
A directory carrying an overlay manifest matches only checkpoints declaring that overlay,
174+
and any other directory only checkpoints declaring none.
175+
"""
152176
try:
153177
architectures = json.loads(config_path.read_text())["architectures"]
154178
except (OSError, ValueError, KeyError, TypeError) as error:
155179
raise _unsupported(f"{source} has no readable config.json architecture") from error
156180
for recipe in recipes():
157181
for checkpoint in recipe.checkpoints:
158-
if isinstance(architectures, list) and checkpoint.architecture in architectures:
182+
if (isinstance(architectures, list) and checkpoint.architecture in architectures
183+
and checkpoint.overlay == (overlay or "")):
159184
return recipe, checkpoint
160-
raise _unsupported(f"no recipe supports architecture {architectures!r} in {source}")
185+
packaging = f" packaged as {overlay}" if overlay else ""
186+
raise _unsupported(f"no recipe supports architecture {architectures!r}{packaging} in {source}")
161187

162188

163189
def _stored_bytes(index_path: Path, source: str) -> int:
@@ -215,12 +241,22 @@ def resolve(model: str, hub_cache: Path | None = None) -> ResolvedModel:
215241
# A directory that a recipe declares keeps its declared placement.
216242
if Path(checkpoint.repository).is_absolute() and Path(checkpoint.repository).resolve() == directory:
217243
return ResolvedModel(recipe, checkpoint, directory)
218-
recipe, qualified = _supporting(metadata / "config.json", str(directory))
244+
from ignition.preparation.trellismx_release import overlay_schema, trellismx_stored_bytes
245+
246+
# An overlay release is placed like the checkpoint declared with its packaging, and stores
247+
# its carrier's payload plus every file its manifest names.
248+
try:
249+
overlay = overlay_schema(directory)
250+
overlay_stored = None if overlay is None else trellismx_stored_bytes(directory)
251+
except ValueError as error:
252+
raise UsageError(f"{directory} has an unreadable release manifest: {error}") from error
253+
recipe, qualified = _supporting(metadata / "config.json", str(directory), overlay)
219254
# The directory's path identifies its measurements on this host.
220255
revision = hashlib.sha256(str(directory).encode()).hexdigest()[:40]
221256
index = directory / "model.safetensors.index.json"
222257
single = directory / "model.safetensors"
223258
stored = (_csf_stored_bytes(container, directory) if container is not None else
259+
overlay_stored if overlay_stored is not None else
224260
_stored_bytes(index, str(directory)) if index.exists() else
225261
single.stat().st_size if single.is_file() else qualified.stored_bytes)
226262
return ResolvedModel(recipe, _replacement(qualified, str(directory), revision, stored), directory)

‎ignition/recipes/declaration.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ class Checkpoint:
9292
# contract without the value, and dashboards then show no weight bandwidth.
9393
decode_weight_bytes: int = 0
9494
plain_ranks: Mapping[int, "RankCosts"] | None = None
95+
# The tensor-parallel sizes that can load this checkpoint when its storage fixes them, such
96+
# as weights published as per-rank shards; empty means every size its recipe declares.
97+
tensor_parallel: tuple[int, ...] = ()
98+
# The schema of the overlay manifest this checkpoint is published with, when its storage
99+
# overlays a carrier checkpoint. A local directory resolves to a checkpoint declaring the
100+
# overlay its manifest names, and a directory without one to a checkpoint declaring none.
101+
overlay: str = ""
95102

96103

97104
@dataclass(frozen=True)
@@ -256,11 +263,20 @@ def __post_init__(self):
256263
if any(costs is not None and set(costs) != set(self.ranks)
257264
for costs in (checkpoint.ranks, checkpoint.plain_ranks)):
258265
raise ValueError(f"{checkpoint.alias} declares costs for other tensor-parallel sizes than its recipe")
266+
sizes = checkpoint.tensor_parallel
267+
if (not isinstance(sizes, tuple) or any(type(size) is not int for size in sizes)
268+
or list(sizes) != sorted(set(sizes)) or not set(sizes) <= set(self.ranks)):
269+
raise ValueError(f"{checkpoint.alias} must declare its tensor-parallel sizes as an increasing tuple"
270+
" of sizes its recipe declares")
259271

260272
@property
261273
def tensor_parallel(self) -> tuple[int, ...]:
262274
return tuple(sorted(self.ranks))
263275

276+
def checkpoint_tensor_parallel(self, checkpoint: Checkpoint) -> tuple[int, ...]:
277+
"""The tensor-parallel sizes that can load `checkpoint`."""
278+
return checkpoint.tensor_parallel or self.tensor_parallel
279+
264280
def costs(self, world_size: int, speculation: Speculation | None, checkpoint: Checkpoint | None = None) -> RankCosts:
265281
"""Return one rank's costs under the given speculation settings, for `checkpoint` when it declares its own."""
266282
ranks = self.ranks if checkpoint is None or checkpoint.ranks is None else checkpoint.ranks

0 commit comments

Comments
 (0)