|
40 | 40 | _make_loss, _make_optim, _make_sharding, _graph_to_specs, |
41 | 41 | ) |
42 | 42 | from cppmega_v4.jsonrpc.schema import VerifyParams |
| 43 | +from cppmega_mlx.tokenizer.fingerprint import tokenizer_fingerprint |
43 | 44 |
|
44 | 45 |
|
45 | 46 | StageStatus = Literal["ok", "skipped", "fail", "cancelled"] |
@@ -253,21 +254,34 @@ def stage_estimate_memory(ctx: StageContext) -> StageResult: |
253 | 254 | master_fp32_bytes = int(params_bytes) |
254 | 255 | # Inference probe + token embedding forward stash ~params/2. |
255 | 256 | probe_bytes = int(params_bytes // 2) |
| 257 | + # MLX peak counters include any allocator-resident buffers already |
| 258 | + # active in this long-lived Python process. Account for that baseline |
| 259 | + # so full-suite parity is not hostage to previous Metal tests. |
| 260 | + allocator_active_bytes = _mlx_active_memory_bytes() |
256 | 261 | estimated_peak_bytes = (params_bytes + activation_bytes |
257 | 262 | + adam_moments_bytes + grad_bytes |
258 | | - + master_fp32_bytes + probe_bytes) |
| 263 | + + master_fp32_bytes + probe_bytes |
| 264 | + + allocator_active_bytes) |
259 | 265 | return _ok( |
260 | 266 | "estimate_memory", t0, |
261 | 267 | total_bytes=params_bytes, |
262 | 268 | params_bytes=params_bytes, |
263 | 269 | activation_bytes=activation_bytes, |
264 | 270 | adam_moments_bytes=adam_moments_bytes, |
| 271 | + allocator_active_bytes=allocator_active_bytes, |
265 | 272 | estimated_peak_bytes=estimated_peak_bytes, |
266 | 273 | ) |
267 | 274 | except Exception as exc: |
268 | 275 | return _fail("estimate_memory", t0, exc) |
269 | 276 |
|
270 | 277 |
|
| 278 | +def _mlx_active_memory_bytes() -> int: |
| 279 | + try: |
| 280 | + return int(mx.get_active_memory()) |
| 281 | + except Exception: |
| 282 | + return 0 |
| 283 | + |
| 284 | + |
271 | 285 | def stage_check_gotchas(ctx: StageContext) -> StageResult: |
272 | 286 | t0 = time.perf_counter() |
273 | 287 | try: |
@@ -771,6 +785,10 @@ def loss_fn(model: nn.Module, emb: mx.array, tgt: mx.array) -> mx.array: |
771 | 785 | parquet_stream = None |
772 | 786 | targets = mx.random.randint(0, vocab_size, shape=(batch, seq)) |
773 | 787 | if len(parquet_shards) > 1: |
| 788 | + _validate_parquet_stream_tokenizer_fingerprints( |
| 789 | + parquet_shards, |
| 790 | + expected_tokenizer=tokenizer_path, |
| 791 | + ) |
774 | 792 | if tokenizer_path: |
775 | 793 | tokens, used, stream = _tokenize_parquet_text_from_shards( |
776 | 794 | parquet_shards, tokenizer_path, n_tokens=batch * seq) |
@@ -798,6 +816,10 @@ def loss_fn(model: nn.Module, emb: mx.array, tgt: mx.array) -> mx.array: |
798 | 816 | except Exception: |
799 | 817 | pass |
800 | 818 | elif parquet_path: |
| 819 | + _validate_parquet_stream_tokenizer_fingerprints( |
| 820 | + [str(parquet_path)], |
| 821 | + expected_tokenizer=tokenizer_path, |
| 822 | + ) |
801 | 823 | # V4-2: prefer tokenize(text) path when both tokenizer and a |
802 | 824 | # 'text' column are present; fall back to raw-int input_ids |
803 | 825 | # column (V3-2) when not; fall through to synthetic otherwise. |
@@ -1853,6 +1875,56 @@ def _parquet_row_count(path: str) -> int: |
1853 | 1875 | return 0 |
1854 | 1876 |
|
1855 | 1877 |
|
| 1878 | +def _parquet_tokenizer_fingerprint(path: str) -> str | None: |
| 1879 | + import pyarrow.parquet as pq |
| 1880 | + try: |
| 1881 | + parquet_file = pq.ParquetFile(path) |
| 1882 | + except Exception: |
| 1883 | + return None |
| 1884 | + if "tokenizer_fingerprint" not in parquet_file.schema_arrow.names: |
| 1885 | + return None |
| 1886 | + seen: set[str] = set() |
| 1887 | + for batch in parquet_file.iter_batches( |
| 1888 | + columns=["tokenizer_fingerprint"], |
| 1889 | + batch_size=1024, |
| 1890 | + ): |
| 1891 | + for value in batch.column(0).to_pylist(): |
| 1892 | + if value is not None: |
| 1893 | + seen.add(str(value)) |
| 1894 | + if len(seen) > 1: |
| 1895 | + raise ValueError( |
| 1896 | + f"tokenizer_fingerprint mismatch within shard {path}" |
| 1897 | + ) |
| 1898 | + return next(iter(seen)) if seen else None |
| 1899 | + |
| 1900 | + |
| 1901 | +def _validate_parquet_stream_tokenizer_fingerprints( |
| 1902 | + parquet_paths: list[str], |
| 1903 | + *, |
| 1904 | + expected_tokenizer: Any | None = None, |
| 1905 | +) -> str | None: |
| 1906 | + fingerprints = { |
| 1907 | + fingerprint |
| 1908 | + for path in parquet_paths |
| 1909 | + if (fingerprint := _parquet_tokenizer_fingerprint(path)) is not None |
| 1910 | + } |
| 1911 | + if len(fingerprints) > 1: |
| 1912 | + joined = ", ".join(sorted(fingerprints)) |
| 1913 | + raise ValueError( |
| 1914 | + "tokenizer_fingerprint mismatch across parquet shards: " |
| 1915 | + f"{joined}" |
| 1916 | + ) |
| 1917 | + fingerprint = next(iter(fingerprints)) if fingerprints else None |
| 1918 | + if fingerprint is not None and expected_tokenizer is not None: |
| 1919 | + expected = tokenizer_fingerprint(expected_tokenizer) |
| 1920 | + if fingerprint != expected: |
| 1921 | + raise ValueError( |
| 1922 | + "tokenizer_fingerprint mismatch between parquet shards " |
| 1923 | + f"and active tokenizer: shard={fingerprint} active={expected}" |
| 1924 | + ) |
| 1925 | + return fingerprint |
| 1926 | + |
| 1927 | + |
1856 | 1928 | # G10: in-process LRU cache of opt.state by run_id. Used for warm-start |
1857 | 1929 | # across sequential Train clicks in the same backend session. |
1858 | 1930 | _RUN_CACHE: dict[str, Any] = {} |
|
0 commit comments