2020 CausalSelfAttention ,
2121 sparse_mla_fp8_route_enabled ,
2222)
23+ from cppmega_mlx .nn .concept import ConceptBlock , ConceptBlockConfig
24+ from cppmega_mlx .nn .engram import EngramBranch , EngramConfig
2325from cppmega_mlx .nn .m2rnn import M2RNNConfig , M2RNNMixer
2426from cppmega_mlx .nn .mamba3 import Mamba3Config , Mamba3ReferenceBlock
2527from cppmega_mlx .nn .mhc import ManifoldBranchMixer , ManifoldBranchMixerConfig
2830from cppmega_mlx .nn .structure_embedding import CppMegaStructureEmbedding
2931from cppmega_mlx .recipes .pattern import ExpandedNamPattern , NamLayer , expand_nam_pattern
3032from cppmega_mlx .runtime .kernel_policy import KernelPath , selected_path
31-
32- HybridBackend = Literal ["attention" , "mamba3" , "moe" , "m2rnn" ]
33- HybridBlockModule = CausalSelfAttention | Mamba3ReferenceBlock | ReferenceMoE | M2RNNMixer
33+ from cppmega_mlx .training .mtp import MinimalMTPHead , MTPLossConfig
34+
35+ HybridBackend = Literal ["attention" , "mamba3" , "moe" , "m2rnn" , "engram" , "concept" ]
36+ HybridBlockModule = (
37+ CausalSelfAttention
38+ | Mamba3ReferenceBlock
39+ | ReferenceMoE
40+ | M2RNNMixer
41+ | EngramBranch
42+ | ConceptBlock
43+ )
3444
3545_ROUTE_SYMBOL_BACKENDS : dict [str , HybridBackend ] = {
3646 "A" : "attention" ,
3747 "M" : "mamba3" ,
3848 "E" : "moe" ,
3949 "R" : "m2rnn" ,
50+ "N" : "engram" ,
51+ "C" : "concept" ,
4052}
4153
54+ HybridAttentionMode = Literal ["mla" , "dsa" , "full" , "gqa" ]
55+
4256
4357class StructureEmbeddingConfigKwargs (TypedDict ):
4458 hidden_size : int
@@ -110,6 +124,28 @@ class HybridTinyConfig:
110124 ngram_hash_seed : int | None = None
111125 mhc_enabled : bool = False
112126 grad_checkpoint : bool = False
127+ # Attention mode default applied to A-layers that did not get DSA routing.
128+ # "mla" preserves the legacy dense path. "full" and "gqa" are explicit
129+ # aliases for the same SDPA path with stricter num_kv_heads validation.
130+ attention_mode : HybridAttentionMode = "mla"
131+ # Engram (symbol "N") — local causal n-gram branch from cppmega_mlx.nn.engram.
132+ engram_ngram_orders : tuple [int , ...] = (2 , 3 , 4 )
133+ engram_bottleneck_dim : int = 0
134+ engram_dropout : float = 0.0
135+ engram_gated : bool = False
136+ engram_conv_kernel : int = 0
137+ # Concept (symbol "C") — concept-retrieval cross-attention ported from
138+ # nanochat. ``concept_dim=None`` means use hidden_size.
139+ concept_num_concepts : int = 64
140+ concept_num_heads : int = 4
141+ concept_dim : int | None = None
142+ # MTP — Multi-Token Prediction. When enabled, an MTPHead is attached to the
143+ # model in HybridTinyLM.__init__ and made reachable as ``model.mtp_head``.
144+ mtp_enabled : bool = False
145+ mtp_depth : int = 2
146+ mtp_decay : float = 0.6
147+ mtp_loss_weight : float = 0.3
148+ mtp_ignore_index : int = - 1
113149
114150 def __post_init__ (self ) -> None :
115151 if self .vocab_size < 2 :
@@ -141,13 +177,26 @@ def __post_init__(self) -> None:
141177 # Validate the route plan at config construction time, including DSA
142178 # ranks at tiny depths.
143179 self .expanded_pattern ()
144- self .attention_config ("mla" )
180+ # Validate the user-selected dense attention mode end-to-end.
181+ if self .attention_mode not in ("mla" , "dsa" , "full" , "gqa" ):
182+ raise ValueError (
183+ f"attention_mode must be one of 'mla', 'dsa', 'full', 'gqa', "
184+ f"got { self .attention_mode !r} "
185+ )
186+ self .attention_config (self .attention_mode )
187+ # Always also validate the dsa mode contract because dsa_a_layer_ranks
188+ # may pin specific A-layers to dsa regardless of attention_mode.
189+ if self .attention_mode != "dsa" :
190+ self .attention_config ("dsa" )
145191 self .mamba3_config ()
146192 self .m2rnn_config ()
147193 self .moe_config ()
148194 self .structure_embedding_config ()
149195 self .ngram_hash_config ()
150196 self .mhc_config ()
197+ self .engram_config ()
198+ self .concept_config ()
199+ self .mtp_config ()
151200
152201 def expanded_pattern (self ) -> ExpandedNamPattern :
153202 return expand_nam_pattern (
@@ -156,12 +205,15 @@ def expanded_pattern(self) -> ExpandedNamPattern:
156205 dsa_a_layer_ranks = self .dsa_a_layer_ranks ,
157206 )
158207
159- def attention_config (self , mode : Literal ["mla" , "dsa" ]) -> AttentionConfig :
208+ def attention_config (
209+ self , mode : HybridAttentionMode | None = None
210+ ) -> AttentionConfig :
211+ active_mode : HybridAttentionMode = mode if mode is not None else self .attention_mode
160212 return AttentionConfig (
161213 d_model = self .hidden_size ,
162214 num_q_heads = self .num_attention_heads ,
163215 num_kv_heads = self .num_attention_kv_heads ,
164- mode = mode ,
216+ mode = active_mode ,
165217 sparse_topk = self .attention_sparse_topk ,
166218 )
167219
@@ -263,9 +315,80 @@ def mhc_config(self) -> ManifoldBranchMixerConfig | None:
263315 return None
264316 return ManifoldBranchMixerConfig (hidden_size = self .hidden_size , max_branches = 2 )
265317
318+ def engram_config (self ) -> EngramConfig :
319+ return EngramConfig (
320+ hidden_size = self .hidden_size ,
321+ ngram_orders = self .engram_ngram_orders ,
322+ bottleneck_dim = self .engram_bottleneck_dim ,
323+ dropout = self .engram_dropout ,
324+ gated = self .engram_gated ,
325+ conv_kernel = self .engram_conv_kernel ,
326+ )
327+
328+ def concept_config (self ) -> ConceptBlockConfig :
329+ return ConceptBlockConfig (
330+ hidden_size = self .hidden_size ,
331+ num_concepts = self .concept_num_concepts ,
332+ num_heads = self .concept_num_heads ,
333+ concept_dim = self .concept_dim ,
334+ )
335+
336+ def mtp_config (self ) -> MTPLossConfig | None :
337+ if not self .mtp_enabled :
338+ return None
339+ return MTPLossConfig (
340+ depth = self .mtp_depth ,
341+ decay = self .mtp_decay ,
342+ loss_weight = self .mtp_loss_weight ,
343+ ignore_index = self .mtp_ignore_index ,
344+ )
345+
266346 def to_dict (self ) -> dict [str , object ]:
267347 return asdict (self )
268348
349+ def to_yaml (self ) -> str :
350+ """Serialize this config to a YAML document.
351+
352+ Lists are emitted as flow-style for compactness. Use ``from_yaml`` to
353+ parse the output back into a ``HybridTinyConfig``.
354+ """
355+ import yaml # PyYAML is part of the existing dev requirements.
356+
357+ return yaml .safe_dump (
358+ self .to_dict (),
359+ sort_keys = False ,
360+ default_flow_style = None ,
361+ )
362+
363+ @classmethod
364+ def from_dict (cls , data : dict [str , object ]) -> HybridTinyConfig :
365+ """Build a config from a plain dict (e.g., loaded YAML/JSON)."""
366+
367+ coerced = dict (data )
368+ # YAML and JSON lose tuple-ness; coerce sequence-typed fields back.
369+ for field_name in (
370+ "dsa_a_layer_ranks" ,
371+ "ngram_hash_orders" ,
372+ "engram_ngram_orders" ,
373+ ):
374+ if field_name in coerced and coerced [field_name ] is not None :
375+ coerced [field_name ] = tuple (coerced [field_name ]) # type: ignore[arg-type]
376+ return cls (** coerced ) # type: ignore[arg-type]
377+
378+ @classmethod
379+ def from_yaml (cls , text : str ) -> HybridTinyConfig :
380+ """Parse a YAML document into a ``HybridTinyConfig`` with validation."""
381+
382+ import yaml
383+
384+ loaded = yaml .safe_load (text )
385+ if not isinstance (loaded , dict ):
386+ raise ValueError (
387+ "from_yaml expects a YAML mapping at the top level, "
388+ f"got { type (loaded ).__name__ } "
389+ )
390+ return cls .from_dict (loaded )
391+
269392
270393class HybridTinyBlock (nn .Module ):
271394 """One pre-norm residual A/M/E/R route block.
@@ -290,7 +413,12 @@ def __init__(self, layer: NamLayer, config: HybridTinyConfig):
290413 self .block : HybridBlockModule
291414
292415 if layer .symbol == "A" :
293- mode = layer .attention_route or "mla"
416+ # DSA pinning via dsa_a_layer_ranks always wins. Otherwise the
417+ # model-wide attention_mode (default 'mla', or user-chosen
418+ # 'full'/'gqa') applies to this A-layer.
419+ mode : HybridAttentionMode = (
420+ "dsa" if layer .attention_route == "dsa" else config .attention_mode
421+ )
294422 self .backend : HybridBackend = "attention"
295423 self .block = CausalSelfAttention (config .attention_config (mode ))
296424 elif layer .symbol == "M" :
@@ -302,6 +430,12 @@ def __init__(self, layer: NamLayer, config: HybridTinyConfig):
302430 elif layer .symbol == "R" :
303431 self .backend = "m2rnn"
304432 self .block = M2RNNMixer (config .m2rnn_config ())
433+ elif layer .symbol == "N" :
434+ self .backend = "engram"
435+ self .block = EngramBranch (config .engram_config ())
436+ elif layer .symbol == "C" :
437+ self .backend = "concept"
438+ self .block = ConceptBlock (config .concept_config ())
305439 else : # pragma: no cover - expand_nam_pattern rejects this first.
306440 raise ValueError (f"unsupported hybrid layer symbol { layer .symbol !r} " )
307441
@@ -321,6 +455,14 @@ def moe_block(self) -> ReferenceMoE | None:
321455 def m2rnn_block (self ) -> M2RNNMixer | None :
322456 return self .block if self .backend == "m2rnn" else None # type: ignore[return-value]
323457
458+ @property
459+ def engram_block (self ) -> EngramBranch | None :
460+ return self .block if self .backend == "engram" else None # type: ignore[return-value]
461+
462+ @property
463+ def concept_block (self ) -> ConceptBlock | None :
464+ return self .block if self .backend == "concept" else None # type: ignore[return-value]
465+
324466 def __call__ (
325467 self ,
326468 hidden_states : mx .array ,
@@ -362,6 +504,8 @@ def validate_backend(self) -> None:
362504 "mamba3" : Mamba3ReferenceBlock ,
363505 "moe" : ReferenceMoE ,
364506 "m2rnn" : M2RNNMixer ,
507+ "engram" : EngramBranch ,
508+ "concept" : ConceptBlock ,
365509 }[self .backend ]
366510 if not isinstance (self .block , expected_cls ):
367511 raise ValueError (
@@ -403,6 +547,10 @@ def route_delta(
403547 )
404548 else :
405549 delta , _ = m2rnn (x )
550+ elif self .backend == "engram" :
551+ delta = cast (EngramBranch , self .block )(x )
552+ elif self .backend == "concept" :
553+ delta = cast (ConceptBlock , self .block )(x )
406554 else : # pragma: no cover - self.backend is fixed during construction.
407555 raise ValueError (f"unsupported hybrid backend { self .backend !r} " )
408556 return delta
@@ -440,6 +588,13 @@ def __init__(
440588 self .norm = nn .RMSNorm (cfg .hidden_size )
441589 self .lm_head = nn .Linear (cfg .hidden_size , cfg .vocab_size , bias = False )
442590
591+ mtp_config = cfg .mtp_config ()
592+ self .mtp_head : MinimalMTPHead | None = (
593+ MinimalMTPHead (self .token_embedding , self .lm_head , config = mtp_config )
594+ if mtp_config is not None
595+ else None
596+ )
597+
443598 if dtype is not None and dtype != mx .float32 :
444599 self .set_dtype (dtype )
445600
@@ -640,6 +795,7 @@ def _attention_layer_count(layers: list[HybridTinyBlock]) -> int:
640795 return sum (1 for layer in layers if layer .backend == "attention" )
641796
642797__all__ = [
798+ "HybridAttentionMode" ,
643799 "HybridBackend" ,
644800 "HybridBlockModule" ,
645801 "HybridTinyBlock" ,
0 commit comments