Skip to content

Commit 288de99

Browse files
committed
feat(v4): YAML/JSON run-template for declarative V4 block stacks
Declarative spec for assembling V4 models from named blocks — single source of truth for a run, diff-friendly under code review. Surface: - SUPPORTED_BLOCK_KINDS: gdn, kda, mla, mla_absorb, attention, engram, moe, mlp, lightning_indexer (ROI 7), nsa (ROI 8), csa_hca (ROI 9). Adding a new block kind is one identifier here. - BlockSpec(kind, repeat, params): one entry; validates kind against the registry, repeat≥1, params is a dict. - MTPSpec(depth, hidden_size_override): optional MTP head spec. - RunTemplate(name, hidden_size, blocks, mtp, vocab_size): full run spec; total_blocks() / block_kinds_used() helpers. - load_template / dump_template — file round-trip; dispatch on extension (.yaml/.yml → YAML, .json → JSON). PyYAML is optional; JSON works without it. - loads_template / dumps_template — string round-trip with explicit fmt. Schema versioning: schema_version: 1 written on every dump; readers reject newer-than-self with an actionable error so we can bump the schema later without silent data loss. Tests (21 new): BlockSpec/MTPSpec/RunTemplate validation rules, total_blocks summation, kinds_used set, round-trip via dict / JSON string+file / YAML string+file, unknown-extension rejection, forward-compat schema_version rejection, registry covers all documented V4 ROIs, mtp is optional. v4 suite: 216 passed / 2 skipped.
1 parent 3eacee2 commit 288de99

2 files changed

Lines changed: 409 additions & 0 deletions

File tree

cppmega_v4/run_template.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""YAML/JSON run-template for V4 block stacks.
2+
3+
Declarative spec for assembling a V4 model from named blocks. Two surfaces:
4+
5+
1. RunTemplate — pydantic-style dataclass with strict validation.
6+
2. load_template / dump_template — round-trip via PyYAML or stdlib json.
7+
8+
Schema (one top-level entry per logical block):
9+
10+
name: my_v4_1b
11+
hidden_size: 2048
12+
blocks:
13+
- kind: gdn # GatedDeltaNet ("L" symbol)
14+
repeat: 2
15+
params: { num_heads: 16, head_dim_k: 128, head_dim_v: 128 }
16+
- kind: kda # Kimi Delta Attention ("K" symbol)
17+
repeat: 1
18+
params: { num_heads: 16, num_v_heads: 32, head_dim_k: 128, head_dim_v: 128 }
19+
- kind: mla_absorb # FlashMLA absorbed attention
20+
repeat: 4
21+
params: { num_heads: 16, q_lora_rank: 1024, kv_lora_rank: 256 }
22+
- kind: engram # Engram block (N symbol, doc_id-aware)
23+
repeat: 1
24+
params: { num_branches: 4, branch_dim: 512 }
25+
- kind: moe # aux-loss-free V4 MoE
26+
repeat: 2
27+
params: { num_experts: 64, num_experts_per_tok: 8, intermediate_size: 4096 }
28+
mtp:
29+
depth: 2
30+
hidden_size_override: null # null → use top-level hidden_size
31+
32+
The YAML/JSON file is the single source of truth for a run; programs
33+
should not pass extra block kwargs at runtime. The template format is
34+
deliberately a *flat list of named entries* (no nested groupings) so
35+
diffs against a config under code review stay small.
36+
"""
37+
38+
import json
39+
from dataclasses import asdict, dataclass, field
40+
from pathlib import Path
41+
from typing import Any, ClassVar, Optional
42+
43+
try:
44+
import yaml as _yaml # type: ignore
45+
HAS_YAML = True
46+
except ImportError:
47+
HAS_YAML = False
48+
49+
50+
SUPPORTED_BLOCK_KINDS = frozenset({
51+
"gdn", # GatedDeltaNet (L) — cppmega_v4.nn.linear_attention
52+
"kda", # Kimi Delta Attention (K) — cppmega_v4.nn.kimi_delta_attention
53+
"mla_absorb", # FlashMLA absorbed — cppmega_v4.nn.mla_absorb
54+
"mla", # Standard MLA — vendored mlx_lm reference
55+
"attention", # Standard multi-head attention
56+
"engram", # Engram block (N) — cppmega_v4.nn._external.tilekernels_engram
57+
"moe", # V4 MoE — cppmega_v4.nn.moe_v4
58+
"mlp", # Dense MLP
59+
"lightning_indexer", # V3.2 DSA indexer (ROI 7)
60+
"nsa", # Native Sparse Attention (ROI 8)
61+
"csa_hca", # CSA+HCA hybrid (ROI 9)
62+
})
63+
64+
65+
@dataclass
66+
class BlockSpec:
67+
"""One block entry in a RunTemplate."""
68+
69+
kind: str
70+
repeat: int = 1
71+
params: dict[str, Any] = field(default_factory=dict)
72+
73+
def __post_init__(self) -> None:
74+
if self.kind not in SUPPORTED_BLOCK_KINDS:
75+
raise ValueError(
76+
f"unsupported block kind {self.kind!r}; "
77+
f"must be one of {sorted(SUPPORTED_BLOCK_KINDS)}"
78+
)
79+
if not isinstance(self.repeat, int) or self.repeat < 1:
80+
raise ValueError(f"repeat must be a positive int, got {self.repeat!r}")
81+
if not isinstance(self.params, dict):
82+
raise ValueError(f"params must be a dict, got {type(self.params).__name__}")
83+
84+
85+
@dataclass
86+
class MTPSpec:
87+
"""Optional MTP-head spec attached to the run."""
88+
89+
depth: int = 0
90+
hidden_size_override: Optional[int] = None
91+
92+
def __post_init__(self) -> None:
93+
if not isinstance(self.depth, int) or self.depth < 0:
94+
raise ValueError(f"depth must be a non-negative int, got {self.depth!r}")
95+
if self.hidden_size_override is not None and self.hidden_size_override <= 0:
96+
raise ValueError(
97+
f"hidden_size_override must be positive or None, "
98+
f"got {self.hidden_size_override!r}"
99+
)
100+
101+
102+
@dataclass
103+
class RunTemplate:
104+
"""Full run spec: name + dims + ordered block list + optional MTP."""
105+
106+
name: str
107+
hidden_size: int
108+
blocks: list[BlockSpec]
109+
mtp: Optional[MTPSpec] = None
110+
vocab_size: Optional[int] = None
111+
# Schema version — bump on breaking changes; readers use this to migrate.
112+
schema_version: ClassVar[int] = 1
113+
114+
def __post_init__(self) -> None:
115+
if not isinstance(self.name, str) or not self.name:
116+
raise ValueError("name must be a non-empty string")
117+
if not isinstance(self.hidden_size, int) or self.hidden_size <= 0:
118+
raise ValueError(f"hidden_size must be positive, got {self.hidden_size!r}")
119+
if not self.blocks:
120+
raise ValueError("blocks must be non-empty")
121+
if self.vocab_size is not None and self.vocab_size <= 0:
122+
raise ValueError(
123+
f"vocab_size must be positive or None, got {self.vocab_size!r}"
124+
)
125+
126+
@classmethod
127+
def from_dict(cls, data: dict[str, Any]) -> "RunTemplate":
128+
if not isinstance(data, dict):
129+
raise TypeError(f"expected dict, got {type(data).__name__}")
130+
# Schema version check (forward-compatible: warn but accept older).
131+
ver = data.get("schema_version", 1)
132+
if ver > cls.schema_version:
133+
raise ValueError(
134+
f"template schema_version {ver} is newer than reader "
135+
f"version {cls.schema_version}; upgrade cppmega_v4"
136+
)
137+
blocks = [BlockSpec(**b) for b in data.get("blocks", [])]
138+
mtp = MTPSpec(**data["mtp"]) if data.get("mtp") else None
139+
return cls(
140+
name=data["name"],
141+
hidden_size=data["hidden_size"],
142+
blocks=blocks,
143+
mtp=mtp,
144+
vocab_size=data.get("vocab_size"),
145+
)
146+
147+
def to_dict(self) -> dict[str, Any]:
148+
d = {
149+
"schema_version": self.schema_version,
150+
"name": self.name,
151+
"hidden_size": self.hidden_size,
152+
"blocks": [asdict(b) for b in self.blocks],
153+
}
154+
if self.mtp is not None:
155+
d["mtp"] = asdict(self.mtp)
156+
if self.vocab_size is not None:
157+
d["vocab_size"] = self.vocab_size
158+
return d
159+
160+
def total_blocks(self) -> int:
161+
return sum(b.repeat for b in self.blocks)
162+
163+
def block_kinds_used(self) -> set[str]:
164+
return {b.kind for b in self.blocks}
165+
166+
167+
def load_template(path: str | Path) -> RunTemplate:
168+
"""Load a RunTemplate from YAML or JSON, dispatching on file extension."""
169+
p = Path(path)
170+
text = p.read_text()
171+
return loads_template(text, fmt=_fmt_for_path(p))
172+
173+
174+
def dump_template(template: RunTemplate, path: str | Path) -> None:
175+
"""Write a RunTemplate to YAML or JSON, dispatching on file extension."""
176+
p = Path(path)
177+
p.write_text(dumps_template(template, fmt=_fmt_for_path(p)))
178+
179+
180+
def loads_template(text: str, *, fmt: str = "yaml") -> RunTemplate:
181+
"""Parse a RunTemplate from a string (fmt = 'yaml' or 'json')."""
182+
if fmt == "yaml":
183+
if not HAS_YAML:
184+
raise ImportError("PyYAML not installed; pip install pyyaml or use fmt='json'")
185+
data = _yaml.safe_load(text)
186+
elif fmt == "json":
187+
data = json.loads(text)
188+
else:
189+
raise ValueError(f"fmt must be 'yaml' or 'json', got {fmt!r}")
190+
return RunTemplate.from_dict(data)
191+
192+
193+
def dumps_template(template: RunTemplate, *, fmt: str = "yaml") -> str:
194+
"""Serialize a RunTemplate to a string (fmt = 'yaml' or 'json')."""
195+
data = template.to_dict()
196+
if fmt == "yaml":
197+
if not HAS_YAML:
198+
raise ImportError("PyYAML not installed; pip install pyyaml or use fmt='json'")
199+
return _yaml.safe_dump(data, sort_keys=False, default_flow_style=False)
200+
if fmt == "json":
201+
return json.dumps(data, indent=2, sort_keys=False)
202+
raise ValueError(f"fmt must be 'yaml' or 'json', got {fmt!r}")
203+
204+
205+
def _fmt_for_path(p: Path) -> str:
206+
sfx = p.suffix.lower()
207+
if sfx in (".yaml", ".yml"):
208+
return "yaml"
209+
if sfx == ".json":
210+
return "json"
211+
raise ValueError(f"unsupported template extension {sfx!r}; use .yaml/.yml/.json")
212+
213+
214+
__all__ = [
215+
"BlockSpec",
216+
"MTPSpec",
217+
"RunTemplate",
218+
"SUPPORTED_BLOCK_KINDS",
219+
"dump_template",
220+
"dumps_template",
221+
"load_template",
222+
"loads_template",
223+
]

0 commit comments

Comments
 (0)