Summary
Upgrade the drift analysis from Word2Vec/TWEC (300-d, distributional) to contextual drift: run the historical FineWeb corpus (2014–2025) through a frozen transformer encoder and measure how each word's contextual sense shifts over time (e.g. zoom lens→video-call, covid/mask/remote post-2020, delve becoming LLM-ese). This captures polysemy/sense change that Word2Vec cannot.
This issue supersedes the Antigravity-drafted ticket DATA-402 ("DGX-Spark cluster + PySpark"), which assumed infrastructure that does not exist. We run on a single DGX Spark, so the design is a single-process PyTorch pipeline with on-the-fly aggregation.
Verified environment (we run ON the target box)
aarch64, NVIDIA GB10, 128 GB unified CPU+GPU memory, ~273 GB/s mem bandwidth, ~1 PFLOP FP4.
torch 2.12.0+cu130 (CUDA OK), transformers 5.10.2.
- 519 GB free of a 916 GB volume.
- Shared vocab
data/vocab/vocab.json = 119,466 words (<UNK>=0).
- No Spark / HDFS / cluster. Everything in-process.
Why we discard the original ticket's design
The ticket frets about 36 TB of token vectors and 2 TB of shuffle space and specs a 3-node / 24×A100 cluster with PySpark mapPartitions/reduceByKey. All of that is an artifact of a distributed design we are not using. With on-the-fly aggregation (never store per-token vectors) on one device:
- Total disk footprint is ~20 GB (a non-issue at 519 GB free).
- The real budget is GPU-hours, set by tokens/year.
Decisions (confirmed with user)
- Encoder:
bert-base-uncased (768-d). Fast WordPiece tokenizer with clean word_ids() alignment, already lowercase (matches the existing vocab), SDPA attention. Frozen — AutoModel, take last_hidden_state.
- Corpus: full 1 B tokens/year × 12 = 12 B tokens (max coverage; multi-day run → throughput probe up front + solid checkpointing).
- Scope this round: data pipeline + local eval only. Web packing/frontend deferred to a follow-up.
Correctness — where the ticket's snippet was wrong
| Ticket approach |
What we do instead |
decode each token id, strip "##" |
word_ids() from the fast tokenizer maps subwords→words (works for WordPiece/BPE/SP; ## does not). First-subword pooling for v1 ("mean" deferred knob). |
python dict[str, np.ndarray] per token |
Vectorized GPU index_add_ into preallocated sum[V,768], sumsq[V,768], count[V], keyed by the shared vocab id. Accumulate in fp32 even though the forward runs bf16. |
| raw contextual cosine |
Per-year mean-centering before cosine — BERT vectors are strongly anisotropic (random words sit at cos ≈ 0.3–0.6, which would swamp the drift signal). Optional all-but-top-k PCA removal as a knob. |
| Procrustes / TWEC compass per year |
None. A frozen shared encoder puts every year in one coordinate system already → drift(w,y) = 1 − cos(c̄_{w,y}, c̄_{w,2018}) on centered centroids. |
| store/segment true sentences |
Non-overlapping ~128-word windows (v1, no extra dependency). |
Reuse the existing shared vocab to index the accumulator → result is row-comparable to the Word2Vec word set (same words, different basis). Nothing under models/embeddings*, models/aligned/*, or existing drift artifacts is touched.
Architecture / data flow
re-stream FineWeb RAW text (per snapshot) ── reuses snapshot_registry + the
filter language_score >= 0.65 streaming/checkpoint contract of
| pipeline/data_pipeline.py
v lowercase + whitespace split, slice into 128-word windows
GPU forward (bf16, SDPA, inference_mode) -> last_hidden_state [B,L,768]
| word_ids() -> first-subword positions -> target vocab-id tensor [B,L] (-1 = ignore)
v
ContextualAccumulator sum[V,768] += h ; sumsq[V,768] += h*h ; count[V] += 1 (index_add_)
| checkpoint per completed snapshot (resumable)
v per year
finalize: centroid = sum/count ; dispersion = sumsq/count - centroid^2
per-year mean-center (anisotropy fix) -> models/contextual/{year}.npy (+ _centered, _count, _dispersion)
v
drift vs 2018 (reuse analysis/drift.py) -> models/contextual/drift_vs_2018.parquet + summary
v
local eval: noise-floor / SNR vs the Word2Vec drift; sanity words
Key reuse: mirror the streaming + checkpoint contract of pipeline/data_pipeline.py (but read raw row["text"] — the existing pipeline lowercases/strips and discards raw text, so we must re-stream); pipeline/snapshot_registry.py::get_snapshots; analysis/drift.py (compute_drift_from_base, compute_drift_summary); the --smoke/--device entry-point style of scripts/twec_full.py.
New files
pipeline/contextual_model.py — load frozen model + fast tokenizer (attn_implementation="sdpa", .eval().requires_grad_(False)); encode_windows(...) with is_split_into_words=True so word_ids() is clean; bf16 inference_mode forward. Width from model.config.hidden_size (never hardcode 768).
pipeline/contextual_aggregate.py — ContextualAccumulator: GPU sum/sumsq/count; add_batch(...) builds the target-id tensor (ignore special/continuation/OOV/stopword/len<MIN_WORD_LENGTH/digit) + three index_add_s; save_state/load_state (npz), to_numpy.
pipeline/contextual_stream.py — iter_windows(year, tokenizer): re-stream FineWeb per snapshot (same load_dataset(..., streaming=True) + language_score>=0.65 filter), raw text, lowercase + 128-word windows, stop per snapshot at target_tokens // len(snapshots). run_year(...) batches → forward → accumulator.add_batch, checkpointing each completed snapshot.
pipeline/contextual_finalize.py — centroids, dispersion (on raw vectors — centering doesn't change within-word variance), per-year mean-centering (+optional PCA), write models/contextual/{year}*.npy.
analysis/contextual_drift.py — load centered centroids into dict[int,np.ndarray], call analysis/drift.py::compute_drift_from_base(base_year=ANCHOR_YEAR) + compute_drift_summary, gate on a per-(word,year) min-count valid_mask; emit parquet + summary with a dispersion column.
scripts/contextual_benchmark.py — run FIRST. Pull ~2k windows from one 2018 snapshot (no disk write), warm up, time batches, sweep batch ∈ {128,256,512}; print windows/sec, tokens/sec, GPU mem, and an hours-per-year + total-wall-clock table to confirm the 12 B-token ETA and pick batch size.
scripts/contextual_drift.py — entry point mirroring scripts/twec_full.py (sys.path prelude, from config import ...): --smoke, --device, --model, --years, --stage {stream,finalize,drift,all}. Resumable (skip finalized {year}.npy, resume partials). --stage stream runs overnight in background; finalize/drift after.
config.py additions (append only — import, never hardcode)
# --- Contextual drift pipeline ---
CONTEXTUAL_MODEL = "bert-base-uncased"
CONTEXTUAL_LAYER = -1 # last_hidden_state
CONTEXTUAL_POOLING = "first" # "first" | "mean" (mean deferred)
CONTEXTUAL_MAX_LEN = 128 # non-overlapping window length
CONTEXTUAL_BATCH_SIZE = 256 # tune from benchmark
CONTEXTUAL_TARGET_TOKENS_PER_YEAR = TARGET_TOKENS_PER_YEAR # 1B (full)
CONTEXTUAL_MIN_COUNT = 50 # min obs for a trusted (word,year) centroid
CONTEXTUAL_CENTERING = True # per-year mean-centering (anisotropy fix)
CONTEXTUAL_PCA_REMOVE_K = 0 # all-but-top-k; 0 = off
CONTEXTUAL_USE_STOPWORDS = True
CONTEXTUAL_STOPWORDS = frozenset({...}) # small English list
CONTEXTUAL_DIR = MODELS_DIR / "contextual"
CONTEXTUAL_STATE_DIR = DATA_DIR / "contextual_state"
Checkpoint format (resumable multi-day run)
Per year in data/contextual_state/:
{year}.partial.npz — sum(V×768 f32), sumsq(V×768 f32), count(V f32) ≈ 0.73 GB/yr. Atomic write (.tmp + os.replace).
{year}_checkpoint.json — {completed_snapshots, total_tokens, total_windows, model, hidden_dim, pooling, max_len, config_hash, updated_at}. Resume loads the npz and skips completed snapshots only if model/config_hash match (mismatch → refuse). On year completion, finalize then delete partial + checkpoint (mirrors data_pipeline.py).
Budget
- Disk: corpus streamed (writes nothing); 12 partials ≈ 8.8 GB + final artifacts ≈ 8.8 GB → < 20 GB of 519 GB free. Not a constraint.
- Compute: the binding cost. Benchmark sets the real number; full 12 B tokens through bert-base is expected to be a multi-day, checkpointed run. 128 GB unified memory is ample (model <1 GB, accumulators ~0.73 GB/yr, activations a few GB).
Verification
- Smoke (
--smoke, 1 snapshot for {2014, 2018}, ~2 M tokens → models/contextual_smoke/): counts>0 for common words; {year}.npy shape (119466, 768); checkpoint deletes on completion; kill mid-run + relaunch → completed snapshot skipped, counts continue.
- Throughput probe → confirm ETA, set batch size (reconsider 1 B target if untenable).
- Anisotropy: random word-pair mean cosine high on raw, ≈ 0 after mean-centering (else enable
PCA_REMOVE_K=1..3).
- Stable words:
cos(centered("house",2014), centered("house",2025)) high / low drift; same for table, water.
- Drifters:
covid/zoom/mask/remote spike 2020+, delve 2023+; rank by total drift, confirm near top while function words sit low.
- Dispersion: polysemous (
bank, apple, python) > monosemous; invariant to centering.
- Quality vs Word2Vec: noise-floor / SNR vs shipped TWEC drift; confirm contextual signal adds info on polysemy-driven cases.
Build order
- config additions
pipeline/contextual_model.py
scripts/contextual_benchmark.py (run, set batch/ETA)
pipeline/contextual_aggregate.py
pipeline/contextual_stream.py
scripts/contextual_drift.py --smoke (verify resume)
pipeline/contextual_finalize.py
analysis/contextual_drift.py + artifacts
- full run (background, checkpointed)
- eval (Verification section)
Out of scope (follow-up phase)
Web packing (scripts/pack_web_data.py 14400-byte stride / vecs.bin), the web/lib/* 300-d hardcodes, and a /contextual-drift view. Contextual artifacts are 768-d; the follow-up parametrizes the dim and publishes a new data/vN alongside the existing TWEC data rather than replacing it.
Origin: Antigravity drafts linear_ticket_dgx_spark_drift_pipeline.md / dgx_spark_drift_pipeline.md. Planned with Claude Code; full plan also at ~/.claude/plans/cozy-greeting-music.md.
Summary
Upgrade the drift analysis from Word2Vec/TWEC (300-d, distributional) to contextual drift: run the historical FineWeb corpus (2014–2025) through a frozen transformer encoder and measure how each word's contextual sense shifts over time (e.g.
zoomlens→video-call,covid/mask/remotepost-2020,delvebecoming LLM-ese). This captures polysemy/sense change that Word2Vec cannot.This issue supersedes the Antigravity-drafted ticket
DATA-402("DGX-Spark cluster + PySpark"), which assumed infrastructure that does not exist. We run on a single DGX Spark, so the design is a single-process PyTorch pipeline with on-the-fly aggregation.Verified environment (we run ON the target box)
aarch64, NVIDIA GB10, 128 GB unified CPU+GPU memory, ~273 GB/s mem bandwidth, ~1 PFLOP FP4.torch 2.12.0+cu130(CUDA OK),transformers 5.10.2.data/vocab/vocab.json= 119,466 words (<UNK>=0).Why we discard the original ticket's design
The ticket frets about 36 TB of token vectors and 2 TB of shuffle space and specs a 3-node / 24×A100 cluster with PySpark
mapPartitions/reduceByKey. All of that is an artifact of a distributed design we are not using. With on-the-fly aggregation (never store per-token vectors) on one device:Decisions (confirmed with user)
bert-base-uncased(768-d). Fast WordPiece tokenizer with cleanword_ids()alignment, already lowercase (matches the existing vocab), SDPA attention. Frozen —AutoModel, takelast_hidden_state.Correctness — where the ticket's snippet was wrong
"##"word_ids()from the fast tokenizer maps subwords→words (works for WordPiece/BPE/SP;##does not). First-subword pooling for v1 ("mean"deferred knob).dict[str, np.ndarray]per tokenindex_add_into preallocatedsum[V,768],sumsq[V,768],count[V], keyed by the shared vocab id. Accumulate in fp32 even though the forward runs bf16.drift(w,y) = 1 − cos(c̄_{w,y}, c̄_{w,2018})on centered centroids.Reuse the existing shared vocab to index the accumulator → result is row-comparable to the Word2Vec word set (same words, different basis). Nothing under
models/embeddings*,models/aligned/*, or existing drift artifacts is touched.Architecture / data flow
Key reuse: mirror the streaming + checkpoint contract of
pipeline/data_pipeline.py(but read rawrow["text"]— the existing pipeline lowercases/strips and discards raw text, so we must re-stream);pipeline/snapshot_registry.py::get_snapshots;analysis/drift.py(compute_drift_from_base,compute_drift_summary); the--smoke/--deviceentry-point style ofscripts/twec_full.py.New files
pipeline/contextual_model.py— load frozen model + fast tokenizer (attn_implementation="sdpa",.eval().requires_grad_(False));encode_windows(...)withis_split_into_words=Truesoword_ids()is clean; bf16inference_modeforward. Width frommodel.config.hidden_size(never hardcode 768).pipeline/contextual_aggregate.py—ContextualAccumulator: GPUsum/sumsq/count;add_batch(...)builds the target-id tensor (ignore special/continuation/OOV/stopword/len<MIN_WORD_LENGTH/digit) + threeindex_add_s;save_state/load_state(npz),to_numpy.pipeline/contextual_stream.py—iter_windows(year, tokenizer): re-stream FineWeb per snapshot (sameload_dataset(..., streaming=True)+language_score>=0.65filter), raw text, lowercase + 128-word windows, stop per snapshot attarget_tokens // len(snapshots).run_year(...)batches → forward →accumulator.add_batch, checkpointing each completed snapshot.pipeline/contextual_finalize.py— centroids, dispersion (on raw vectors — centering doesn't change within-word variance), per-year mean-centering (+optional PCA), writemodels/contextual/{year}*.npy.analysis/contextual_drift.py— load centered centroids intodict[int,np.ndarray], callanalysis/drift.py::compute_drift_from_base(base_year=ANCHOR_YEAR)+compute_drift_summary, gate on a per-(word,year) min-countvalid_mask; emit parquet + summary with a dispersion column.scripts/contextual_benchmark.py— run FIRST. Pull ~2k windows from one 2018 snapshot (no disk write), warm up, time batches, sweep batch ∈ {128,256,512}; print windows/sec, tokens/sec, GPU mem, and an hours-per-year + total-wall-clock table to confirm the 12 B-token ETA and pick batch size.scripts/contextual_drift.py— entry point mirroringscripts/twec_full.py(sys.path prelude,from config import ...):--smoke,--device,--model,--years,--stage {stream,finalize,drift,all}. Resumable (skip finalized{year}.npy, resume partials).--stage streamruns overnight in background;finalize/driftafter.config.py additions (append only — import, never hardcode)
Checkpoint format (resumable multi-day run)
Per year in
data/contextual_state/:{year}.partial.npz—sum(V×768 f32),sumsq(V×768 f32),count(V f32) ≈ 0.73 GB/yr. Atomic write (.tmp+os.replace).{year}_checkpoint.json—{completed_snapshots, total_tokens, total_windows, model, hidden_dim, pooling, max_len, config_hash, updated_at}. Resume loads the npz and skips completed snapshots only if model/config_hash match (mismatch → refuse). On year completion, finalize then delete partial + checkpoint (mirrorsdata_pipeline.py).Budget
Verification
--smoke, 1 snapshot for {2014, 2018}, ~2 M tokens →models/contextual_smoke/): counts>0 for common words;{year}.npyshape(119466, 768); checkpoint deletes on completion; kill mid-run + relaunch → completed snapshot skipped, counts continue.PCA_REMOVE_K=1..3).cos(centered("house",2014), centered("house",2025))high / low drift; same fortable,water.covid/zoom/mask/remotespike 2020+,delve2023+; rank by total drift, confirm near top while function words sit low.bank,apple,python) > monosemous; invariant to centering.Build order
pipeline/contextual_model.pyscripts/contextual_benchmark.py(run, set batch/ETA)pipeline/contextual_aggregate.pypipeline/contextual_stream.pyscripts/contextual_drift.py --smoke(verify resume)pipeline/contextual_finalize.pyanalysis/contextual_drift.py+ artifactsOut of scope (follow-up phase)
Web packing (
scripts/pack_web_data.py14400-byte stride /vecs.bin), theweb/lib/*300-d hardcodes, and a/contextual-driftview. Contextual artifacts are 768-d; the follow-up parametrizes the dim and publishes a newdata/vNalongside the existing TWEC data rather than replacing it.Origin: Antigravity drafts
linear_ticket_dgx_spark_drift_pipeline.md/dgx_spark_drift_pipeline.md. Planned with Claude Code; full plan also at~/.claude/plans/cozy-greeting-music.md.