Skip to content

Commit b9b043e

Browse files
committed
feat(v8-r09): HF Hub quickstart — stream → tokenize → parquet
Closes cppmega-mlx-yz6n.9. New module scripts/data/hf_quickstart.py streams a HF Hub dataset via datasets.load_dataset(streaming=True), tokenizes each document with cppmega tokenizer, emits a parquet shard with the canonical 4-column schema (token_ids/doc_ids/byte_offsets/ byte_lengths). Pure-Python in-memory shim hf_quickstart_from_iterable keeps tests offline. New RPC: data.hf_quickstart(dataset_id, split, tokenizer, n_tokens, job_id, ...) → {parquet_path, n_tokens_written, n_docs_seen, elapsed_ms}. Network can be disabled via VBGUI_DISABLE_NETWORK=1 env. New WS: /ws/data/{job_id} streams start/progress/done frames from the new data_event_bus (same publish/subscribe pattern as train/gen/verify). UI: HFQuickStartModal — opens from DataInspector header "HF quickstart" button, runs the RPC, surfaces the result parquet path back to the inspector's path field. Tests: 6 new pytest (parquet schema, event bus, RPC env-block, dispatch envelope), 3 new vitest (modal mount, Run+result, error banner). Regression: 950 pytest + 466 vitest green.
1 parent a540ea2 commit b9b043e

10 files changed

Lines changed: 736 additions & 0 deletions

File tree

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
AutoFitParams,
8484
auto_fit,
8585
)
86+
from cppmega_v4.jsonrpc.hf_quickstart_method import (
87+
HfQuickstartParams,
88+
hf_quickstart_method,
89+
)
8690
from cppmega_v4.jsonrpc.tokenizer_roundtrip_text_method import (
8791
TokenizerRoundtripTextParams,
8892
roundtrip_text,
@@ -145,6 +149,10 @@
145149
AutoFitParams,
146150
lambda p, c: auto_fit(p, cache=c),
147151
),
152+
"data.hf_quickstart": (
153+
HfQuickstartParams,
154+
lambda p, c: hf_quickstart_method(p, cache=c),
155+
),
148156
"probe.run": (
149157
ProbeRunParams,
150158
lambda p, c: probe_run(p, cache=c),
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""V8-R09: ``data.hf_quickstart`` RPC handler.
2+
3+
Wraps :func:`scripts.data.hf_quickstart.hf_quickstart` for the GUI.
4+
The handler runs synchronously — progress arrives on the
5+
``/ws/data/{job_id}`` WebSocket from the underlying data_event_bus.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
from pydantic import BaseModel, ConfigDict
12+
13+
from cppmega_v4.jsonrpc.cache import LRUCache
14+
from cppmega_v4.jsonrpc.methods import _cache_lookup, _cache_store
15+
16+
17+
__all__ = [
18+
"HfQuickstartParams",
19+
"HfQuickstartResult",
20+
"hf_quickstart_method",
21+
]
22+
23+
24+
class HfQuickstartParams(BaseModel):
25+
"""Input — HF dataset + tokenizer + token budget."""
26+
27+
model_config = ConfigDict(extra="forbid")
28+
29+
dataset_id: str
30+
split: str = "train"
31+
tokenizer: str = "cppmega_v3"
32+
n_tokens: int = 100_000
33+
job_id: str | None = None
34+
out_dir: str | None = None
35+
text_field: str = "text"
36+
37+
38+
class HfQuickstartResult(BaseModel):
39+
"""Output — parquet path + token / doc counters + elapsed wall."""
40+
41+
model_config = ConfigDict(extra="forbid")
42+
43+
parquet_path: str
44+
n_tokens_written: int
45+
n_docs_seen: int
46+
elapsed_ms: float
47+
48+
49+
def hf_quickstart_method(
50+
params: HfQuickstartParams, *, cache: LRUCache | None = None,
51+
) -> HfQuickstartResult:
52+
"""Run the HF quickstart and return the resulting parquet shard."""
53+
# Cache only on full param-key equality; HF jobs aren't typically
54+
# repeated with identical params so the hit rate is near-zero.
55+
key, hit = _cache_lookup(cache, "data.hf_quickstart", params)
56+
if hit is not None:
57+
return hit
58+
59+
if os.environ.get("VBGUI_DISABLE_NETWORK") == "1":
60+
raise RuntimeError(
61+
"HF Hub network access disabled via VBGUI_DISABLE_NETWORK")
62+
63+
from scripts.data.hf_quickstart import hf_quickstart as _hf
64+
r = _hf(
65+
dataset_id=params.dataset_id,
66+
split=params.split,
67+
tokenizer=params.tokenizer,
68+
n_tokens=params.n_tokens,
69+
job_id=params.job_id,
70+
out_dir=params.out_dir,
71+
text_field=params.text_field,
72+
)
73+
out = HfQuickstartResult(
74+
parquet_path=r.parquet_path,
75+
n_tokens_written=r.n_tokens_written,
76+
n_docs_seen=r.n_docs_seen,
77+
elapsed_ms=r.elapsed_ms,
78+
)
79+
_cache_store(cache, key, out)
80+
return out

cppmega_v4/jsonrpc/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ class PipelineRunResult(BaseModel):
810810
"architectures.scale_down",
811811
"memory.matrix",
812812
"architectures.auto_fit",
813+
"data.hf_quickstart",
813814
})
814815

815816

cppmega_v4/jsonrpc/server.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,43 @@ def _try_get(timeout: float = 0.2):
171171
finally:
172172
train_event_bus.unsubscribe(run_id, q)
173173

174+
@app.websocket("/ws/data/{job_id}")
175+
async def ws_data(socket: WebSocket, job_id: str) -> None:
176+
"""V8-R09: live data-job progress stream.
177+
178+
The UI opens this WS *before* invoking data.hf_quickstart or
179+
data.github_corpus with the same ``job_id``. Producer publishes
180+
{phase: "start"|"progress"|"done", ...} dicts onto
181+
``data_event_bus``; the None sentinel ends the stream with a
182+
final {finish:"ok"} frame."""
183+
from cppmega_v4.runtime import data_event_bus
184+
import queue as _queue
185+
await socket.accept()
186+
q = data_event_bus.subscribe(job_id)
187+
188+
def _try_get(timeout: float = 0.2):
189+
try:
190+
return q.get(timeout=timeout)
191+
except _queue.Empty:
192+
return _SENTINEL_EMPTY
193+
194+
try:
195+
while True:
196+
ev = await asyncio.to_thread(_try_get, 0.2)
197+
if ev is _SENTINEL_EMPTY:
198+
await asyncio.sleep(0)
199+
continue
200+
if ev is None:
201+
await socket.send_json({"finish": "ok",
202+
"job_id": job_id})
203+
return
204+
await socket.send_json({"job_id": job_id,
205+
"event": ev})
206+
except WebSocketDisconnect:
207+
pass
208+
finally:
209+
data_event_bus.unsubscribe(job_id, q)
210+
174211
@app.websocket("/ws/verify/{spec_hash}")
175212
async def ws_verify(socket: WebSocket, spec_hash: str) -> None:
176213
"""V7-L45: live verify progress stream.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""V8-R09: per-job pub/sub bus for HF / GitHub data quickstart jobs.
2+
3+
Pattern identical to ``train_event_bus`` — see that module's docstring
4+
for the protocol. Subscribers drain until they see ``None`` (the
5+
completion sentinel emitted by the producer when the job finishes).
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import queue
11+
import threading
12+
from typing import Any
13+
14+
15+
_LOCK = threading.Lock()
16+
_QUEUES: dict[str, list[queue.Queue]] = {}
17+
18+
19+
def publish(job_id: str | None, event: dict[str, Any] | None) -> None:
20+
if not job_id:
21+
return
22+
with _LOCK:
23+
subs = list(_QUEUES.get(job_id, []))
24+
for q in subs:
25+
try:
26+
q.put_nowait(event)
27+
except Exception:
28+
pass
29+
30+
31+
def subscribe(job_id: str) -> queue.Queue:
32+
q: queue.Queue = queue.Queue(maxsize=1024)
33+
with _LOCK:
34+
_QUEUES.setdefault(job_id, []).append(q)
35+
return q
36+
37+
38+
def unsubscribe(job_id: str, q: queue.Queue) -> None:
39+
with _LOCK:
40+
subs = _QUEUES.get(job_id, [])
41+
if q in subs:
42+
subs.remove(q)
43+
if not subs:
44+
_QUEUES.pop(job_id, None)
45+
46+
47+
def reset() -> None:
48+
with _LOCK:
49+
_QUEUES.clear()
50+
51+
52+
def subscriber_count(job_id: str) -> int:
53+
with _LOCK:
54+
return len(_QUEUES.get(job_id, []))
55+
56+
57+
__all__ = ["publish", "subscribe", "unsubscribe", "reset",
58+
"subscriber_count"]

0 commit comments

Comments
 (0)