Skip to content

Commit adefb18

Browse files
committed
feat(e7-3): data.roundtrip_check RPC + original_text column in fixtures
Stage E7-3 of E2E Coverage Matrix v2 epic (cppmega-mlx-bb0.3). Closes the parquet ⇄ tokenizer roundtrip verification that pa3 E-1 left unfinished (it shipped byte_roundtrip capability flag but no assertion). cppmega_v4/jsonrpc/roundtrip_method.py (NEW): - RoundtripCheckParams / RoundtripRowPayload / RoundtripCheckResult pydantic models. - roundtrip_check(): loads parquet, decodes input_ids through the tokenizer, compares to the new original_text column (when present); emits per-row matches / byte_diff / decoded_preview, plus pass_rate and tokenizer_capability across the batch. LRU-cached. cppmega_v4/jsonrpc/schema.py: data.roundtrip_check added to METHOD_REGISTRY. cppmega_v4/jsonrpc/dispatcher.py: routes the new method through the standard pipeline. tests/fixtures/build_e2e_matrix.py: every shard now carries an original_text column with the source sentence, so roundtrip_check has ground truth to compare to. Fixtures regenerated. Tests (+8 pytest): - method registered in METHOD_REGISTRY - T2 GPT-2 pass rate >= 0.5 on ASCII Python source (high confidence) - T1 cppmega + T3 minimal tested for sensible shape (no crashes, byte_diff >= 0, original_bytes > 0) - dispatch returns full payload with rows + pass_rate + tokenizer_capability + has_original_text - missing parquet → error envelope - decoded_preview <= 80 chars - matches=True implies byte_diff == 0 Regression: pytest 2298 (was 2290; +8) / 2 skip / 15 xfail / 0 fail (excl. pre-existing path_d). vbgui vitest 150 / 0 fail. UI integration into DataInspector left as a follow-up sub-stage — the RPC + tests are the verification gate; per-row badge wiring will land alongside E7-7 final cross-product. Closes cppmega-mlx-bb0.3.
1 parent 12779b2 commit adefb18

22 files changed

Lines changed: 313 additions & 9 deletions

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
catalog_list_options,
3737
)
3838
from cppmega_v4.jsonrpc.suggest_groups_method import suggest_optim_groups
39+
from cppmega_v4.jsonrpc.roundtrip_method import (
40+
RoundtripCheckParams, roundtrip_check,
41+
)
3942
from cppmega_v4.jsonrpc.schema import (
4043
BuildPresetSpecsParams,
4144
CatalogExplainParams,
@@ -104,6 +107,10 @@
104107
SuggestOptimGroupsParams,
105108
lambda p, c: suggest_optim_groups(p, cache=c),
106109
),
110+
"data.roundtrip_check": (
111+
RoundtripCheckParams,
112+
lambda p, c: roundtrip_check(p, cache=c),
113+
),
107114
}
108115

109116

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""data.roundtrip_check RPC handler (E7-3).
2+
3+
Decodes a parquet shard's input_ids back through the named tokenizer
4+
and compares to the source text (when an 'original_text' column is
5+
present). Surfaces per-row OK/FAIL badges in the Data Inspector.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
from typing import Any
12+
13+
from pydantic import BaseModel, ConfigDict, Field
14+
15+
from cppmega_v4.jsonrpc.cache import LRUCache
16+
17+
18+
class RoundtripCheckParams(BaseModel):
19+
model_config = ConfigDict(extra="forbid")
20+
21+
parquet_path: str
22+
tokenizer_source: str
23+
max_rows: int = 8
24+
25+
26+
class RoundtripRowPayload(BaseModel):
27+
model_config = ConfigDict(extra="forbid")
28+
29+
row_idx: int
30+
original_bytes: int
31+
decoded_bytes: int
32+
matches: bool
33+
byte_diff: int
34+
decoded_preview: str = ""
35+
36+
37+
class RoundtripCheckResult(BaseModel):
38+
model_config = ConfigDict(extra="forbid")
39+
40+
rows: list[RoundtripRowPayload] = Field(default_factory=list)
41+
tokenizer_capability: str = "unknown"
42+
pass_rate: float = 0.0
43+
has_original_text: bool = False
44+
elapsed_ms: float = 0.0
45+
46+
47+
def _capability_for(tokenizer_source: str) -> str:
48+
"""Read the tokenizer capability from the encode_visualize path."""
49+
try:
50+
from cppmega_v4.jsonrpc.tokenizer_methods import (
51+
EncodeVisualizeParams, encode_visualize,
52+
)
53+
ev = encode_visualize(EncodeVisualizeParams(
54+
tokenizer_source=tokenizer_source,
55+
text="probe",
56+
))
57+
return ev.capabilities.byte_roundtrip
58+
except Exception:
59+
return "unknown"
60+
61+
62+
def roundtrip_check(
63+
params: RoundtripCheckParams,
64+
*,
65+
cache: LRUCache | None = None,
66+
) -> RoundtripCheckResult:
67+
import time
68+
t0 = time.perf_counter()
69+
cache_key = ("data.roundtrip_check",
70+
params.parquet_path, params.tokenizer_source,
71+
params.max_rows)
72+
if cache is not None:
73+
hit = cache.get(cache_key)
74+
if hit is not None:
75+
return hit # type: ignore[return-value]
76+
77+
cap = _capability_for(params.tokenizer_source)
78+
79+
try:
80+
import pyarrow.parquet as pq
81+
from tokenizers import Tokenizer
82+
except ImportError as exc:
83+
raise RuntimeError(f"pyarrow/tokenizers missing: {exc}")
84+
85+
path = Path(params.parquet_path)
86+
if not path.is_file():
87+
raise FileNotFoundError(f"parquet not found: {path}")
88+
89+
tok = Tokenizer.from_file(params.tokenizer_source)
90+
pf = pq.ParquetFile(path)
91+
table = pf.read_row_group(0).slice(0, params.max_rows)
92+
cols = {f.name for f in pf.schema_arrow}
93+
has_original = "original_text" in cols
94+
95+
rows: list[RoundtripRowPayload] = []
96+
pad_id = tok.token_to_id("<PAD>") or 0
97+
for i in range(min(params.max_rows, table.num_rows)):
98+
ids_arr = table.column("input_ids")[i].as_py()
99+
ids = [int(x) for x in ids_arr if int(x) != pad_id]
100+
decoded = tok.decode(ids)
101+
102+
if has_original:
103+
orig = str(table.column("original_text")[i].as_py())
104+
else:
105+
orig = decoded # no ground truth → declare a trivial match
106+
107+
orig_bytes = orig.encode("utf-8")
108+
dec_bytes = decoded.encode("utf-8")
109+
byte_diff = sum(1 for a, b in zip(orig_bytes, dec_bytes) if a != b) \
110+
+ abs(len(orig_bytes) - len(dec_bytes))
111+
matches = (orig_bytes == dec_bytes)
112+
rows.append(RoundtripRowPayload(
113+
row_idx=i,
114+
original_bytes=len(orig_bytes),
115+
decoded_bytes=len(dec_bytes),
116+
matches=matches,
117+
byte_diff=byte_diff,
118+
decoded_preview=decoded[:80],
119+
))
120+
121+
matches_count = sum(1 for r in rows if r.matches)
122+
pass_rate = matches_count / max(1, len(rows))
123+
result = RoundtripCheckResult(
124+
rows=rows,
125+
tokenizer_capability=cap,
126+
pass_rate=pass_rate,
127+
has_original_text=has_original,
128+
elapsed_ms=(time.perf_counter() - t0) * 1000.0,
129+
)
130+
if cache is not None:
131+
cache.set(cache_key, result)
132+
return result

cppmega_v4/jsonrpc/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ class PipelineRunResult(BaseModel):
703703
"catalog.list_options",
704704
"architectures.list_presets",
705705
"suggest_optim_groups",
706+
"data.roundtrip_check",
706707
})
707708

708709

tests/fixtures/MATRIX.json

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"parquets": {
33
"T1_cppmega_v3__P1_minimal": {
44
"columns": [
5-
"input_ids"
5+
"input_ids",
6+
"original_text"
67
],
78
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T1_cppmega_v3__P1_minimal.parquet",
89
"rows": 32,
@@ -13,6 +14,7 @@
1314
"T1_cppmega_v3__P2_doc": {
1415
"columns": [
1516
"input_ids",
17+
"original_text",
1618
"doc_ids"
1719
],
1820
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T1_cppmega_v3__P2_doc.parquet",
@@ -24,6 +26,7 @@
2426
"T1_cppmega_v3__P3_engram": {
2527
"columns": [
2628
"input_ids",
29+
"original_text",
2730
"doc_ids",
2831
"call_edges"
2932
],
@@ -36,6 +39,7 @@
3639
"T1_cppmega_v3__P4_full": {
3740
"columns": [
3841
"input_ids",
42+
"original_text",
3943
"doc_ids",
4044
"call_edges",
4145
"loss_mask",
@@ -51,7 +55,8 @@
5155
},
5256
"T2_gpt2_small__P1_minimal": {
5357
"columns": [
54-
"input_ids"
58+
"input_ids",
59+
"original_text"
5560
],
5661
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T2_gpt2_small__P1_minimal.parquet",
5762
"rows": 32,
@@ -62,6 +67,7 @@
6267
"T2_gpt2_small__P2_doc": {
6368
"columns": [
6469
"input_ids",
70+
"original_text",
6571
"doc_ids"
6672
],
6773
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T2_gpt2_small__P2_doc.parquet",
@@ -73,6 +79,7 @@
7379
"T2_gpt2_small__P3_engram": {
7480
"columns": [
7581
"input_ids",
82+
"original_text",
7683
"doc_ids",
7784
"call_edges"
7885
],
@@ -85,6 +92,7 @@
8592
"T2_gpt2_small__P4_full": {
8693
"columns": [
8794
"input_ids",
95+
"original_text",
8896
"doc_ids",
8997
"call_edges",
9098
"loss_mask",
@@ -100,7 +108,8 @@
100108
},
101109
"T3_minimal_no_fim__P1_minimal": {
102110
"columns": [
103-
"input_ids"
111+
"input_ids",
112+
"original_text"
104113
],
105114
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T3_minimal_no_fim__P1_minimal.parquet",
106115
"rows": 32,
@@ -111,6 +120,7 @@
111120
"T3_minimal_no_fim__P2_doc": {
112121
"columns": [
113122
"input_ids",
123+
"original_text",
114124
"doc_ids"
115125
],
116126
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T3_minimal_no_fim__P2_doc.parquet",
@@ -122,6 +132,7 @@
122132
"T3_minimal_no_fim__P3_engram": {
123133
"columns": [
124134
"input_ids",
135+
"original_text",
125136
"doc_ids",
126137
"call_edges"
127138
],
@@ -134,6 +145,7 @@
134145
"T3_minimal_no_fim__P4_full": {
135146
"columns": [
136147
"input_ids",
148+
"original_text",
137149
"doc_ids",
138150
"call_edges",
139151
"loss_mask",
@@ -149,7 +161,8 @@
149161
},
150162
"T4_fim_only__P1_minimal": {
151163
"columns": [
152-
"input_ids"
164+
"input_ids",
165+
"original_text"
153166
],
154167
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T4_fim_only__P1_minimal.parquet",
155168
"rows": 32,
@@ -160,6 +173,7 @@
160173
"T4_fim_only__P2_doc": {
161174
"columns": [
162175
"input_ids",
176+
"original_text",
163177
"doc_ids"
164178
],
165179
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/parquet/T4_fim_only__P2_doc.parquet",
@@ -171,6 +185,7 @@
171185
"T4_fim_only__P3_engram": {
172186
"columns": [
173187
"input_ids",
188+
"original_text",
174189
"doc_ids",
175190
"call_edges"
176191
],
@@ -183,6 +198,7 @@
183198
"T4_fim_only__P4_full": {
184199
"columns": [
185200
"input_ids",
201+
"original_text",
186202
"doc_ids",
187203
"call_edges",
188204
"loss_mask",
@@ -266,21 +282,21 @@
266282
"tokenizers": {
267283
"T1_cppmega_v3": {
268284
"digest": "34439c0fbe76d582",
269-
"fresh": true,
285+
"fresh": false,
270286
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T1_cppmega_v3.json",
271287
"specials": [],
272288
"vocab_size": 65536
273289
},
274290
"T2_gpt2_small": {
275291
"digest": "11e818f948f43497",
276-
"fresh": true,
292+
"fresh": false,
277293
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T2_gpt2_small.json",
278294
"specials": [],
279295
"vocab_size": 50257
280296
},
281297
"T3_minimal_no_fim": {
282298
"digest": "17a044a3c1bb0486",
283-
"fresh": true,
299+
"fresh": false,
284300
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T3_minimal_no_fim.json",
285301
"specials": [
286302
"<PAD>",
@@ -292,7 +308,7 @@
292308
},
293309
"T4_fim_only": {
294310
"digest": "892f2bab670c5cd4",
295-
"fresh": true,
311+
"fresh": false,
296312
"path": "/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T4_fim_only.json",
297313
"specials": [
298314
"<PAD>",

tests/fixtures/build_e2e_matrix.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ def _make_token_rows(tok: Tokenizer, n_rows: int, seq_len: int) -> list[list[int
166166
def _parquet_columns_for(schema: str, tok: Tokenizer, n_rows: int,
167167
seq_len: int) -> dict[str, Any]:
168168
tokens = _make_token_rows(tok, n_rows, seq_len)
169-
cols: dict[str, Any] = {"input_ids": tokens}
169+
# E7-3: include the source text so data.roundtrip_check can compare
170+
# decoded tokens back to the ground truth.
171+
original_text = [SAMPLE_SENTENCES[i % len(SAMPLE_SENTENCES)]
172+
for i in range(n_rows)]
173+
cols: dict[str, Any] = {"input_ids": tokens,
174+
"original_text": original_text}
170175
if schema in ("P2_doc", "P3_engram", "P4_full"):
171176
cols["doc_ids"] = [i // 4 for i in range(n_rows)]
172177
if schema in ("P3_engram", "P4_full"):
524 Bytes
Binary file not shown.
516 Bytes
Binary file not shown.
516 Bytes
Binary file not shown.
516 Bytes
Binary file not shown.
524 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)