88import mlx .core as mx
99import numpy as np
1010
11+ SideChannelDropoutPolicy = Mapping [str , float ]
12+
13+ _SIDE_CHANNEL_FAMILY_FIELDS : Mapping [str , tuple [str , ...]] = {
14+ "platform" : ("platform_ids" ,),
15+ "syntax" : ("ast_depth_ids" , "sibling_index_ids" , "node_type_ids" ),
16+ "structure" : ("structure_ids" , "dep_levels" ),
17+ }
18+
1119
1220@dataclass (frozen = True )
1321class LMTokenBatch :
@@ -24,6 +32,7 @@ class LMTokenBatch:
2432 sibling_index_ids : mx .array | None = None
2533 node_type_ids : mx .array | None = None
2634 platform_ids : mx .array | None = None
35+ side_channels : Mapping [str , Mapping [str , mx .array ]] | None = None
2736 metadata : Mapping [str , Any ] | None = None
2837
2938 def __post_init__ (self ) -> None :
@@ -105,6 +114,8 @@ def __post_init__(self) -> None:
105114 mx .eval (has_negative )
106115 if bool (has_negative .item ()):
107116 raise ValueError ("platform_ids must be non-negative" )
117+ if self .side_channels is not None :
118+ _validate_side_channel_map (self .side_channels )
108119 if self .metadata is not None and not isinstance (self .metadata , Mapping ):
109120 raise ValueError ("metadata must be a mapping when provided" )
110121
@@ -163,6 +174,62 @@ def model_kwargs(self) -> dict[str, mx.array]:
163174 )
164175 return kwargs
165176
177+ def side_channel_map (self ) -> dict [str , dict [str , mx .array ]]:
178+ """Return side-channel tensors grouped by family and column name."""
179+
180+ out : dict [str , dict [str , mx .array ]] = {
181+ family : {}
182+ for family in _SIDE_CHANNEL_FAMILY_FIELDS
183+ }
184+ for family , fields in _SIDE_CHANNEL_FAMILY_FIELDS .items ():
185+ for field_name in fields :
186+ value = getattr (self , field_name )
187+ if value is not None :
188+ out [family ][field_name ] = value
189+ if self .side_channels is not None :
190+ for family , columns in self .side_channels .items ():
191+ out .setdefault (family , {}).update (dict (columns ))
192+ return {family : columns for family , columns in out .items () if columns }
193+
194+ def with_side_channel_dropout (
195+ self ,
196+ policy : SideChannelDropoutPolicy | None ,
197+ * ,
198+ seed : int | None = None ,
199+ training : bool = True ,
200+ ) -> "LMTokenBatch" :
201+ """Apply shape-stable family dropout by zeroing selected channels."""
202+
203+ if not training or not policy :
204+ return self
205+ rates = _validate_side_channel_dropout_policy (policy )
206+ if not rates :
207+ return self
208+ rng = np .random .default_rng (seed )
209+ dropped = {
210+ family
211+ for family , rate in rates .items ()
212+ if rate >= 1.0 or (rate > 0.0 and float (rng .random ()) < rate )
213+ }
214+ if not dropped :
215+ return self
216+
217+ kwargs = self .as_dict (include_metadata = True , include_side_channels = True )
218+ for family in dropped :
219+ for field_name in _SIDE_CHANNEL_FAMILY_FIELDS .get (family , ()):
220+ value = kwargs .get (field_name )
221+ if value is not None :
222+ kwargs [field_name ] = mx .zeros_like (value )
223+ if self .side_channels is not None :
224+ kwargs ["side_channels" ] = {
225+ family : {
226+ name : mx .zeros_like (value ) if family in dropped else value
227+ for name , value in columns .items ()
228+ }
229+ for family , columns in self .side_channels .items ()
230+ }
231+ return LMTokenBatch (** kwargs )
232+
166233 def _target_aligned (self , value : mx .array ) -> mx .array :
167234 if tuple (value .shape ) == tuple (self .targets .shape ):
168235 return value
@@ -206,7 +273,12 @@ def training_metadata(self) -> dict[str, Any]:
206273
207274 return {} if self .metadata is None else dict (self .metadata )
208275
209- def as_dict (self , * , include_metadata : bool = False ) -> dict [str , Any ]:
276+ def as_dict (
277+ self ,
278+ * ,
279+ include_metadata : bool = False ,
280+ include_side_channels : bool = False ,
281+ ) -> dict [str , Any ]:
210282 data : dict [str , Any ] = {"tokens" : self .tokens }
211283 if self .target_tokens is not None :
212284 data ["target_tokens" ] = self .target_tokens
@@ -219,6 +291,8 @@ def as_dict(self, *, include_metadata: bool = False) -> dict[str, Any]:
219291 data .update ({k : v for k , v in self .structure_fields ().items () if v is not None })
220292 if self .platform_ids is not None :
221293 data ["platform_ids" ] = self .platform_ids
294+ if include_side_channels and self .side_channels is not None :
295+ data ["side_channels" ] = self .side_channels
222296 if include_metadata and self .metadata is not None :
223297 data ["metadata" ] = self .metadata
224298 return data
@@ -227,6 +301,41 @@ def as_dict(self, *, include_metadata: bool = False) -> dict[str, Any]:
227301_DOCUMENT_ID_ALIASES = ("document_ids" , "doc_ids" , "packing_document_ids" )
228302
229303
304+ def _validate_side_channel_map (
305+ side_channels : Mapping [str , Mapping [str , mx .array ]],
306+ ) -> None :
307+ if not isinstance (side_channels , Mapping ):
308+ raise ValueError ("side_channels must be a mapping when provided" )
309+ for family , columns in side_channels .items ():
310+ if not isinstance (family , str ) or not family .strip ():
311+ raise ValueError ("side channel family names must be non-empty strings" )
312+ if not isinstance (columns , Mapping ):
313+ raise ValueError (f"side channel family { family !r} must map column names" )
314+ for name , value in columns .items ():
315+ if not isinstance (name , str ) or not name .strip ():
316+ raise ValueError ("side channel column names must be non-empty strings" )
317+ if not isinstance (value , mx .array ):
318+ raise ValueError (
319+ f"side channel { family } .{ name } must be an mlx array"
320+ )
321+
322+
323+ def _validate_side_channel_dropout_policy (
324+ policy : SideChannelDropoutPolicy ,
325+ ) -> dict [str , float ]:
326+ rates : dict [str , float ] = {}
327+ for family , rate in policy .items ():
328+ if not isinstance (family , str ) or not family .strip ():
329+ raise ValueError ("side channel dropout family names must be non-empty" )
330+ value = float (rate )
331+ if not 0.0 <= value <= 1.0 :
332+ raise ValueError (
333+ f"side channel dropout for { family !r} must be in [0, 1], got { rate !r} "
334+ )
335+ rates [family ] = value
336+ return rates
337+
338+
230339def _document_ids_from_mapping (batch : Mapping [str , Any ]) -> Any | None :
231340 present = [
232341 alias
@@ -271,6 +380,7 @@ def ensure_lm_batch(batch: LMTokenBatch | Mapping[str, Any] | mx.array) -> LMTok
271380 sibling_index_ids = batch .get ("sibling_index_ids" ),
272381 node_type_ids = batch .get ("node_type_ids" ),
273382 platform_ids = batch .get ("platform_ids" ),
383+ side_channels = batch .get ("side_channels" ),
274384 metadata = batch .get ("metadata" ),
275385 )
276386 raise TypeError (f"unsupported batch type: { type (batch )!r} " )
@@ -331,4 +441,9 @@ def synthetic_token_batch(
331441 )
332442
333443
334- __all__ = ["LMTokenBatch" , "ensure_lm_batch" , "synthetic_token_batch" ]
444+ __all__ = [
445+ "LMTokenBatch" ,
446+ "SideChannelDropoutPolicy" ,
447+ "ensure_lm_batch" ,
448+ "synthetic_token_batch" ,
449+ ]
0 commit comments