Skip to content

Commit 5975307

Browse files
committed
feat: introduce PathCGradientBufferCapture and extend training step with zero-copy gradient probing for Path C fusion support.
1 parent 6459d95 commit 5975307

25 files changed

Lines changed: 4103 additions & 690 deletions

cppmega_mlx/models/hybrid_lm.py

Lines changed: 272 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@
88
from __future__ import annotations
99

1010
from dataclasses import asdict, dataclass
11-
from typing import TYPE_CHECKING, Literal, TypedDict, cast
11+
from typing import (
12+
TYPE_CHECKING,
13+
Any,
14+
Callable,
15+
Literal,
16+
Mapping,
17+
Sequence,
18+
TypedDict,
19+
cast,
20+
)
1221

1322
import mlx.core as mx
1423
import mlx.nn as nn
24+
from mlx.utils import tree_flatten
1525

1626
from cppmega_mlx.data.packing import mlx_document_boundary_mask
1727
from cppmega_mlx.inference.engine import ContiguousKVCache, kv_cache_position
@@ -29,6 +39,7 @@
2939
from cppmega_mlx.nn.ngram_hash import NgramHashEmbedding
3040
from cppmega_mlx.nn.structure_embedding import CppMegaStructureEmbedding
3141
from cppmega_mlx.recipes.pattern import ExpandedNamPattern, NamLayer, expand_nam_pattern
42+
from cppmega_mlx.runtime.path_c_physical_abi import PathCLogicalBufferOwner
3243
from cppmega_mlx.runtime.kernel_policy import KernelPath, selected_path
3344
from cppmega_mlx.training.mtp import MinimalMTPHead, MTPLossConfig
3445

@@ -44,6 +55,7 @@
4455
| EngramBranch
4556
| ConceptBlock
4657
)
58+
PathCActivationProbe = Callable[[Mapping[str, Any]], None]
4759

4860
_ROUTE_SYMBOL_BACKENDS: dict[str, HybridBackend] = {
4961
"A": "attention",
@@ -68,6 +80,41 @@ class StructureEmbeddingConfigKwargs(TypedDict):
6880
bottleneck_dim: int
6981

7082

83+
class PathCActivationBufferCapture:
84+
"""Opt-in Path C activation owner that stores references, not copies."""
85+
86+
def __init__(
87+
self,
88+
aliases: Mapping[str, str | Sequence[str]] | None = None,
89+
*,
90+
owner_name: str = "HybridTinyLM.path_c_activation_capture",
91+
) -> None:
92+
self.owner_name = owner_name
93+
self.aliases = {
94+
str(source): (str(target),)
95+
if isinstance(target, str)
96+
else tuple(str(item) for item in target)
97+
for source, target in (aliases or {}).items()
98+
}
99+
self.buffers: dict[str, mx.array] = {}
100+
self.events: list[Mapping[str, Any]] = []
101+
102+
def __call__(self, event: Mapping[str, Any]) -> None:
103+
tensor = event.get("tensor")
104+
if not isinstance(tensor, mx.array):
105+
return
106+
logical_names = tuple(str(name) for name in event.get("logical_names", ()))
107+
for name in logical_names:
108+
self.buffers[name] = tensor
109+
for alias in self.aliases.get(name, ()):
110+
self.buffers[alias] = tensor
111+
self.events.append(event)
112+
113+
def clear(self) -> None:
114+
self.buffers.clear()
115+
self.events.clear()
116+
117+
71118
@dataclass(frozen=True)
72119
class HybridTinyConfig:
73120
"""Tiny local hybrid-LM config.
@@ -408,6 +455,8 @@ class HybridTinyBlock(nn.Module):
408455
def __init__(self, layer: NamLayer, config: HybridTinyConfig):
409456
super().__init__()
410457
self.layer = layer
458+
self.path_c_layer_index = layer.number - 1
459+
self.path_c_brick_name = f"layer_{self.path_c_layer_index}_{layer.symbol.lower()}"
411460
self.norm = nn.RMSNorm(config.hidden_size)
412461
mhc_config = config.mhc_config()
413462
self.mhc: ManifoldBranchMixer | None = (
@@ -442,6 +491,40 @@ def __init__(self, layer: NamLayer, config: HybridTinyConfig):
442491
else: # pragma: no cover - expand_nam_pattern rejects this first.
443492
raise ValueError(f"unsupported hybrid layer symbol {layer.symbol!r}")
444493

494+
def _path_c_activation_probe(self) -> PathCActivationProbe | None:
495+
probe = getattr(self, "_path_c_activation_probe_callback", None)
496+
return probe if callable(probe) else None
497+
498+
def _path_c_activation_logical_names(self, name: str) -> tuple[str, ...]:
499+
brick_name = str(getattr(self, "path_c_profile_brick_name", self.path_c_brick_name))
500+
if name == "hidden":
501+
return (f"{brick_name}_hidden",)
502+
if name == "normed":
503+
return (f"{brick_name}_residual_norm_hidden",)
504+
if name == "delta":
505+
return (f"{brick_name}_delta",)
506+
if name == "hidden_after":
507+
return (f"{brick_name}_hidden_after",)
508+
return (f"{brick_name}_{name}",)
509+
510+
def _emit_path_c_activation(self, name: str, tensor: mx.array) -> None:
511+
probe = self._path_c_activation_probe()
512+
if probe is None:
513+
return
514+
probe(
515+
{
516+
"name": name,
517+
"logical_names": self._path_c_activation_logical_names(name),
518+
"tensor": tensor,
519+
"layer_number": self.layer.number,
520+
"layer_index": self.path_c_layer_index,
521+
"route_symbol": self.layer.symbol,
522+
"backend": self.backend,
523+
"brick_name": self.path_c_brick_name,
524+
"profile_brick_name": getattr(self, "path_c_profile_brick_name", None),
525+
}
526+
)
527+
445528
@property
446529
def attention_block(self) -> CausalSelfAttention | None:
447530
return self.block if self.backend == "attention" else None # type: ignore[return-value]
@@ -477,14 +560,17 @@ def __call__(
477560
) -> mx.array:
478561
self.validate_backend()
479562
residual = hidden_states
563+
self._emit_path_c_activation("hidden", residual)
480564
delta = self.route_delta(
481565
hidden_states,
482566
mask,
483567
kv_cache=kv_cache,
484568
attention_layer_idx=attention_layer_idx,
485569
doc_ids=doc_ids,
486570
)
571+
self._emit_path_c_activation("delta", delta)
487572
updated = residual + delta
573+
self._emit_path_c_activation("hidden_after", updated)
488574
if self.mhc is not None:
489575
return self.mhc([updated, residual])
490576
return updated
@@ -542,6 +628,7 @@ def route_delta(
542628
if kv_cache is not None and self.backend != "attention":
543629
raise ValueError("kv_cache may only be passed to attention route blocks")
544630
x = self.norm(hidden_states)
631+
self._emit_path_c_activation("normed", x)
545632
if self.backend == "attention":
546633
delta = cast(CausalSelfAttention, self.block)(
547634
x,
@@ -625,13 +712,191 @@ def route_roles(self) -> tuple[str, ...]:
625712
def path_c_bricks(self) -> tuple[dict[str, str], ...]:
626713
return tuple(
627714
{
628-
"name": f"layer_{index}_{block.layer.symbol.lower()}",
715+
"name": str(
716+
getattr(
717+
block,
718+
"path_c_profile_brick_name",
719+
f"layer_{index}_{block.layer.symbol.lower()}",
720+
)
721+
),
629722
"kind": block.backend,
630723
"route_symbol": block.layer.symbol,
724+
**(
725+
{
726+
"attention_qkv_has_bias": str(
727+
bool(block.attention_block.config.bias)
728+
).lower(),
729+
"attention_out_proj_has_bias": str(
730+
bool(block.attention_block.config.bias)
731+
).lower(),
732+
}
733+
if block.attention_block is not None
734+
else {}
735+
),
631736
}
632737
for index, block in enumerate(self.layers)
633738
)
634739

740+
def attach_path_c_activation_probe(self, probe: PathCActivationProbe) -> int:
741+
"""Install an explicit zero-copy activation probe on route blocks."""
742+
743+
if not callable(probe):
744+
raise TypeError("probe must be callable")
745+
for block in self.layers:
746+
block._path_c_activation_probe_callback = probe # type: ignore[attr-defined]
747+
return len(self.layers)
748+
749+
def detach_path_c_activation_probe(self) -> None:
750+
"""Remove the explicit Path C activation probe from all route blocks."""
751+
752+
for block in self.layers:
753+
if hasattr(block, "_path_c_activation_probe_callback"):
754+
delattr(block, "_path_c_activation_probe_callback")
755+
756+
def path_c_parameter_logical_aliases(self) -> dict[str, tuple[str, ...]]:
757+
"""Map MLX parameter tree names to Path C logical input names."""
758+
759+
parameter_names = {
760+
name
761+
for name, value in tree_flatten(self.trainable_parameters())
762+
if isinstance(value, mx.array)
763+
}
764+
aliases: dict[str, tuple[str, ...]] = {}
765+
766+
def add(
767+
layer_index: int,
768+
parameter_suffix: str,
769+
logical_suffix: str,
770+
*,
771+
block: HybridTinyBlock,
772+
) -> None:
773+
parameter_name = f"layers.{layer_index}.{parameter_suffix}"
774+
if parameter_name not in parameter_names:
775+
return
776+
logical_prefixes = [block.path_c_brick_name]
777+
profile_name = getattr(block, "path_c_profile_brick_name", None)
778+
if profile_name is not None and profile_name not in logical_prefixes:
779+
logical_prefixes.append(str(profile_name))
780+
aliases[parameter_name] = tuple(
781+
f"{prefix}_{logical_suffix}" for prefix in logical_prefixes
782+
)
783+
784+
for index, block in enumerate(self.layers):
785+
add(
786+
index,
787+
"norm.weight",
788+
"residual_norm_weight",
789+
block=block,
790+
)
791+
if block.backend == "mamba3":
792+
for parameter_suffix, logical_suffix in (
793+
("block.in_proj.weight", "mamba3_in_proj_weight"),
794+
("block.out_proj.weight", "mamba3_out_proj_weight"),
795+
("block.conv_weight", "mamba3_conv_weight"),
796+
("block.conv_bias", "mamba3_conv_bias"),
797+
("block.dt_bias", "mamba3_dt_bias"),
798+
("block.B_norm_weight", "mamba3_B_norm_weight"),
799+
("block.C_norm_weight", "mamba3_C_norm_weight"),
800+
("block.B_bias", "mamba3_B_bias"),
801+
("block.C_bias", "mamba3_C_bias"),
802+
("block.D", "mamba3_D"),
803+
):
804+
add(index, parameter_suffix, logical_suffix, block=block)
805+
elif block.backend == "m2rnn":
806+
for parameter_suffix, logical_suffix in (
807+
("block.in_proj.weight", "m2rnn_in_proj_weight"),
808+
("block.out_proj.weight", "m2rnn_out_proj_weight"),
809+
("block.conv_weight", "m2rnn_conv_weight"),
810+
("block.conv_bias", "m2rnn_conv_bias"),
811+
("block.g_norm.weight", "m2rnn_g_norm_weight"),
812+
("block.state_weight", "m2rnn_state_weight"),
813+
("block.A_log", "m2rnn_A_log"),
814+
("block.dt_bias", "m2rnn_dt_bias"),
815+
("block.D", "m2rnn_D"),
816+
):
817+
add(index, parameter_suffix, logical_suffix, block=block)
818+
elif block.backend == "attention":
819+
for parameter_suffix, logical_suffix in (
820+
(
821+
"block.q_proj.weight",
822+
"qkv_projection_attention_q_proj_weight",
823+
),
824+
(
825+
"block.q_proj.bias",
826+
"qkv_projection_attention_q_proj_bias",
827+
),
828+
(
829+
"block.sparse_kv_proj.weight",
830+
"qkv_projection_attention_sparse_kv_proj_weight",
831+
),
832+
(
833+
"block.sparse_kv_proj.bias",
834+
"qkv_projection_attention_sparse_kv_proj_bias",
835+
),
836+
(
837+
"block.rope_inv_freq",
838+
"qkv_projection_attention_rope_inv_freq",
839+
),
840+
(
841+
"block.out_proj.weight",
842+
"sparse_mla_fp8_apply_attention_out_proj_weight",
843+
),
844+
(
845+
"block.out_proj.bias",
846+
"sparse_mla_fp8_apply_attention_out_proj_bias",
847+
),
848+
):
849+
add(index, parameter_suffix, logical_suffix, block=block)
850+
851+
return aliases
852+
853+
def path_c_parameter_gradient_aliases(self) -> dict[str, tuple[str, ...]]:
854+
"""Map MLX parameter-grad tree names to Path C logical grad names."""
855+
856+
return {
857+
f"{parameter_name}_grad": tuple(
858+
f"{logical_name}_grad" for logical_name in logical_names
859+
)
860+
for parameter_name, logical_names in (
861+
self.path_c_parameter_logical_aliases()
862+
).items()
863+
}
864+
865+
def make_path_c_direct_fusion_chain_logical_buffer_owner(
866+
self,
867+
*,
868+
owner_name: str | None = None,
869+
) -> PathCLogicalBufferOwner:
870+
"""Expose model parameter tensors to direct Path C chain binding.
871+
872+
The owner only records existing MLX array references. It deliberately
873+
does not flatten, cast, or synthesize missing constants/state buffers.
874+
"""
875+
876+
parameters = {
877+
name: value
878+
for name, value in tree_flatten(self.trainable_parameters())
879+
if isinstance(value, mx.array)
880+
}
881+
buffers: dict[str, mx.array] = {}
882+
for parameter_name, logical_names in (
883+
self.path_c_parameter_logical_aliases()
884+
).items():
885+
tensor = parameters.get(parameter_name)
886+
if tensor is None:
887+
continue
888+
for logical_name in logical_names:
889+
buffers[logical_name] = tensor
890+
891+
profile_name = str(
892+
getattr(self, "path_c_profile_name", "HybridTinyLM")
893+
)
894+
return PathCLogicalBufferOwner(
895+
owner_name=owner_name
896+
or f"{profile_name}.path_c_model_parameter_buffers",
897+
buffers=buffers,
898+
)
899+
635900
def path_c_fusion_regions(
636901
self,
637902
*,
@@ -646,7 +911,9 @@ def path_c_fusion_regions(
646911

647912
return build_path_c_model_regions_from_model(
648913
self,
649-
region_prefix="hybrid_tiny_lm_path_c",
914+
region_prefix=str(
915+
getattr(self, "path_c_region_prefix", "hybrid_tiny_lm_path_c")
916+
),
650917
include_backward=include_backward,
651918
min_route_bricks=min_route_bricks,
652919
)
@@ -852,6 +1119,8 @@ def _attention_layer_count(layers: list[HybridTinyBlock]) -> int:
8521119
"HybridAttentionMode",
8531120
"HybridBackend",
8541121
"HybridBlockModule",
1122+
"PathCActivationBufferCapture",
1123+
"PathCActivationProbe",
8551124
"HybridTinyBlock",
8561125
"HybridTinyConfig",
8571126
"HybridTinyLM",

0 commit comments

Comments
 (0)