Skip to content

Commit d35634e

Browse files
committed
track1(batch4): thread micro-batch axis end-to-end through path_c gridded step
Thread batch=N (bs1 default, bs4 selectable) through the path_c shape-config -> delegation prim -> physical ABI bank sizing -> probe, keeping bs1 byte-identical. - PathCModelShapeEnv: add batch:int=1 field + __post_init__ RAISE on batch<1 (no silent clamp); _path_c_model_shape_env_from_config reads config.micro_batch_size (RAISE if present-but-non-positive); replace() preserves batch. - ModelFactoryProfile: add micro_batch_size:int=1 (+_require_positive); hybrid_config() carries it onto the frozen HybridTinyConfig via object.__setattr__ as the single source of truth, reads it back + RAISES if it failed to stick (no bsN->bs1 revert). - delegation prim call site: pass batch=int(resolved_shape_env.batch) (kills the silent batch=1 default trap) + add a RULE #1 batch-consistency guard that RAISEs with buffer-name + expected-vs-got batch if a compiled grid kernel's leading dim disagrees with the region-declared batch. - _mamba3_chunked_fwd_region_buffer_shape: batch=int(shape_env.batch) (resizes ALL chunked region/handoff banks); serial-emit feasibility threads shape_env.batch. - _buffer_shape: apply batch multiplier EXPLICITLY, per-buffer, ONLY to per-token activation/state/checkpoint banks (hidden, mamba/m2rnn state+checkpoint+conv_state, target_ids/mask, q_fp8/kv_fp8/scales/indices/lse, *_hidden/_delta/_out); weights stay 1x (verified: act/state scale 4x, weight banks unchanged). - path_c_relax_step_banks: add real_bank_numels_for_batch() + assert_batch_scaling() bs4 self-check (param/paramg banks must stay 1x; act/actg/state must grow -> RAISE). - probes: add --bs4 (requires --prod; RAISE otherwise) flipping the prod cfg batch 1->4; fwd probe adds an F0/F2 grid-total == 4x bs1 sanity assert. Builders/grids already consume batch; no kernel edit. RULE #1: every batch path RAISES on mismatch; no bs4->bs1 fallback anywhere. Local check: ast.parse + py_compile (py3.13) PASS on all 6; isolated batch-logic + per-buffer scaling discipline validated (gb10 compile/run is the Profile agent).
1 parent 2b884ce commit d35634e

6 files changed

Lines changed: 309 additions & 25 deletions

File tree

cppmega_mlx/recipes/model_factory.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ class ModelFactoryProfile:
132132
num_attention_heads: int
133133
head_dim: int
134134
vocab_size: int
135+
# Outer micro-batch axis for the path_c gridded step. Default 1 keeps every
136+
# existing profile/consumer at the historical bs1 behavior byte-for-byte; set
137+
# it to 4 (via ``local_gb10_quarter_profile(micro_batch_size=4)`` or a
138+
# ``replace``) to select the bs4 gridded path. It is threaded onto the
139+
# HybridTinyConfig the profile yields so the path_c shape-env reads it as the
140+
# single source of truth. RULE #1: a non-positive value RAISES in __post_init__.
141+
micro_batch_size: int = 1
135142
max_seq_length: int = LOCAL_GB10_QUARTER_MAX_SEQ_LENGTH
136143
dsa_a_layer_ranks: tuple[int, ...] = LOCAL_GB10_QUARTER_DSA_A_LAYER_RANKS
137144
dsa_indexer_n_heads: int | None = 32
@@ -161,6 +168,7 @@ def __post_init__(self) -> None:
161168
_require_positive("num_attention_heads", self.num_attention_heads)
162169
_require_positive("head_dim", self.head_dim)
163170
_require_positive("vocab_size", self.vocab_size)
171+
_require_positive("micro_batch_size", self.micro_batch_size)
164172
_require_positive("max_seq_length", self.max_seq_length)
165173
_require_positive("moe_num_experts", self.moe_num_experts)
166174
_require_positive("moe_top_k", self.moe_top_k)
@@ -217,9 +225,43 @@ def nam56r_config(self) -> Nam56RModelConfig:
217225
)
218226

219227
def hybrid_config(self, **overrides) -> HybridTinyConfig:
220-
"""Return a HybridTinyConfig using existing NAM56R-to-MLX mapping."""
221-
222-
return build_hybrid_tiny_config_from_nam56r(self.nam56r_config(), **overrides)
228+
"""Return a HybridTinyConfig using existing NAM56R-to-MLX mapping.
229+
230+
The profile's ``micro_batch_size`` rides onto the returned config as the
231+
single source of truth for the path_c gridded step's outer batch axis
232+
(``_path_c_model_shape_env_from_config`` reads ``config.micro_batch_size``).
233+
``HybridTinyConfig`` is a frozen dataclass with no such field, so the value
234+
is carried directly on the instance via ``object.__setattr__`` (the
235+
documented "carry it on the config/region directly" path). RULE #1: an
236+
explicit ``micro_batch_size`` override that conflicts with the profile's is
237+
rejected, and the carried value is read back and asserted — a value that
238+
failed to stick RAISES rather than silently reverting to bs1.
239+
"""
240+
241+
requested_batch = int(self.micro_batch_size)
242+
if "micro_batch_size" in overrides:
243+
override_batch = int(overrides.pop("micro_batch_size"))
244+
if override_batch < 1:
245+
raise ValueError(
246+
"hybrid_config(micro_batch_size=...) must be a positive int "
247+
f"(got {override_batch!r}); RULE #1 forbids a silent clamp to 1"
248+
)
249+
requested_batch = override_batch
250+
config = build_hybrid_tiny_config_from_nam56r(
251+
self.nam56r_config(), **overrides
252+
)
253+
# Carry the micro-batch axis onto the frozen config instance (no field in
254+
# HybridTinyConfig) so the path_c shape-env reads ONE source of truth.
255+
object.__setattr__(config, "micro_batch_size", requested_batch)
256+
carried = int(getattr(config, "micro_batch_size", 0))
257+
if carried != requested_batch:
258+
raise RuntimeError(
259+
"model_factory.hybrid_config: failed to carry micro_batch_size "
260+
f"onto the HybridTinyConfig (requested {requested_batch}, read back "
261+
f"{carried!r}); RULE #1: refusing a silent bs{requested_batch}->bs1 "
262+
"revert"
263+
)
264+
return config
223265

224266
def tiny_smoke_config(self, **overrides) -> HybridTinyConfig:
225267
"""Return a small T=512-capable config preserving this route profile."""

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,19 @@ class PathCModelShapeEnv:
433433
m2rnn_conv_kernel: int
434434
model_value_dtype: str = "float32"
435435
vocab_size: int = 0
436+
# Outer micro-batch axis threaded into every gridded F0/F1/F2/B0/B1/B2 builder
437+
# and into the per-token activation/state bank shapes. Default 1 keeps the
438+
# historical bs1 behavior byte-identical for every existing caller; a >1 value
439+
# selects the bs4 (or any bsN) gridded path. RULE #1: an invalid batch RAISES
440+
# in __post_init__ (no silent clamp to 1).
441+
batch: int = 1
442+
443+
def __post_init__(self) -> None:
444+
if int(self.batch) < 1:
445+
raise ValueError(
446+
"PathCModelShapeEnv.batch must be a positive micro-batch size "
447+
f"(got {self.batch!r}); RULE #1 forbids a silent clamp to 1"
448+
)
436449

437450
@property
438451
def mamba_inner_dim(self) -> int:
@@ -1438,9 +1451,26 @@ def _path_c_model_shape_env_from_config(config: Any | None) -> PathCModelShapeEn
14381451
hidden_size = int(getattr(config, "hidden_size"))
14391452
except AttributeError:
14401453
return None
1454+
# Micro-batch axis (bs1 default for every config that does not carry it; bs4
1455+
# and friends ride on ``micro_batch_size``). RULE #1: a config that DOES carry
1456+
# a ``micro_batch_size`` must yield a usable positive int — if it is present
1457+
# but non-coercible/non-positive we RAISE rather than silently defaulting to 1
1458+
# (a silent bs4->bs1 revert is exactly the forbidden fallback).
1459+
raw_batch = getattr(config, "micro_batch_size", None)
1460+
if raw_batch is None:
1461+
batch = 1
1462+
else:
1463+
batch = int(raw_batch)
1464+
if batch < 1:
1465+
raise ValueError(
1466+
"Path C model shape-env: config.micro_batch_size must be a "
1467+
f"positive int (got {raw_batch!r}); RULE #1 forbids a silent "
1468+
"clamp/revert to bs1"
1469+
)
14411470
return PathCModelShapeEnv(
14421471
sequence_length=sequence_length,
14431472
hidden_size=hidden_size,
1473+
batch=batch,
14441474
attention_num_q_heads=int(attention_config.num_q_heads),
14451475
attention_num_kv_heads=int(attention_config.kv_heads),
14461476
attention_head_dim=int(attention_config.q_head_dim),

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,6 +2377,13 @@ def build_path_c_descriptor_prim_func(
23772377
op_name=nodes[0].op_name,
23782378
production_source=descriptors[0].production_source,
23792379
shape_env=resolved_shape_env,
2380+
# Thread the micro-batch axis EXPLICITLY: the delegation prim's
2381+
# ``batch: int = 1`` default is the canonical silent-fallback trap —
2382+
# if we passed no batch= the F0..B2 grid kernels would compile bs1
2383+
# even when the surrounding step is bs4 (degraded/wrong, not a crash).
2384+
# Sourcing it from ``resolved_shape_env.batch`` keeps banks and kernels
2385+
# on the SAME single source of truth (RULE #1).
2386+
batch=int(resolved_shape_env.batch),
23802387
)
23812388
delegated_prim._cppmega_path_c_schedule_generator = (
23822389
PATH_C_DESCRIPTOR_SCHEDULE_GENERATOR
@@ -2430,6 +2437,49 @@ def build_path_c_descriptor_prim_func(
24302437
for param in tuple(getattr(delegated_prim, "params", ()))
24312438
if not (hasattr(param, "is_scalar") and bool(param.is_scalar()))
24322439
)
2440+
# RULE #1 BATCH-CONSISTENCY GUARD (no silent bs4->bs1): the region surface
2441+
# sized its handoff/region banks for ``resolved_shape_env.batch`` (via
2442+
# ``_mamba3_chunked_fwd_region_buffer_shape``); the compiled grid kernel was
2443+
# compiled for the SAME batch (passed above). If a half-threaded path let
2444+
# them disagree, a bs4 kernel writing into a bs1-sized bank would be a
2445+
# silent out-of-bounds / truncation. Cross-check the compiled kernel's
2446+
# device-buffer param leading dim against the region-declared batch for
2447+
# every buffer whose region shape carries an explicit leading ``batch``
2448+
# axis (the per-token/per-state chunked banks; the bare ``A``/``D`` vectors
2449+
# and any scalar slots carry no batch axis and are skipped). RAISE with
2450+
# WHERE (buffer name) + WHAT (expected batch vs prim leading dim).
2451+
_expected_batch = int(resolved_shape_env.batch)
2452+
if len(delegated_buffer_order) == len(delegated_buffer_params):
2453+
for _bufname, _param in zip(
2454+
delegated_buffer_order, delegated_buffer_params
2455+
):
2456+
_region_shape = _mamba3_chunked_fwd_region_buffer_shape(
2457+
_bufname, resolved_shape_env
2458+
)
2459+
if _region_shape is None or len(_region_shape) == 0:
2460+
continue
2461+
# The chunked region buffers that carry an outer batch axis declare
2462+
# it as dim 0 == batch (x/B/C/dt/h0/cb/.../final_state/d*). Vectors
2463+
# like A=(nheads,)/D=(nheads,) have a leading dim != batch by design
2464+
# — skip those (their dim 0 is a head/feature count, not batch).
2465+
if int(_region_shape[0]) != _expected_batch:
2466+
continue
2467+
_prim_shape = tuple(
2468+
int(dim) for dim in tuple(getattr(_param, "shape", ()))
2469+
)
2470+
if len(_prim_shape) == 0:
2471+
continue
2472+
if int(_prim_shape[0]) != _expected_batch:
2473+
raise ValueError(
2474+
"mamba3 chunked-scan delegation batch mismatch for buffer "
2475+
f"{_bufname!r}: region surface sized leading dim "
2476+
f"{_expected_batch} (shape_env.batch) but the compiled grid "
2477+
f"kernel param leading dim is {int(_prim_shape[0])} "
2478+
f"(prim shape {_prim_shape}). A half-threaded bs"
2479+
f"{_expected_batch} path would write OOB into a bs"
2480+
f"{int(_prim_shape[0])}-sized bank (RULE #1: no silent "
2481+
"bs4->bs1 fallback)."
2482+
)
24332483
# Attach the named-buffer ABI only when the region surface's ordered
24342484
# buffers RECONCILE 1:1 with the kernel's device-buffer param slots. The
24352485
# FULL model surface declares exactly the kernel's inputs+outputs (verified
@@ -7499,7 +7549,9 @@ def _append_row_phased_mamba3_body(
74997549
_chunked_limit,
75007550
) = _mamba3_chunked_forward_scan_feasibility(
75017551
sequence_length=int(shape_env.sequence_length),
7502-
batch=1,
7552+
# Thread the micro-batch axis for consistency (flag-OFF serial-emit
7553+
# classification only; does not change grid feasibility True/False).
7554+
batch=int(shape_env.batch),
75037555
heads=heads,
75047556
head_dim=head_dim,
75057557
state_dim=state_dim,
@@ -14691,7 +14743,11 @@ def _mamba3_chunked_fwd_region_buffer_shape(
1469114743
break
1469214744
if suffix is None:
1469314745
return None
14694-
batch = 1
14746+
# Outer micro-batch axis: every chunked region/handoff bank below carries
14747+
# ``batch`` as leading dim, so this single source resizes ALL of them to bs4
14748+
# (or any bsN). RULE #1: sourced from the SAME shape_env the delegation prim
14749+
# compiles against, so banks and kernels cannot disagree.
14750+
batch = int(shape_env.batch)
1469514751
seqlen = int(shape_env.sequence_length)
1469614752
nheads = int(shape_env.mamba_num_heads)
1469714753
headdim = int(shape_env.mamba_head_dim)
@@ -14755,6 +14811,15 @@ def _buffer_shape(
1475514811
q_dim = q_heads * head_dim
1475614812
kv_dim = kv_heads * head_dim
1475714813
topk = shape_env.attention_sparse_topk
14814+
# Outer micro-batch axis. RULE #1: applied EXPLICITLY, per-buffer, ONLY to the
14815+
# per-token activation/state/checkpoint banks below — NEVER to weight banks
14816+
# (mamba3_in_proj_weight, *_norm_weight, lm_head_weight, dt_bias, D,
14817+
# conv_weight, ...), which are batch-INVARIANT. A blanket multiply would 4x the
14818+
# parameter banks (wrong) and break the param-1x liveness model + the bs1
14819+
# self-check. The chunked region buffers (x/B/C/cb/prev_states/...) already
14820+
# carried batch via _mamba3_chunked_fwd_region_buffer_shape above and never
14821+
# reach here.
14822+
batch = int(shape_env.batch)
1475814823
if name in {
1475914824
"hidden",
1476014825
"mamba3_delta",
@@ -14765,28 +14830,36 @@ def _buffer_shape(
1476514830
"hidden_after_m2rnn",
1476614831
"attention_out",
1476714832
}:
14768-
return (seq * hidden,)
14833+
return (batch * seq * hidden,)
1476914834
if _is_mamba3_state_like_buffer(buffer_name, name):
1477014835
return (
14771-
shape_env.mamba_num_heads
14836+
batch
14837+
* shape_env.mamba_num_heads
1477214838
* shape_env.mamba_head_dim
1477314839
* shape_env.mamba_state_dim,
1477414840
)
1477514841
if name == "mamba3_angle_state":
1477614842
return (
14777-
max(1, shape_env.mamba_num_heads * shape_env.mamba_num_rope_angles),
14843+
max(
14844+
1,
14845+
batch * shape_env.mamba_num_heads * shape_env.mamba_num_rope_angles,
14846+
),
1477814847
)
1477914848
if name == "mamba3_angle_grad_state":
1478014849
return (
14781-
max(1, shape_env.mamba_num_heads * shape_env.mamba_num_rope_angles),
14850+
max(
14851+
1,
14852+
batch * shape_env.mamba_num_heads * shape_env.mamba_num_rope_angles,
14853+
),
1478214854
)
1478314855
if name == "mamba3_angle_checkpoint":
1478414856
checkpoint_interval = MAMBA3_BWD_REPLAY_CHECKPOINT_INTERVAL
1478514857
checkpoint_count = (
1478614858
shape_env.sequence_length + checkpoint_interval - 1
1478714859
) // checkpoint_interval
1478814860
return (
14789-
(checkpoint_count + 1)
14861+
batch
14862+
* (checkpoint_count + 1)
1479014863
* max(1, shape_env.mamba_num_heads * shape_env.mamba_num_rope_angles),
1479114864
)
1479214865
if name == "mamba3_h_checkpoint":
@@ -14795,7 +14868,8 @@ def _buffer_shape(
1479514868
shape_env.sequence_length + checkpoint_interval - 1
1479614869
) // checkpoint_interval
1479714870
return (
14798-
(checkpoint_count + 1)
14871+
batch
14872+
* (checkpoint_count + 1)
1479914873
* shape_env.mamba_num_heads
1480014874
* shape_env.mamba_head_dim
1480114875
* shape_env.mamba_state_dim,
@@ -14804,7 +14878,9 @@ def _buffer_shape(
1480414878
return (
1480514879
max(
1480614880
1,
14807-
(shape_env.mamba_conv_kernel - 1) * shape_env.mamba_conv_channels,
14881+
batch
14882+
* (shape_env.mamba_conv_kernel - 1)
14883+
* shape_env.mamba_conv_channels,
1480814884
),
1480914885
)
1481014886
if name == "mamba3_in_proj_weight":
@@ -14841,7 +14917,7 @@ def _buffer_shape(
1484114917
# the eager ``layers.{first_in_region}.norm.weight`` parameter.
1484214918
return (hidden,)
1484314919
if name in {"target_ids", "target_mask"}:
14844-
return (seq,)
14920+
return (batch * seq,)
1484514921
if name == "lm_head_weight":
1484614922
vocab = max(1, int(getattr(shape_env, "vocab_size", 0) or 0))
1484714923
return (vocab * hidden,)
@@ -14865,7 +14941,8 @@ def _buffer_shape(
1486514941
return (hidden * shape_env.m2rnn_num_heads * shape_env.m2rnn_v_head_dim,)
1486614942
if name in {"m2rnn_h0", "m2rnn_h_state"}:
1486714943
return (
14868-
shape_env.m2rnn_num_heads
14944+
batch
14945+
* shape_env.m2rnn_num_heads
1486914946
* shape_env.m2rnn_k_head_dim
1487014947
* shape_env.m2rnn_v_head_dim,
1487114948
)
@@ -14875,13 +14952,16 @@ def _buffer_shape(
1487514952
shape_env.sequence_length + checkpoint_interval - 1
1487614953
) // checkpoint_interval
1487714954
return (
14878-
(checkpoint_count + 1)
14955+
batch
14956+
* (checkpoint_count + 1)
1487914957
* shape_env.m2rnn_num_heads
1488014958
* shape_env.m2rnn_k_head_dim
1488114959
* shape_env.m2rnn_v_head_dim,
1488214960
)
1488314961
if name == "m2rnn_conv_state":
14884-
return ((shape_env.m2rnn_conv_kernel - 1) * shape_env.m2rnn_conv_dim,)
14962+
return (
14963+
batch * (shape_env.m2rnn_conv_kernel - 1) * shape_env.m2rnn_conv_dim,
14964+
)
1488514965
if name == "attention_q_proj_weight":
1488614966
return (q_dim * hidden,)
1488714967
if name == "attention_q_proj_bias":
@@ -14897,17 +14977,17 @@ def _buffer_shape(
1489714977
if name == "attention_out_proj_bias":
1489814978
return (hidden,)
1489914979
if name == "q_fp8":
14900-
return (seq * q_heads * head_dim,)
14980+
return (batch * seq * q_heads * head_dim,)
1490114981
if name == "q_scale":
14902-
return (seq * q_heads,)
14982+
return (batch * seq * q_heads,)
1490314983
if name == "kv_fp8":
14904-
return (seq * kv_heads * head_dim,)
14984+
return (batch * seq * kv_heads * head_dim,)
1490514985
if name == "kv_scale":
14906-
return (seq * kv_heads,)
14986+
return (batch * seq * kv_heads,)
1490714987
if name == "indices":
14908-
return (seq * kv_heads * topk,)
14988+
return (batch * seq * kv_heads * topk,)
1490914989
if name == "lse":
14910-
return (seq * q_heads,)
14990+
return (batch * seq * q_heads,)
1491114991
if name == "sparse_mla_sm_scale":
1491214992
return (1,)
1491314993
if name == "sparse_mla_sinks":
@@ -14920,7 +15000,7 @@ def _buffer_shape(
1492015000
or name.endswith("_delta")
1492115001
or name.endswith("_out")
1492215002
):
14923-
return (seq * hidden,)
15003+
return (batch * seq * hidden,)
1492415004
return (buffer_extent,)
1492515005

1492615006

0 commit comments

Comments
 (0)