88from __future__ import annotations
99
1010from 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
1322import mlx .core as mx
1423import mlx .nn as nn
24+ from mlx .utils import tree_flatten
1525
1626from cppmega_mlx .data .packing import mlx_document_boundary_mask
1727from cppmega_mlx .inference .engine import ContiguousKVCache , kv_cache_position
2939from cppmega_mlx .nn .ngram_hash import NgramHashEmbedding
3040from cppmega_mlx .nn .structure_embedding import CppMegaStructureEmbedding
3141from cppmega_mlx .recipes .pattern import ExpandedNamPattern , NamLayer , expand_nam_pattern
42+ from cppmega_mlx .runtime .path_c_physical_abi import PathCLogicalBufferOwner
3243from cppmega_mlx .runtime .kernel_policy import KernelPath , selected_path
3344from cppmega_mlx .training .mtp import MinimalMTPHead , MTPLossConfig
3445
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 )
72119class 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