Skip to content

Commit 4a28d24

Browse files
committed
Route side-channel training metadata
1 parent df792dd commit 4a28d24

6 files changed

Lines changed: 296 additions & 17 deletions

File tree

cppmega_mlx/data/batch.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
import mlx.core as mx
99
import 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)
1321
class 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+
230339
def _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+
]

cppmega_mlx/nn/structure_embedding.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ def __init__(
7171
self.stacked_emb = nn.Embedding(total_vocab, self.bottleneck_dim)
7272
self.up_proj = nn.Linear(self.bottleneck_dim, self.hidden_size, bias=False)
7373
self.stacked_emb.weight = mx.zeros_like(self.stacked_emb.weight)
74-
self.up_proj.weight = mx.zeros_like(self.up_proj.weight)
74+
# Keep the residual output exactly zero at init via the zero table, but
75+
# leave a small non-zero projection so CE gradients reach the table.
76+
self.up_proj.weight = self.up_proj.weight * mx.array(
77+
0.02,
78+
dtype=self.up_proj.weight.dtype,
79+
)
7580
self.component_scales = mx.full(
7681
(len(self.active_component_names),),
7782
1.0 / len(self.active_component_names),
@@ -180,4 +185,3 @@ def __call__(
180185

181186

182187
CppMegaStructureEmbedding = StructureEmbedding
183-

cppmega_mlx/training/compiled.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"ast_depth_ids",
5656
"sibling_index_ids",
5757
"node_type_ids",
58+
"platform_ids",
5859
)
5960

6061
CompiledBatch = dict[str, mx.array | None]

cppmega_mlx/training/loss.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
import mlx.core as mx
99
import mlx.nn as nn
1010

11-
from cppmega_mlx.data.batch import LMTokenBatch, ensure_lm_batch
11+
from cppmega_mlx.data.batch import (
12+
LMTokenBatch,
13+
SideChannelDropoutPolicy,
14+
ensure_lm_batch,
15+
)
1216
from cppmega_mlx.nn.structure_embedding import StructureEmbedding
1317
from cppmega_mlx.training.cut_cross_entropy import (
1418
DEFAULT_CHUNK_ROWS,
@@ -31,10 +35,18 @@
3135
def next_token_cross_entropy(
3236
model: nn.Module,
3337
batch: LMTokenBatch | Mapping[str, mx.array] | mx.array,
38+
*,
39+
side_channel_dropout: SideChannelDropoutPolicy | None = None,
40+
side_channel_dropout_seed: int | None = None,
3441
) -> tuple[mx.array, mx.array]:
3542
"""Return masked next-token CE loss and the number of contributing tokens."""
3643

3744
lm_batch = ensure_lm_batch(batch)
45+
lm_batch = lm_batch.with_side_channel_dropout(
46+
side_channel_dropout,
47+
seed=side_channel_dropout_seed,
48+
training=True,
49+
)
3850
document_ids = lm_batch.input_document_ids
3951
model_kwargs = lm_batch.model_kwargs()
4052
if document_ids is not None:
@@ -63,6 +75,8 @@ def next_token_cut_cross_entropy(
6375
*,
6476
chunk_rows: int = DEFAULT_CHUNK_ROWS,
6577
eval_chunks: bool = True,
78+
side_channel_dropout: SideChannelDropoutPolicy | None = None,
79+
side_channel_dropout_seed: int | None = None,
6680
) -> tuple[mx.array, mx.array]:
6781
"""Return masked next-token CE via the MLX-native chunked linear path.
6882
@@ -73,6 +87,11 @@ def next_token_cut_cross_entropy(
7387
"""
7488

7589
lm_batch = ensure_lm_batch(batch)
90+
lm_batch = lm_batch.with_side_channel_dropout(
91+
side_channel_dropout,
92+
seed=side_channel_dropout_seed,
93+
training=True,
94+
)
7695
document_ids = lm_batch.input_document_ids
7796
hidden_states = _decoder_hidden_states_for_mtp(
7897
model,

0 commit comments

Comments
 (0)