Skip to content

Commit 0211553

Browse files
committed
feat(data): carry semantic temporal parquet side channels
1 parent 36c3445 commit 0211553

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

cppmega_mlx/data/parquet_dataset.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@
6868
"hunk_id_per_token",
6969
"edit_op_per_token",
7070
)
71+
_FAMILY_TOKEN_SIDE_CHANNEL_COLUMNS: Mapping[str, tuple[str, ...]] = {
72+
"semantic_graph": _TOKEN_SEMANTIC_METADATA_COLUMNS,
73+
"temporal_diff": _TOKEN_TEMPORAL_METADATA_COLUMNS,
74+
}
75+
_FAMILY_TOKEN_SIDE_CHANNEL_COLUMNS_FLAT = tuple(
76+
column
77+
for columns in _FAMILY_TOKEN_SIDE_CHANNEL_COLUMNS.values()
78+
for column in columns
79+
)
7180
_SOURCE_PLATFORM_IDS_COLUMN = "source_platform_ids"
7281
_VALID_TOKEN_COUNT_COLUMN = "valid_token_count"
7382
_ROW_METADATA_COLUMNS = (
@@ -179,6 +188,12 @@ class _SideChannelColumns:
179188
skipped: Sequence[Mapping[str, str | None]]
180189

181190

191+
@dataclass(frozen=True)
192+
class _FamilySideChannelColumns:
193+
channels: Mapping[str, Mapping[str, np.ndarray]]
194+
sources: Mapping[str, Mapping[str, Mapping[str, str | None]]]
195+
196+
182197
@dataclass(frozen=True)
183198
class _ModelMetadataColumns:
184199
channels: Mapping[str, np.ndarray]
@@ -288,6 +303,11 @@ def __init__(
288303
)
289304
side_channel_columns = _side_channel_windows(columns, token_rows, seq_len)
290305
side_channels = side_channel_columns.channels
306+
family_side_channel_columns = _family_side_channel_windows(
307+
columns,
308+
token_rows,
309+
seq_len,
310+
)
291311
model_metadata_columns = _model_metadata_windows(columns, token_rows, seq_len)
292312
batch_metadata_columns = _resolve_batch_metadata_columns(
293313
columns,
@@ -325,6 +345,13 @@ def __init__(
325345
key: _to_side_channel_values(key, value)
326346
for key, value in side_channels.items()
327347
}
348+
self._family_side_channels = {
349+
family: {
350+
column: value.astype(np.int32, copy=False)
351+
for column, value in family_columns.items()
352+
}
353+
for family, family_columns in family_side_channel_columns.channels.items()
354+
}
328355
self._model_metadata_channels = {
329356
key: value.astype(np.int32, copy=False)
330357
for key, value in model_metadata_columns.channels.items()
@@ -342,6 +369,7 @@ def __init__(
342369
document_id_source=document_id_source,
343370
side_channel_sources=side_channel_columns.sources,
344371
skipped_side_channels=side_channel_columns.skipped,
372+
family_side_channel_sources=family_side_channel_columns.sources,
345373
model_metadata_sources=model_metadata_columns.sources,
346374
batch_metadata_columns=batch_metadata_columns,
347375
)
@@ -441,6 +469,13 @@ def _make_batch(self, sample_idx: np.ndarray) -> LMTokenBatch:
441469
for key, value in self._model_metadata_channels.items()
442470
}
443471
)
472+
family_side_channels = {
473+
family: {
474+
column: mx.array(value[sample_idx])
475+
for column, value in family_columns.items()
476+
}
477+
for family, family_columns in self._family_side_channels.items()
478+
}
444479
return LMTokenBatch(
445480
tokens=mx.array(self._tokens[sample_idx]),
446481
target_tokens=None
@@ -452,6 +487,7 @@ def _make_batch(self, sample_idx: np.ndarray) -> LMTokenBatch:
452487
document_ids=None
453488
if self._document_ids is None
454489
else mx.array(self._document_ids[sample_idx]),
490+
side_channels=family_side_channels or None,
455491
metadata=self._make_batch_metadata(sample_idx),
456492
**kwargs,
457493
)
@@ -496,6 +532,7 @@ def _candidate_parquet_columns(
496532
candidates.append(text_key)
497533
candidates.extend(_LOSS_MASK_COLUMN_ALIASES)
498534
candidates.extend(_DOCUMENT_ID_COLUMN_ALIASES)
535+
candidates.extend(_FAMILY_TOKEN_SIDE_CHANNEL_COLUMNS_FLAT)
499536
candidates.append(_SOURCE_PLATFORM_IDS_COLUMN)
500537
candidates.append(_VALID_TOKEN_COUNT_COLUMN)
501538
for aliases in _SIDE_CHANNEL_COLUMN_ALIASES.values():
@@ -721,6 +758,41 @@ def _side_channel_windows(
721758
return _SideChannelColumns(channels=channels, sources=sources, skipped=skipped)
722759

723760

761+
def _family_side_channel_windows(
762+
columns: ParquetColumns,
763+
token_rows: list[list[int]],
764+
seq_len: int,
765+
) -> _FamilySideChannelColumns:
766+
channels: dict[str, dict[str, np.ndarray]] = {}
767+
sources: dict[str, dict[str, dict[str, str | None]]] = {}
768+
for family, family_columns in _FAMILY_TOKEN_SIDE_CHANNEL_COLUMNS.items():
769+
for column in family_columns:
770+
if column not in columns.values:
771+
continue
772+
_reject_non_integer_parquet_type(
773+
columns,
774+
column,
775+
f"{column} {family} side-channel IDs",
776+
)
777+
rows = [
778+
_coerce_token_row(value, label=f"{column} {family} side-channel")
779+
for value in columns.require(column)
780+
]
781+
if not _rows_are_token_aligned(rows, token_rows):
782+
raise ValueError(
783+
f"{column} rows must be token-aligned with token IDs"
784+
)
785+
channels.setdefault(family, {})[column] = _fixed_windows_from_rows(
786+
rows,
787+
seq_len,
788+
)
789+
sources.setdefault(family, {})[column] = {
790+
"column": column,
791+
"type": columns.type_label(column),
792+
}
793+
return _FamilySideChannelColumns(channels=channels, sources=sources)
794+
795+
724796
def _model_metadata_windows(
725797
columns: ParquetColumns,
726798
token_rows: list[list[int]],
@@ -1137,6 +1209,10 @@ def _parquet_receipt(
11371209
document_id_source: str | None,
11381210
side_channel_sources: Mapping[str, Mapping[str, str | None]],
11391211
skipped_side_channels: Sequence[Mapping[str, str | None]],
1212+
family_side_channel_sources: Mapping[
1213+
str,
1214+
Mapping[str, Mapping[str, str | None]],
1215+
],
11401216
model_metadata_sources: Mapping[str, Mapping[str, str | None]],
11411217
batch_metadata_columns: Sequence[str],
11421218
) -> dict[str, Any]:
@@ -1173,6 +1249,14 @@ def _parquet_receipt(
11731249
receipt["model_metadata_sources"] = {
11741250
key: dict(value) for key, value in sorted(model_metadata_sources.items())
11751251
}
1252+
if family_side_channel_sources:
1253+
receipt["family_side_channel_sources"] = {
1254+
family: {
1255+
column: dict(source)
1256+
for column, source in sorted(columns.items())
1257+
}
1258+
for family, columns in sorted(family_side_channel_sources.items())
1259+
}
11761260
training_sources = {
11771261
"target_tokens": target_source,
11781262
"loss_mask": loss_mask_source,

tests/test_parquet_dataset.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import numpy as np
66
import pytest
7+
import mlx.core as mx
78
import mlx.nn as nn
89

910
from cppmega_mlx.data.parquet_dataset import TokenParquetDataset
@@ -334,6 +335,114 @@ def test_all_metadata_excludes_token_content_and_slices_token_level_fields() ->
334335
assert metadata_windows[1]["repo"] == "llvm"
335336

336337

338+
def test_parquet_token_semantic_and_temporal_metadata_reach_side_channel_map(
339+
tmp_path,
340+
) -> None:
341+
pa = pytest.importorskip("pyarrow")
342+
pq = pytest.importorskip("pyarrow.parquet")
343+
path = tmp_path / "semantic_temporal.parquet"
344+
table = pa.table(
345+
{
346+
"token_ids": pa.array(
347+
[[0, 1, 2, 3, 4, 5, 6, 7]],
348+
type=pa.large_list(pa.int32()),
349+
),
350+
"token_symbol_ids": pa.array(
351+
[[10, 11, 12, 13, 14, 15, 16, 17]],
352+
type=pa.large_list(pa.int32()),
353+
),
354+
"token_call_targets": pa.array(
355+
[[20, 21, 22, 23, 24, 25, 26, 27]],
356+
type=pa.large_list(pa.int32()),
357+
),
358+
"token_change_mask_post": pa.array(
359+
[[0, 0, 1, 1, 0, 1, 0, 1]],
360+
type=pa.large_list(pa.int32()),
361+
),
362+
"edit_op_per_token": pa.array(
363+
[[0, 0, 2, 2, 0, 3, 0, 3]],
364+
type=pa.large_list(pa.int32()),
365+
),
366+
}
367+
)
368+
pq.write_table(table, path)
369+
370+
dataset = TokenParquetDataset(path, seq_len=4, batch_size=2, token_key="token_ids")
371+
batch = next(dataset.iter_batches())
372+
373+
assert batch.side_channels is not None
374+
assert tuple(batch.model_kwargs()) == ()
375+
assert set(batch.side_channels) == {"semantic_graph", "temporal_diff"}
376+
np.testing.assert_array_equal(
377+
np.array(batch.side_channels["semantic_graph"]["token_symbol_ids"]),
378+
[[10, 11, 12, 13], [14, 15, 16, 17]],
379+
)
380+
np.testing.assert_array_equal(
381+
np.array(batch.side_channels["semantic_graph"]["token_call_targets"]),
382+
[[20, 21, 22, 23], [24, 25, 26, 27]],
383+
)
384+
np.testing.assert_array_equal(
385+
np.array(batch.side_channels["temporal_diff"]["token_change_mask_post"]),
386+
[[0, 0, 1, 1], [0, 1, 0, 1]],
387+
)
388+
np.testing.assert_array_equal(
389+
np.array(batch.side_channels["temporal_diff"]["edit_op_per_token"]),
390+
[[0, 0, 2, 2], [0, 3, 0, 3]],
391+
)
392+
393+
dropped = batch.with_side_channel_dropout(
394+
{"semantic_graph": 1.0, "temporal_diff": 1.0},
395+
seed=7,
396+
)
397+
assert dropped.side_channels is not None
398+
assert float(
399+
mx.sum(
400+
mx.abs(dropped.side_channels["semantic_graph"]["token_symbol_ids"])
401+
).item()
402+
) == 0.0
403+
assert float(
404+
mx.sum(
405+
mx.abs(dropped.side_channels["temporal_diff"]["edit_op_per_token"])
406+
).item()
407+
) == 0.0
408+
assert (
409+
dataset.parquet_receipt["family_side_channel_sources"]["semantic_graph"][
410+
"token_symbol_ids"
411+
]["column"]
412+
== "token_symbol_ids"
413+
)
414+
assert (
415+
dataset.parquet_receipt["family_side_channel_sources"]["temporal_diff"][
416+
"edit_op_per_token"
417+
]["column"]
418+
== "edit_op_per_token"
419+
)
420+
421+
422+
def test_parquet_token_semantic_side_channel_fails_closed_when_not_token_aligned(
423+
tmp_path,
424+
) -> None:
425+
pa = pytest.importorskip("pyarrow")
426+
pq = pytest.importorskip("pyarrow.parquet")
427+
path = tmp_path / "bad_semantic.parquet"
428+
table = pa.table(
429+
{
430+
"token_ids": pa.array(
431+
[[0, 1, 2, 3, 4, 5, 6, 7]],
432+
type=pa.large_list(pa.int32()),
433+
),
434+
"token_symbol_ids": pa.array(
435+
[[10, 11]],
436+
type=pa.large_list(pa.int32()),
437+
),
438+
}
439+
)
440+
pq.write_table(table, path)
441+
442+
with pytest.raises(ValueError, match="token_symbol_ids.*token-aligned"):
443+
TokenParquetDataset(path, seq_len=4, batch_size=1, token_key="token_ids")
444+
445+
337446
def test_platform_ids_parquet_column_threads_to_model_kwargs(tmp_path) -> None:
338447
pa = pytest.importorskip("pyarrow")
339448
pq = pytest.importorskip("pyarrow.parquet")

0 commit comments

Comments
 (0)