Marimo Diffusion 0.6B: checkpoint, sampler, OpenAI server, ledger-needle bench
Browse files- .gitattributes +1 -0
- README.md +155 -0
- bench/bench_ledger_needle.py +602 -0
- bench/ledger-needle-compare.html +91 -0
- bench/ledger-needle-ours.html +0 -0
- bench/results-ledger-ours.jsonl +0 -0
- bench/results-qwen25-05b-full.jsonl +100 -0
- bench/results-qwen25-15b-full.jsonl +100 -0
- bench/results-qwen3-06b-nothink-budget512.jsonl +100 -0
- bench/results-qwen3-06b-nothink-full.jsonl +100 -0
- bench/results-qwen3-06b-thinking-full.jsonl +100 -0
- bench/results-smollm2-360m-full.jsonl +100 -0
- bench/results-tinyllama-11b-full.jsonl +100 -0
- marimo-diffusion-0.6b.pt +3 -0
- src/diffusion_lm/__init__.py +26 -0
- src/diffusion_lm/chat_server.py +251 -0
- src/diffusion_lm/chatcorpus.py +233 -0
- src/diffusion_lm/claims.py +512 -0
- src/diffusion_lm/config.py +174 -0
- src/diffusion_lm/corpus.py +920 -0
- src/diffusion_lm/data.py +296 -0
- src/diffusion_lm/diffusion.py +338 -0
- src/diffusion_lm/distill.py +328 -0
- src/diffusion_lm/flexattn.py +147 -0
- src/diffusion_lm/hf_bridge.py +270 -0
- src/diffusion_lm/hybrid.py +1139 -0
- src/diffusion_lm/model.py +250 -0
- src/diffusion_lm/playground.py +541 -0
- src/diffusion_lm/quality.py +69 -0
- src/diffusion_lm/reasoning.py +578 -0
- src/diffusion_lm/reasoning_playground.py +1226 -0
- src/diffusion_lm/reasoning_train.py +600 -0
- src/diffusion_lm/sample.py +153 -0
- src/diffusion_lm/stories.py +264 -0
- src/diffusion_lm/tokenizer.py +357 -0
- src/diffusion_lm/train.py +666 -0
- tokenizer-qwen3-adaptive.json +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer-qwen3-adaptive.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: Qwen/Qwen3-0.6B
|
| 4 |
+
pipeline_tag: text-generation
|
| 5 |
+
language:
|
| 6 |
+
- en
|
| 7 |
+
tags:
|
| 8 |
+
- diffusion
|
| 9 |
+
- block-diffusion
|
| 10 |
+
- masked-diffusion
|
| 11 |
+
- chat
|
| 12 |
+
- memory
|
| 13 |
+
- qwen3
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Marimo Diffusion 0.6B
|
| 17 |
+
|
| 18 |
+
A 0.6B chat model that **thinks in denoised blocks and remembers through its own notes**
|
| 19 |
+
instead of re-reading the conversation. It is a retrofit of
|
| 20 |
+
[Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B): continued pretraining converts the
|
| 21 |
+
autoregressive base into a hybrid block-diffusion model, and a supervised fine-tune teaches it
|
| 22 |
+
a note-taking chat format.
|
| 23 |
+
|
| 24 |
+
Three things make it different from a standard chat model of this size:
|
| 25 |
+
|
| 26 |
+
- **Adaptive thinking blocks.** Before answering, the model may open thinking blocks of 32/64/128
|
| 27 |
+
tokens, chosen per thought via control tokens (`<szN>`), each denoised bidirectionally in 16
|
| 28 |
+
steps. Trivial turns skip thinking entirely — that decision is trained, not prompted.
|
| 29 |
+
- **Ledger memory.** Only the last 4 messages are kept verbatim in the prefix. Everything older
|
| 30 |
+
survives only as the model's own notes (`key: value`), merged so the latest value wins. A
|
| 31 |
+
100-turn conversation fits in a ~500-token prefix that never grows.
|
| 32 |
+
- **Constant cost per turn.** ~2 s/turn on an RTX 3090 regardless of conversation length,
|
| 33 |
+
because the prefix is constant by construction.
|
| 34 |
+
|
| 35 |
+
## ⚠️ This model does NOT run under standard runtimes
|
| 36 |
+
|
| 37 |
+
The weights are Qwen3 architecture, but generation requires the block-denoising sampler included
|
| 38 |
+
in this repository. **transformers `generate()`, llama.cpp, GGUF, Ollama and LM Studio will not
|
| 39 |
+
produce correct output** — their autoregressive decoding never matches the training objective.
|
| 40 |
+
Use the bundled code.
|
| 41 |
+
|
| 42 |
+
## Quickstart
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
git clone https://huggingface.co/goldenfox/marimo-diffusion
|
| 46 |
+
cd marimo-diffusion
|
| 47 |
+
pip install torch tokenizers numpy
|
| 48 |
+
|
| 49 |
+
# OpenAI-compatible server (any OpenAI-API chat client can connect)
|
| 50 |
+
PYTHONPATH=src python -m diffusion_lm.chat_server \
|
| 51 |
+
--checkpoint marimo-diffusion-0.6b.pt \
|
| 52 |
+
--tokenizer tokenizer-qwen3-adaptive.json \
|
| 53 |
+
--port 7998
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Then point any OpenAI-compatible client (Chatbox, Open WebUI, curl) at
|
| 57 |
+
`http://127.0.0.1:7998/v1` with model id `marimo-diffusion-0.6b`:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
curl http://127.0.0.1:7998/v1/chat/completions -H 'Content-Type: application/json' -d '{
|
| 61 |
+
"model": "marimo-diffusion-0.6b",
|
| 62 |
+
"messages": [{"role": "user", "content": "my sister lands friday 6pm, flight AR1420"}]
|
| 63 |
+
}'
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
The response carries the model's notes in `reasoning_content` (same field DeepSeek uses), so
|
| 67 |
+
clients that render reasoning show them automatically. The server caches each turn's notes and
|
| 68 |
+
rebuilds the ledger across stateless requests.
|
| 69 |
+
|
| 70 |
+
For the interactive playground (streaming denoise view, per-turn data log):
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
pip install gradio
|
| 74 |
+
PYTHONPATH=src python -m diffusion_lm.reasoning_playground \
|
| 75 |
+
--outputs-dir . --prefix marimo \
|
| 76 |
+
--tokenizer tokenizer-qwen3-adaptive.json --port 7999
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
A CUDA GPU is recommended (any 6 GB+ card fits the bf16 weights). GPU memory: ~2.5 GB.
|
| 80 |
+
|
| 81 |
+
## Benchmark: ledger needle (memory across 100 turns)
|
| 82 |
+
|
| 83 |
+
The long-context needle test, adapted to what this architecture claims: a scripted 100-turn
|
| 84 |
+
conversation plants 15 facts, corrects 5 of them, and probes recall at distances of 3–96 turns.
|
| 85 |
+
Baselines get every reasonable advantage: full history in context, greedy decoding, an explicit
|
| 86 |
+
memory instruction, and native thinking mode where it exists. Identical scoring for all systems.
|
| 87 |
+
Full per-turn data and the interactive viewers are in [`bench/`](./bench).
|
| 88 |
+
|
| 89 |
+
| system | params | recall | test total (s) | s/turn | max prefix (tok) |
|
| 90 |
+
|---|---|---|---|---|---|
|
| 91 |
+
| Qwen2.5-1.5B-Instruct · full history | 1.5B | **9/10** | 80 | 0.8 | 2,881 |
|
| 92 |
+
| Qwen3-0.6B + thinking · full history | 0.6B | 7/10 | 1,320 | 13.2 | 4,450 |
|
| 93 |
+
| **Marimo Diffusion (ledger)** | **0.6B** | **6/10** | **220** | **2.2** | **625** |
|
| 94 |
+
| Qwen2.5-0.5B-Instruct · full history | 0.5B | 4/10 | 110 | 1.1 | 3,908 |
|
| 95 |
+
| SmolLM2-360M-Instruct · full history | 0.36B | 4/10 | 60 | 0.6 | 2,712 |
|
| 96 |
+
| TinyLlama-1.1B-Chat · full history | 1.1B | 3/10 | 120 | 1.2 | 5,126 |
|
| 97 |
+
| Qwen3-0.6B no thinking · full history | 0.6B | 1/10 | 130 | 1.3 | 3,722 |
|
| 98 |
+
| Qwen3-0.6B no thinking · 512-token budget | 0.6B | 1/10 | 160 | 1.6 | 512 |
|
| 99 |
+
|
| 100 |
+
Reading this honestly:
|
| 101 |
+
|
| 102 |
+
- **In its size class it leads**: every ≤0.6B baseline with the full transcript in context
|
| 103 |
+
scores 4/10 or less; the ledger reaches 6/10 from a 7× smaller prefix.
|
| 104 |
+
- **Beating it costs something**: 2.5× the parameters (Qwen2.5-1.5B), or the same base model's
|
| 105 |
+
thinking mode at **6× the latency** with an unbounded prefix — and 7 vs 6 on ten probes is
|
| 106 |
+
within noise.
|
| 107 |
+
- **Failure profiles are complementary.** The thinking baseline re-reads verbatim, so it never
|
| 108 |
+
suffers a corrupted note; the ledger never suffers long-context attention loss (it recalled
|
| 109 |
+
facts at distance 59–60 that the thinking baseline missed with the text in front of it).
|
| 110 |
+
- **Caveats**: one seed, ten probes; and the comparison measures the mechanism *and* its
|
| 111 |
+
training together — this model was trained on this conversational register, the baselines
|
| 112 |
+
were not.
|
| 113 |
+
|
| 114 |
+
## Training
|
| 115 |
+
|
| 116 |
+
- Base: Qwen3-0.6B. Continued pretraining converts AR → hybrid block diffusion (answer region
|
| 117 |
+
stays autoregressive; thinking blocks are masked-denoised bidirectionally).
|
| 118 |
+
- SFT: 174k examples from ~38k conversations — synthetic memory-task dialogues (recap,
|
| 119 |
+
correction, distant-combination, each ending in a consolidating close), passage-grounded QA
|
| 120 |
+
with the source dataset's reference answer as an exact quality gate, an abstention slice, and
|
| 121 |
+
persona-grounded dialogues. Sequence length 512 (median example: 194 tokens); the checkpoint
|
| 122 |
+
is served at 2,048 (RoPE, no learned positions).
|
| 123 |
+
- `steps_per_block 16` is the measured optimum for this checkpoint: best numeric fidelity at
|
| 124 |
+
half the latency of 32; below 8 both prose and numbers degrade.
|
| 125 |
+
|
| 126 |
+
### Training data provenance
|
| 127 |
+
|
| 128 |
+
| source | role | license |
|
| 129 |
+
|---|---|---|
|
| 130 |
+
| synthetic dialogues (DeepSeek v4-flash generated, machine-audited) | chat + memory tasks | — |
|
| 131 |
+
| [stanfordnlp/coqa](https://huggingface.co/datasets/stanfordnlp/coqa) | multi-turn grounded QA | other (mixed provenance) |
|
| 132 |
+
| [rajpurkar/squad_v2](https://huggingface.co/datasets/rajpurkar/squad_v2) | abstention | cc-by-sa-4.0 |
|
| 133 |
+
| [dgslibisey/MuSiQue](https://huggingface.co/datasets/dgslibisey/MuSiQue) | multi-hop reasoning | undeclared on mirror |
|
| 134 |
+
| [ucinlp/drop](https://huggingface.co/datasets/ucinlp/drop) | arithmetic over passages | cc-by-sa-4.0 |
|
| 135 |
+
| [nayohan/multi_session_chat](https://huggingface.co/datasets/nayohan/multi_session_chat) | human-written persona facts | undeclared on mirror |
|
| 136 |
+
|
| 137 |
+
## Limitations
|
| 138 |
+
|
| 139 |
+
- **0.6B knowledge ceiling.** It confabulates on open-domain facts like any model this size;
|
| 140 |
+
the training includes an abstention slice ("the passage doesn't say") but it is not a fix.
|
| 141 |
+
- **Note-taking can corrupt compound values** (an alphanumeric like `harbor858` was once noted
|
| 142 |
+
as `8858` and then faithfully recalled wrong). What enters the ledger wrong stays wrong.
|
| 143 |
+
- **Ledger interference**: with 50+ accumulated entries, similar-typed values (several money
|
| 144 |
+
amounts) can cross-contaminate. Training saw ~24 entries max.
|
| 145 |
+
- **No code in training data.** Reasoning about pasted code runs on the base model's residual
|
| 146 |
+
ability.
|
| 147 |
+
- SFT never saw examples past 512 tokens; behaviour between 512 and 2,048 rides on the
|
| 148 |
+
continued pretraining.
|
| 149 |
+
- English only.
|
| 150 |
+
|
| 151 |
+
## License
|
| 152 |
+
|
| 153 |
+
Apache 2.0, inheriting the Qwen3-0.6B base license. Training data licenses are listed above;
|
| 154 |
+
CoQA carries mixed-provenance terms and two mirrors declare no license — review them if you
|
| 155 |
+
redistribute derived data.
|
bench/bench_ledger_needle.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ledger needle bench: fact recall across a conversation far longer than the context.
|
| 2 |
+
|
| 3 |
+
The long-context analogue for this architecture. A needle-in-a-haystack test measures whether a
|
| 4 |
+
fact survives distance inside a huge prompt; here the prompt never exceeds the training window
|
| 5 |
+
(last ``keep`` messages) and facts survive only through the ledger of the model's own notes, so
|
| 6 |
+
the same question — does distance kill recall? — is asked of the memory mechanism instead of the
|
| 7 |
+
attention span.
|
| 8 |
+
|
| 9 |
+
``run`` drives a deterministic scripted conversation through the engine on the GPU box and
|
| 10 |
+
records everything per turn. ``render`` turns that record into a self-contained HTML viewer:
|
| 11 |
+
timeline, per-turn notes, the exact ledger state, and every probe scored.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import html as html_lib
|
| 18 |
+
import json
|
| 19 |
+
import random
|
| 20 |
+
import re
|
| 21 |
+
import time
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
FACT_POOL = [
|
| 25 |
+
('sister flight', 'flight {code}', 'my sister lands on {value}, write that down'),
|
| 26 |
+
('hotel room', 'room {num3}', 'we got {value} at the hotel'),
|
| 27 |
+
('locker code', 'code {num4}', 'the gym locker is {value}'),
|
| 28 |
+
('dentist slot', '{clock}', 'dentist moved me to {value}'),
|
| 29 |
+
('plumber quote', '${money}', 'the plumber quoted {value} for the bathroom'),
|
| 30 |
+
('car plate', '{plate}', 'the rental has plate {value}'),
|
| 31 |
+
('wifi password', '{word}{num3}', 'cabin wifi password is {value}'),
|
| 32 |
+
('train platform', 'platform {num2}', 'our train leaves from {value}'),
|
| 33 |
+
('order number', 'order {code}', 'the couch is {value}, keep it handy'),
|
| 34 |
+
('rent due', 'day {num2}', 'landlord wants rent by {value} each month'),
|
| 35 |
+
('kid teacher', 'ms. {name}', "tomas' new teacher is {value}"),
|
| 36 |
+
('parking spot', 'spot {letter}{num2}', 'we always park at {value}'),
|
| 37 |
+
('meds dose', '{num2}mg', 'doctor changed the dose to {value}'),
|
| 38 |
+
('deposit', '${money}', 'the deposit for the venue was {value}'),
|
| 39 |
+
('boarding gate', 'gate {letter}{num2}', 'boarding is at {value}'),
|
| 40 |
+
]
|
| 41 |
+
DISTRACTORS = [
|
| 42 |
+
'what a week, honestly', 'did you catch the game last night?', 'i love this weather lately',
|
| 43 |
+
'work was chaos today', 'thinking of making pasta tonight', 'my back hurts from the gym',
|
| 44 |
+
'the neighbor is renovating again, so loud', 'saw a great documentary yesterday',
|
| 45 |
+
'coffee here is getting expensive', 'might go for a walk later', 'the cat knocked over a plant',
|
| 46 |
+
'traffic was unreal this morning', 'finally finished that book', 'craving something sweet',
|
| 47 |
+
'they repaved our street', 'my phone battery dies so fast now',
|
| 48 |
+
]
|
| 49 |
+
PROBES = ['wait, what was the {key} again?', 'remind me of the {key}?',
|
| 50 |
+
'i forgot the {key}, what was it?', 'quick — the {key}?']
|
| 51 |
+
WORDS = ('maple', 'harbor', 'cactus', 'violet', 'ember', 'quartz')
|
| 52 |
+
NAMES = ('ferro', 'silva', 'duarte', 'campos', 'rojas', 'ibanez')
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _value(template: str, rng: random.Random) -> str:
|
| 56 |
+
return (template
|
| 57 |
+
.replace('{code}', f'{rng.choice("ABKQZ")}{rng.choice("RLMT")}{rng.randint(1000, 9999)}')
|
| 58 |
+
.replace('{num2}', str(rng.randint(10, 99)))
|
| 59 |
+
.replace('{num3}', str(rng.randint(100, 999)))
|
| 60 |
+
.replace('{num4}', str(rng.randint(1000, 9999)))
|
| 61 |
+
.replace('{clock}', f'{rng.randint(1, 12)}:{rng.choice(("15", "30", "45"))}pm')
|
| 62 |
+
.replace('{money}', f'{rng.randint(2, 90) * 100:,}')
|
| 63 |
+
.replace('{plate}', f'{"".join(rng.choice("BCDFGHJK") for _ in range(3))}-{rng.randint(100, 999)}')
|
| 64 |
+
.replace('{word}', rng.choice(WORDS))
|
| 65 |
+
.replace('{letter}', rng.choice('ABCDE'))
|
| 66 |
+
.replace('{name}', rng.choice(NAMES)))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_script(turns: int, seed: int) -> list[dict]:
|
| 70 |
+
"""Deterministic conversation plan: facts early and throughout, probes at all distances."""
|
| 71 |
+
|
| 72 |
+
rng = random.Random(seed)
|
| 73 |
+
facts = []
|
| 74 |
+
for key, template, phrasing in rng.sample(FACT_POOL, len(FACT_POOL)):
|
| 75 |
+
facts.append({'key': key, 'value': _value(template, rng), 'phrasing': phrasing})
|
| 76 |
+
|
| 77 |
+
plan: list[dict] = []
|
| 78 |
+
stated: dict[str, dict] = {}
|
| 79 |
+
fact_iter = iter(facts)
|
| 80 |
+
for index in range(turns):
|
| 81 |
+
remaining = turns - index
|
| 82 |
+
can_probe = [f for f in stated.values() if index - f['turn'] >= 3]
|
| 83 |
+
if index >= turns - 3 and can_probe:
|
| 84 |
+
kind = 'probe'
|
| 85 |
+
elif index % 7 in (0, 3) and (fact := next(fact_iter, None)) is not None:
|
| 86 |
+
plan.append({'kind': 'fact', **fact})
|
| 87 |
+
stated[fact['key']] = {**fact, 'turn': index}
|
| 88 |
+
continue
|
| 89 |
+
elif index % 11 == 5 and can_probe:
|
| 90 |
+
kind = 'probe'
|
| 91 |
+
elif index % 13 == 8 and stated and remaining > 5:
|
| 92 |
+
kind = 'correction'
|
| 93 |
+
else:
|
| 94 |
+
kind = 'distractor'
|
| 95 |
+
|
| 96 |
+
if kind == 'probe':
|
| 97 |
+
target = rng.choice(can_probe)
|
| 98 |
+
plan.append({'kind': 'probe', 'key': target['key'], 'value': target['value'],
|
| 99 |
+
'distance': index - target['turn'],
|
| 100 |
+
'text': rng.choice(PROBES).format(key=target['key'])})
|
| 101 |
+
elif kind == 'correction':
|
| 102 |
+
target = rng.choice(list(stated.values()))
|
| 103 |
+
new = _value(next(t for k, t, _ in FACT_POOL if k == target['key']), rng)
|
| 104 |
+
plan.append({'kind': 'correction', 'key': target['key'], 'old': target['value'],
|
| 105 |
+
'value': new,
|
| 106 |
+
'text': f'actually scratch that, the {target["key"]} is {new} now'})
|
| 107 |
+
stated[target['key']] = {**target, 'value': new, 'turn': index}
|
| 108 |
+
else:
|
| 109 |
+
plan.append({'kind': 'distractor', 'text': rng.choice(DISTRACTORS)})
|
| 110 |
+
return plan
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _hit(value: str, answer: str) -> bool:
|
| 114 |
+
"""Whether the answer states the value, matched on its distinctive core.
|
| 115 |
+
|
| 116 |
+
Demanding the full phrase penalised correct answers on both systems ("the locker code is
|
| 117 |
+
4805" failed against 'code 4805'), so the core — the last token, which carries the
|
| 118 |
+
identifier — is what must appear. Values are generated with 2+ digit cores, so incidental
|
| 119 |
+
collisions stay unlikely.
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
canon = lambda t: re.sub(r'[^a-z0-9]', '', t.lower()) # noqa: E731
|
| 123 |
+
core = value.split()[-1]
|
| 124 |
+
return canon(core) in canon(answer)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def run(args: argparse.Namespace) -> None:
|
| 128 |
+
from diffusion_lm.claims import SYSTEM, chat_prefix, ledger_line, ledger_notes, merge_notes
|
| 129 |
+
from diffusion_lm.reasoning_playground import ReasoningEngine
|
| 130 |
+
from diffusion_lm.train import resolve_device
|
| 131 |
+
|
| 132 |
+
engine = ReasoningEngine(args.checkpoint, args.tokenizer, resolve_device('auto'))
|
| 133 |
+
plan = build_script(args.turns, args.seed)
|
| 134 |
+
messages: list[dict] = []
|
| 135 |
+
records = []
|
| 136 |
+
# Appended per turn and flushed, so a tail -f (or a crash) sees every completed turn.
|
| 137 |
+
sink = args.output.open('w', encoding='utf-8')
|
| 138 |
+
for index, turn in enumerate(plan):
|
| 139 |
+
user_text = turn.get('text') or turn['phrasing'].format(value=turn['value'])
|
| 140 |
+
messages.append({'role': 'user', 'content': user_text})
|
| 141 |
+
older = merge_notes(ledger_notes(messages, args.keep))
|
| 142 |
+
window = messages[max(0, len(messages) - args.keep):]
|
| 143 |
+
prefix = chat_prefix(
|
| 144 |
+
[{'role': m['role'], 'content': m['content']} for m in window],
|
| 145 |
+
system=SYSTEM, extra=ledger_line(older),
|
| 146 |
+
)
|
| 147 |
+
blocks: list[tuple[int, str]] = []
|
| 148 |
+
answer = ''
|
| 149 |
+
begin = time.perf_counter()
|
| 150 |
+
for _, answer, _ in engine.stream_chat(
|
| 151 |
+
prefix, temperature=0.7, steps_per_block=args.steps,
|
| 152 |
+
max_answer_tokens=120, seed=args.seed * 1000 + index, blocks_out=blocks,
|
| 153 |
+
):
|
| 154 |
+
pass
|
| 155 |
+
note = '; '.join(text for _, text in blocks if text)
|
| 156 |
+
messages.append({'role': 'assistant', 'content': answer.strip(), 'note': note})
|
| 157 |
+
record = {
|
| 158 |
+
'turn': index, 'kind': turn['kind'], 'user': user_text,
|
| 159 |
+
'notes': [text for _, text in blocks if text], 'ledger': older,
|
| 160 |
+
'prefix_tokens': len(engine.tokenizer.encode(prefix, add_special_tokens=False).ids),
|
| 161 |
+
'answer': answer.strip(), 'seconds': round(time.perf_counter() - begin, 2),
|
| 162 |
+
}
|
| 163 |
+
if turn['kind'] == 'probe':
|
| 164 |
+
record.update({'key': turn['key'], 'expected': turn['value'],
|
| 165 |
+
'distance': turn['distance'],
|
| 166 |
+
'hit': _hit(turn['value'], answer)})
|
| 167 |
+
if turn['kind'] in ('fact', 'correction'):
|
| 168 |
+
record.update({'key': turn['key'], 'value': turn['value']})
|
| 169 |
+
records.append(record)
|
| 170 |
+
sink.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 171 |
+
sink.flush()
|
| 172 |
+
flag = '' if turn['kind'] != 'probe' else (' HIT' if record['hit'] else ' MISS')
|
| 173 |
+
print(f'[{index + 1}/{len(plan)}] {turn["kind"]}{flag} {record["seconds"]}s '
|
| 174 |
+
f'ledger={len(older)}', flush=True)
|
| 175 |
+
print(f' U: {user_text}', flush=True)
|
| 176 |
+
if note:
|
| 177 |
+
print(f' T: {note}', flush=True)
|
| 178 |
+
print(f' A: {answer.strip()}', flush=True)
|
| 179 |
+
if turn['kind'] == 'probe':
|
| 180 |
+
print(f' >> esperado {turn["value"]} (distancia {turn["distance"]}) -> '
|
| 181 |
+
f'{"HIT" if record["hit"] else "MISS"}', flush=True)
|
| 182 |
+
|
| 183 |
+
sink.close()
|
| 184 |
+
probes = [r for r in records if r['kind'] == 'probe']
|
| 185 |
+
hits = sum(r['hit'] for r in probes)
|
| 186 |
+
print(f'\nrecall: {hits}/{len(probes)} ({hits / max(1, len(probes)):.0%})')
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def run_ar(args: argparse.Namespace) -> None:
|
| 190 |
+
"""Same scripted conversation through a plain AR baseline of the same size.
|
| 191 |
+
|
| 192 |
+
``--context full`` gives the baseline the whole history (its native memory);
|
| 193 |
+
``--context budget`` truncates the rendered prompt to the newest messages that fit the
|
| 194 |
+
same token budget the ledger system runs under, so both systems answer from equally many
|
| 195 |
+
prefix tokens and only the memory MECHANISM differs.
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
import torch
|
| 199 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 200 |
+
|
| 201 |
+
tokenizer = AutoTokenizer.from_pretrained(args.model_path)
|
| 202 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 203 |
+
args.model_path, torch_dtype=torch.bfloat16, device_map='cuda',
|
| 204 |
+
).eval()
|
| 205 |
+
plan = build_script(args.turns, args.seed)
|
| 206 |
+
# Deliberately generous: the baselines get an explicit memory instruction, greedy decoding,
|
| 207 |
+
# and (with --thinking) their native reasoning mode. The comparison should show the ledger
|
| 208 |
+
# beating the baselines at their best, or it shows nothing.
|
| 209 |
+
system = ('You are a meticulous assistant. Remember every concrete detail the user '
|
| 210 |
+
'mentions — codes, amounts, times, names, numbers. When asked to recall one, '
|
| 211 |
+
'answer with the exact value.')
|
| 212 |
+
messages: list[dict] = []
|
| 213 |
+
records = []
|
| 214 |
+
try:
|
| 215 |
+
tokenizer.apply_chat_template(
|
| 216 |
+
[{'role': 'system', 'content': 'x'}, {'role': 'user', 'content': 'y'}],
|
| 217 |
+
tokenize=True, add_generation_prompt=True)
|
| 218 |
+
args._no_system = False
|
| 219 |
+
except Exception:
|
| 220 |
+
args._no_system = True
|
| 221 |
+
sink = args.output.open('w', encoding='utf-8')
|
| 222 |
+
for index, turn in enumerate(plan):
|
| 223 |
+
user_text = turn.get('text') or turn['phrasing'].format(value=turn['value'])
|
| 224 |
+
messages.append({'role': 'user', 'content': user_text})
|
| 225 |
+
|
| 226 |
+
def _assemble(tail: list[dict]) -> list[dict]:
|
| 227 |
+
if getattr(args, '_no_system', False):
|
| 228 |
+
head = dict(tail[0]) if tail else {'role': 'user', 'content': ''}
|
| 229 |
+
head['content'] = f'{system}\n\n{head["content"]}'
|
| 230 |
+
return [head] + [dict(m) for m in tail[1:]]
|
| 231 |
+
return [{'role': 'system', 'content': system}] + [dict(m) for m in tail]
|
| 232 |
+
|
| 233 |
+
chat = _assemble(messages)
|
| 234 |
+
if args.context == 'budget':
|
| 235 |
+
# Newest-first packing under the same prefix budget the ledger system uses.
|
| 236 |
+
kept: list[dict] = []
|
| 237 |
+
for message in reversed(messages):
|
| 238 |
+
candidate = _assemble([message, *kept])
|
| 239 |
+
rendered = tokenizer.apply_chat_template(
|
| 240 |
+
candidate, tokenize=True, add_generation_prompt=True,
|
| 241 |
+
enable_thinking=False,
|
| 242 |
+
)
|
| 243 |
+
# BatchEncoding in newer transformers: len() counts KEYS, which silently
|
| 244 |
+
# disabled the cap; count tokens explicitly.
|
| 245 |
+
ids = rendered if isinstance(rendered, list) else rendered['input_ids']
|
| 246 |
+
if len(ids) > args.budget:
|
| 247 |
+
break
|
| 248 |
+
kept = [message, *kept]
|
| 249 |
+
chat = _assemble(kept)
|
| 250 |
+
encoded = tokenizer.apply_chat_template(
|
| 251 |
+
chat, tokenize=True, add_generation_prompt=True, return_tensors='pt',
|
| 252 |
+
enable_thinking=args.thinking,
|
| 253 |
+
)
|
| 254 |
+
# Newer transformers return a BatchEncoding here rather than the bare tensor.
|
| 255 |
+
prompt_ids = (encoded if torch.is_tensor(encoded) else encoded['input_ids']).to(
|
| 256 |
+
model.device)
|
| 257 |
+
torch.manual_seed(args.seed * 1000 + index)
|
| 258 |
+
begin = time.perf_counter()
|
| 259 |
+
with torch.no_grad():
|
| 260 |
+
out = model.generate(
|
| 261 |
+
prompt_ids, max_new_tokens=640 if args.thinking else 120, do_sample=False,
|
| 262 |
+
pad_token_id=tokenizer.eos_token_id or tokenizer.pad_token_id,
|
| 263 |
+
)
|
| 264 |
+
answer = tokenizer.decode(out[0][prompt_ids.shape[1]:], skip_special_tokens=True)
|
| 265 |
+
answer = re.sub(r'(?s)<think>.*?(</think>|$)', '', answer).strip()
|
| 266 |
+
messages.append({'role': 'assistant', 'content': answer})
|
| 267 |
+
record = {
|
| 268 |
+
'turn': index, 'kind': turn['kind'], 'user': user_text, 'notes': [], 'ledger': [],
|
| 269 |
+
'prefix_tokens': int(prompt_ids.shape[1]), 'answer': answer,
|
| 270 |
+
'seconds': round(time.perf_counter() - begin, 2),
|
| 271 |
+
}
|
| 272 |
+
if turn['kind'] == 'probe':
|
| 273 |
+
record.update({'key': turn['key'], 'expected': turn['value'],
|
| 274 |
+
'distance': turn['distance'], 'hit': _hit(turn['value'], answer)})
|
| 275 |
+
if turn['kind'] in ('fact', 'correction'):
|
| 276 |
+
record.update({'key': turn['key'], 'value': turn['value']})
|
| 277 |
+
records.append(record)
|
| 278 |
+
sink.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 279 |
+
sink.flush()
|
| 280 |
+
flag = '' if turn['kind'] != 'probe' else (' HIT' if record['hit'] else ' MISS')
|
| 281 |
+
print(f'[{index + 1}/{len(plan)}] {turn["kind"]}{flag} {record["seconds"]}s '
|
| 282 |
+
f'prefix={record["prefix_tokens"]}', flush=True)
|
| 283 |
+
sink.close()
|
| 284 |
+
probes = [r for r in records if r['kind'] == 'probe']
|
| 285 |
+
hits = sum(r['hit'] for r in probes)
|
| 286 |
+
print(f'\nrecall: {hits}/{len(probes)} ({hits / max(1, len(probes)):.0%})')
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
TEMPLATE = '''<!doctype html><html><head><meta charset="utf-8">
|
| 290 |
+
<title>Ledger needle — __MODEL__</title><style>
|
| 291 |
+
:root { --bg:#11141a; --panel:#1a1f29; --text:#dde3ee; --dim:#8a93a6; --hit:#3fb96b;
|
| 292 |
+
--miss:#e05555; --fact:#4d8fd1; --corr:#d9a23c; --line:#2a3140; }
|
| 293 |
+
* { box-sizing:border-box; margin:0 } body { background:var(--bg); color:var(--text);
|
| 294 |
+
font:14px/1.5 ui-monospace,Menlo,monospace; padding:24px; }
|
| 295 |
+
h1 { font-size:18px; margin-bottom:4px } .sub { color:var(--dim); margin-bottom:18px }
|
| 296 |
+
.metrics { display:flex; gap:14px; flex-wrap:wrap; margin-bottom:18px }
|
| 297 |
+
.metric { background:var(--panel); border:1px solid var(--line); border-radius:8px;
|
| 298 |
+
padding:10px 16px } .metric b { font-size:20px; display:block }
|
| 299 |
+
.dist { margin-bottom:18px } .dist .row { display:flex; align-items:center; gap:8px;
|
| 300 |
+
margin:3px 0 } .dist .bar { height:14px; background:var(--hit); border-radius:3px }
|
| 301 |
+
.dist .bar.m { background:var(--miss) } .dist span { color:var(--dim); font-size:12px;
|
| 302 |
+
min-width:110px }
|
| 303 |
+
.wrap { display:grid; grid-template-columns:340px 1fr; gap:16px; align-items:start }
|
| 304 |
+
.timeline { max-height:75vh; overflow-y:auto; background:var(--panel);
|
| 305 |
+
border:1px solid var(--line); border-radius:8px }
|
| 306 |
+
.t { padding:7px 10px; border-bottom:1px solid var(--line); cursor:pointer;
|
| 307 |
+
display:flex; gap:8px; align-items:center } .t:hover,.t.sel { background:#242b38 }
|
| 308 |
+
.t .n { color:var(--dim); min-width:30px } .dot { width:9px; height:9px; border-radius:50%;
|
| 309 |
+
flex:none } .dot.distractor { background:var(--dim) } .dot.fact { background:var(--fact) }
|
| 310 |
+
.dot.correction { background:var(--corr) } .dot.probe.hit { background:var(--hit) }
|
| 311 |
+
.dot.probe.miss { background:var(--miss) } .t .txt { white-space:nowrap; overflow:hidden;
|
| 312 |
+
text-overflow:ellipsis; font-size:12px }
|
| 313 |
+
.detail { background:var(--panel); border:1px solid var(--line); border-radius:8px;
|
| 314 |
+
padding:16px; max-height:75vh; overflow-y:auto }
|
| 315 |
+
.detail h3 { font-size:13px; color:var(--dim); margin:14px 0 6px; text-transform:uppercase }
|
| 316 |
+
.detail h3:first-child { margin-top:0 }
|
| 317 |
+
.bubble { background:#242b38; border-radius:8px; padding:10px 12px; margin:4px 0 }
|
| 318 |
+
.note { color:#9fc4ea } .ledger-entry { display:inline-block; background:#242b38;
|
| 319 |
+
border:1px solid var(--line); border-radius:5px; padding:2px 8px; margin:2px;
|
| 320 |
+
font-size:12px } .verdict { padding:10px 12px; border-radius:8px; margin-top:6px }
|
| 321 |
+
.verdict.hit { background:#173523; border:1px solid var(--hit) }
|
| 322 |
+
.verdict.miss { background:#3a1d1d; border:1px solid var(--miss) }
|
| 323 |
+
.meta { color:var(--dim); font-size:12px; margin-top:12px }
|
| 324 |
+
</style></head><body>
|
| 325 |
+
<h1>Ledger needle</h1>
|
| 326 |
+
<div class="sub">__MODEL__ · __TURNS__ turnos · ventana de __KEEP__ mensajes ·
|
| 327 |
+
steps_per_block __STEPS__ · el prefijo nunca excede __MAXPREFIX__ tokens</div>
|
| 328 |
+
<div class="metrics" id="metrics"></div>
|
| 329 |
+
<div class="dist" id="dist"></div>
|
| 330 |
+
<div class="wrap"><div class="timeline" id="timeline"></div>
|
| 331 |
+
<div class="detail" id="detail">elegí un turno</div></div>
|
| 332 |
+
<script>
|
| 333 |
+
const DATA = __DATA__;
|
| 334 |
+
const probes = DATA.filter(r => r.kind === 'probe');
|
| 335 |
+
const hits = probes.filter(r => r.hit).length;
|
| 336 |
+
const corr = DATA.filter(r => r.kind === 'correction').length;
|
| 337 |
+
const maxTok = Math.max(...DATA.map(r => r.prefix_tokens));
|
| 338 |
+
const secs = DATA.reduce((a, r) => a + r.seconds, 0) / DATA.length;
|
| 339 |
+
document.getElementById('metrics').innerHTML = [
|
| 340 |
+
['recall', hits + '/' + probes.length + ' (' + Math.round(100 * hits / probes.length) + '%)'],
|
| 341 |
+
['hechos plantados', DATA.filter(r => r.kind === 'fact').length],
|
| 342 |
+
['correcciones', corr], ['prefijo máx', maxTok + ' tok'],
|
| 343 |
+
['media', secs.toFixed(1) + ' s/turno'],
|
| 344 |
+
].map(([k, v]) => '<div class="metric"><b>' + v + '</b>' + k + '</div>').join('');
|
| 345 |
+
const buckets = [[3, 10], [11, 25], [26, 50], [51, 999]];
|
| 346 |
+
document.getElementById('dist').innerHTML = '<span style="color:var(--dim)">recall por distancia (turnos desde que el hecho se dijo):</span>' +
|
| 347 |
+
buckets.map(([lo, hi]) => {
|
| 348 |
+
const set = probes.filter(p => p.distance >= lo && p.distance <= hi);
|
| 349 |
+
if (!set.length) return '';
|
| 350 |
+
const h = set.filter(p => p.hit).length;
|
| 351 |
+
return '<div class="row"><span>' + lo + '–' + (hi > 100 ? '∞' : hi) + ' (' + h + '/' + set.length +
|
| 352 |
+
')</span><div class="bar" style="width:' + (300 * h / set.length) + 'px"></div>' +
|
| 353 |
+
'<div class="bar m" style="width:' + (300 * (set.length - h) / set.length) + 'px"></div></div>';
|
| 354 |
+
}).join('');
|
| 355 |
+
const tl = document.getElementById('timeline');
|
| 356 |
+
DATA.forEach(r => {
|
| 357 |
+
const div = document.createElement('div');
|
| 358 |
+
div.className = 't'; div.dataset.turn = r.turn;
|
| 359 |
+
const cls = r.kind + (r.kind === 'probe' ? (r.hit ? ' hit' : ' miss') : '');
|
| 360 |
+
div.innerHTML = '<span class="n">' + (r.turn + 1) + '</span><span class="dot ' + cls +
|
| 361 |
+
'"></span><span class="txt">' + r.user.replace(/</g, '<') + '</span>';
|
| 362 |
+
div.onclick = () => show(r, div); tl.appendChild(div);
|
| 363 |
+
});
|
| 364 |
+
function esc(t) { return String(t).replace(/</g, '<'); }
|
| 365 |
+
function show(r, el) {
|
| 366 |
+
document.querySelectorAll('.t.sel').forEach(x => x.classList.remove('sel'));
|
| 367 |
+
el.classList.add('sel');
|
| 368 |
+
let h = '<h3>usuario (turno ' + (r.turn + 1) + ' · ' + r.kind + ')</h3><div class="bubble">' +
|
| 369 |
+
esc(r.user) + '</div>';
|
| 370 |
+
h += '<h3>ledger que vio el modelo (' + r.ledger.length + ' entradas, historia fuera de ventana)</h3>' +
|
| 371 |
+
(r.ledger.length ? r.ledger.map(e => '<span class="ledger-entry">' + esc(e) + '</span>').join('')
|
| 372 |
+
: '<span style="color:var(--dim)">vacío</span>');
|
| 373 |
+
h += '<h3>notas de este turno</h3>' + (r.notes.length
|
| 374 |
+
? r.notes.map(n => '<div class="bubble note">' + esc(n) + '</div>').join('')
|
| 375 |
+
: '<span style="color:var(--dim)">sin pensamiento</span>');
|
| 376 |
+
h += '<h3>respuesta</h3><div class="bubble">' + esc(r.answer) + '</div>';
|
| 377 |
+
if (r.kind === 'probe') h += '<div class="verdict ' + (r.hit ? 'hit' : 'miss') + '">esperado: <b>' +
|
| 378 |
+
esc(r.expected) + '</b> · distancia ' + r.distance + ' turnos · ' + (r.hit ? 'RECUPERADO' : 'PERDIDO') + '</div>';
|
| 379 |
+
if (r.kind === 'correction') h += '<div class="verdict" style="border:1px solid var(--corr)">corrige ' +
|
| 380 |
+
esc(r.key) + ': ahora <b>' + esc(r.value) + '</b></div>';
|
| 381 |
+
h += '<div class="meta">prefijo ' + r.prefix_tokens + ' tokens · ' + r.seconds + ' s</div>';
|
| 382 |
+
document.getElementById('detail').innerHTML = h;
|
| 383 |
+
}
|
| 384 |
+
</script></body></html>'''
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def _rescore(records: list[dict]) -> list[dict]:
|
| 388 |
+
"""Verdicts are re-derived from the CURRENT rule, never trusted from the run.
|
| 389 |
+
|
| 390 |
+
The stored ``hit`` reflects whatever rule was live at run time; a scoring fix must reach
|
| 391 |
+
old runs, or the viewers keep showing correct answers marked as losses.
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
for record in records:
|
| 395 |
+
if 'expected' in record:
|
| 396 |
+
record['hit'] = _hit(record['expected'], record['answer'])
|
| 397 |
+
return records
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def render(args: argparse.Namespace) -> None:
|
| 401 |
+
records = _rescore(
|
| 402 |
+
[json.loads(line) for line in args.input.open(encoding='utf-8') if line.strip()])
|
| 403 |
+
page = (TEMPLATE
|
| 404 |
+
.replace('__DATA__', json.dumps(records, ensure_ascii=False))
|
| 405 |
+
.replace('__MODEL__', html_lib.escape(args.model_name))
|
| 406 |
+
.replace('__TURNS__', str(len(records)))
|
| 407 |
+
.replace('__KEEP__', str(args.keep))
|
| 408 |
+
.replace('__STEPS__', str(args.steps))
|
| 409 |
+
.replace('__MAXPREFIX__', str(max(r['prefix_tokens'] for r in records))))
|
| 410 |
+
args.output.write_text(page, encoding='utf-8')
|
| 411 |
+
print(f'viewer -> {args.output}')
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
COMPARE_TEMPLATE = '''<!doctype html><html><head><meta charset="utf-8">
|
| 415 |
+
<title>Ledger needle — comparación</title><style>
|
| 416 |
+
:root { --bg:#11141a; --panel:#1a1f29; --text:#dde3ee; --dim:#8a93a6; --hit:#3fb96b;
|
| 417 |
+
--miss:#e05555; --line:#2a3140; --us:#16233a; }
|
| 418 |
+
* { box-sizing:border-box; margin:0 } body { background:var(--bg); color:var(--text);
|
| 419 |
+
font:14px/1.5 ui-monospace,Menlo,monospace; padding:24px; max-width:1240px; margin:0 auto }
|
| 420 |
+
h1 { font-size:18px } .sub { color:var(--dim); margin:4px 0 20px }
|
| 421 |
+
table { border-collapse:collapse; width:100%; margin-bottom:26px }
|
| 422 |
+
th, td { border:1px solid var(--line); padding:7px 10px; text-align:left; font-size:13px }
|
| 423 |
+
th { background:var(--panel); color:var(--dim); font-weight:normal; cursor:pointer;
|
| 424 |
+
user-select:none; white-space:nowrap }
|
| 425 |
+
th.sorted { color:var(--text) } th.sorted::after { content:' ↓' }
|
| 426 |
+
th.sorted.asc::after { content:' ↑' }
|
| 427 |
+
tr.us td { background:var(--us) }
|
| 428 |
+
td.hitcell { background:#173523; color:var(--hit); text-align:center; font-weight:bold }
|
| 429 |
+
td.misscell { background:#3a1d1d; color:var(--miss); text-align:center }
|
| 430 |
+
#matrix { width:auto } #matrix th, #matrix td { padding:5px 8px }
|
| 431 |
+
#matrix td.hitcell, #matrix td.misscell { width:36px; min-width:36px; padding:5px 0 }
|
| 432 |
+
#matrix th.sys { writing-mode:vertical-rl; transform:rotate(180deg); text-align:left;
|
| 433 |
+
vertical-align:bottom; max-height:190px; font-size:12px; cursor:default; padding:8px 4px }
|
| 434 |
+
#matrix th.sys.usc { color:#9fc4ea }
|
| 435 |
+
#matrix td.exp { white-space:nowrap }
|
| 436 |
+
.bar { display:inline-block; height:11px; background:var(--hit); border-radius:2px;
|
| 437 |
+
vertical-align:middle } .bar.m { background:var(--miss) }
|
| 438 |
+
h2 { font-size:14px; color:var(--dim); margin:22px 0 10px; text-transform:uppercase }
|
| 439 |
+
.caveats { background:var(--panel); border:1px solid var(--line); border-radius:8px;
|
| 440 |
+
padding:14px 18px; color:var(--dim); font-size:13px } .caveats li { margin:6px 0 0 18px }
|
| 441 |
+
.hint { color:var(--dim); font-size:12px; margin:-18px 0 10px }
|
| 442 |
+
</style></head><body>
|
| 443 |
+
<h1>Ledger needle — comparación hasta 1.5B</h1>
|
| 444 |
+
<div class="sub">100 turnos guionados (seed 21) · 15 hechos, 5 correcciones, 10 sondas a
|
| 445 |
+
distancia 3–96 · scoring por núcleo del valor, idéntico para todos · baselines con historia
|
| 446 |
+
completa, decoding greedy e instrucción explícita de memoria</div>
|
| 447 |
+
<table id="summary"><thead><tr>
|
| 448 |
+
<th data-k="label" data-t="s">sistema</th>
|
| 449 |
+
<th data-k="params" data-t="n">tamaño (B)</th>
|
| 450 |
+
<th data-k="hits" data-t="n">recall</th>
|
| 451 |
+
<th data-k="d1" data-t="n">d 3–10</th>
|
| 452 |
+
<th data-k="d2" data-t="n">d 11–50</th>
|
| 453 |
+
<th data-k="d3" data-t="n">d 51+</th>
|
| 454 |
+
<th data-k="total" data-t="n">test total (s)</th>
|
| 455 |
+
<th data-k="per" data-t="n">s/turno</th>
|
| 456 |
+
<th data-k="prefix" data-t="n">prefijo máx</th>
|
| 457 |
+
</tr></thead><tbody></tbody></table>
|
| 458 |
+
<div class="hint">click en una cabecera reordena; por defecto: más aciertos primero</div>
|
| 459 |
+
<h2>Sonda por sonda</h2>
|
| 460 |
+
__MATRIX__
|
| 461 |
+
<h2>Advertencias de lectura</h2>
|
| 462 |
+
<div class="caveats"><ul>
|
| 463 |
+
<li>Un solo seed y 10 sondas por sistema: las diferencias chicas son ruido; las grandes
|
| 464 |
+
sobreviven a ese margen.</li>
|
| 465 |
+
<li>La comparación mide el paquete mecanismo+entrenamiento: el sistema de ledger fue entrenado
|
| 466 |
+
en este registro conversacional y los baselines no. Muestra que el paquete funciona, no que el
|
| 467 |
+
mecanismo solo cause la diferencia.</li>
|
| 468 |
+
<li>Los baselines reciben la mayor ventaja razonable: historia completa en contexto, greedy,
|
| 469 |
+
instrucción de memoria, y el modo thinking nativo donde existe. El sistema de ledger corre con
|
| 470 |
+
sampling a temperatura 0.7 y un prefijo constante de ~500 tokens.</li>
|
| 471 |
+
<li>El guion es sintético y sus valores son arbitrarios por diseño; un valor sin sentido
|
| 472 |
+
("day 97") puede penalizar a un modelo que se resiste a repetir datos absurdos.</li>
|
| 473 |
+
</ul></div>
|
| 474 |
+
<script>
|
| 475 |
+
const ROWS = __ROWS__;
|
| 476 |
+
const tbody = document.querySelector('#summary tbody');
|
| 477 |
+
let sortKey = 'hits', asc = false;
|
| 478 |
+
function fmt(r) {
|
| 479 |
+
return '<td><b>' + r.label + '</b></td><td>' + r.params.toFixed(2) + '</td>' +
|
| 480 |
+
'<td><b>' + r.hits + '/' + r.probes + '</b> <span class="bar" style="width:' + 6 * r.hits +
|
| 481 |
+
'px"></span><span class="bar m" style="width:' + 6 * (r.probes - r.hits) + 'px"></span></td>' +
|
| 482 |
+
['d1', 'd2', 'd3'].map(k => '<td>' + r[k] + '/' + r[k + 'n'] + '</td>').join('') +
|
| 483 |
+
'<td>' + r.total.toFixed(0) + '</td><td>' + r.per.toFixed(1) + '</td><td>' + r.prefix + '</td>';
|
| 484 |
+
}
|
| 485 |
+
function draw() {
|
| 486 |
+
const rows = [...ROWS].sort((a, b) => {
|
| 487 |
+
const va = a[sortKey], vb = b[sortKey];
|
| 488 |
+
const c = typeof va === 'string' ? va.localeCompare(vb) : va - vb;
|
| 489 |
+
return asc ? c : -c;
|
| 490 |
+
});
|
| 491 |
+
tbody.innerHTML = rows.map(r =>
|
| 492 |
+
'<tr' + (r.us ? ' class="us"' : '') + '>' + fmt(r) + '</tr>').join('');
|
| 493 |
+
document.querySelectorAll('#summary th').forEach(th => {
|
| 494 |
+
th.classList.toggle('sorted', th.dataset.k === sortKey);
|
| 495 |
+
th.classList.toggle('asc', th.dataset.k === sortKey && asc);
|
| 496 |
+
});
|
| 497 |
+
}
|
| 498 |
+
document.querySelectorAll('#summary th').forEach(th => th.onclick = () => {
|
| 499 |
+
if (sortKey === th.dataset.k) asc = !asc;
|
| 500 |
+
else { sortKey = th.dataset.k; asc = th.dataset.t === 's'; }
|
| 501 |
+
draw();
|
| 502 |
+
});
|
| 503 |
+
draw();
|
| 504 |
+
</script></body></html>'''
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def compare(args: argparse.Namespace) -> None:
|
| 508 |
+
systems = []
|
| 509 |
+
for spec in args.inputs:
|
| 510 |
+
label, _, rest = spec.partition('=')
|
| 511 |
+
path, _, params = rest.partition('=')
|
| 512 |
+
records = _rescore(
|
| 513 |
+
[json.loads(line) for line in Path(path).open(encoding='utf-8') if line.strip()])
|
| 514 |
+
probes = [r for r in records if r['kind'] == 'probe']
|
| 515 |
+
row = {'label': html_lib.escape(label), 'params': float(params or 0),
|
| 516 |
+
'probes': len(probes), 'hits': sum(r['hit'] for r in probes),
|
| 517 |
+
'total': sum(r['seconds'] for r in records),
|
| 518 |
+
'per': sum(r['seconds'] for r in records) / max(1, len(records)),
|
| 519 |
+
'prefix': max(r['prefix_tokens'] for r in records),
|
| 520 |
+
'us': bool(args.ours and label == args.ours)}
|
| 521 |
+
for name, (lo, hi) in (('d1', (3, 10)), ('d2', (11, 50)), ('d3', (51, 999))):
|
| 522 |
+
subset = [r for r in probes if lo <= r['distance'] <= hi]
|
| 523 |
+
row[name] = sum(r['hit'] for r in subset)
|
| 524 |
+
row[name + 'n'] = len(subset)
|
| 525 |
+
systems.append((label, records, row))
|
| 526 |
+
|
| 527 |
+
first_probes = [r for r in systems[0][1] if r['kind'] == 'probe']
|
| 528 |
+
head = ['<tr><th>turno</th><th>dist</th><th>esperado</th>']
|
| 529 |
+
head += [
|
| 530 |
+
f'<th class="sys{" usc" if args.ours and label == args.ours else ""}">'
|
| 531 |
+
f'{html_lib.escape(label)}</th>'
|
| 532 |
+
for label, _, _ in systems
|
| 533 |
+
]
|
| 534 |
+
rows_html = []
|
| 535 |
+
for probe in first_probes:
|
| 536 |
+
row = [f'<td>{probe["turn"] + 1}</td><td>{probe["distance"]}</td>'
|
| 537 |
+
f'<td class="exp">{html_lib.escape(probe["expected"])}</td>']
|
| 538 |
+
for _, records, _ in systems:
|
| 539 |
+
match = next((r for r in records if r['kind'] == 'probe'
|
| 540 |
+
and r['turn'] == probe['turn']), None)
|
| 541 |
+
if match is None:
|
| 542 |
+
row.append('<td>—</td>')
|
| 543 |
+
else:
|
| 544 |
+
row.append('<td class="hitcell">✓</td>' if match['hit']
|
| 545 |
+
else '<td class="misscell">✗</td>')
|
| 546 |
+
rows_html.append(f'<tr>{"".join(row)}</tr>')
|
| 547 |
+
matrix = f'<table id="matrix">{"".join(head)}</tr>{"".join(rows_html)}</table>'
|
| 548 |
+
|
| 549 |
+
page = (COMPARE_TEMPLATE
|
| 550 |
+
.replace('__ROWS__', json.dumps([row for _, _, row in systems], ensure_ascii=False))
|
| 551 |
+
.replace('__MATRIX__', matrix))
|
| 552 |
+
args.output.write_text(page, encoding='utf-8')
|
| 553 |
+
print(f'comparison -> {args.output}')
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def main() -> None:
|
| 557 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 558 |
+
sub = parser.add_subparsers(dest='command', required=True)
|
| 559 |
+
runner = sub.add_parser('run', help='drive the scripted conversation through the engine')
|
| 560 |
+
runner.add_argument('--checkpoint', type=Path,
|
| 561 |
+
default=Path('outputs/qwen06b-genchat-sft/inference-latest.pt'))
|
| 562 |
+
runner.add_argument('--tokenizer', type=Path,
|
| 563 |
+
default=Path('artifacts/tokenizer-qwen3-adaptive.json'))
|
| 564 |
+
runner.add_argument('--turns', type=int, default=100)
|
| 565 |
+
runner.add_argument('--keep', type=int, default=4)
|
| 566 |
+
runner.add_argument('--steps', type=int, default=16)
|
| 567 |
+
runner.add_argument('--seed', type=int, default=7)
|
| 568 |
+
runner.add_argument('--output', type=Path, default=Path('bench-ledger-needle.jsonl'))
|
| 569 |
+
ar = sub.add_parser('run-ar', help='same script through a plain AR baseline')
|
| 570 |
+
ar.add_argument('--model-path', type=Path, default=Path('/root/models/Qwen3-0.6B'))
|
| 571 |
+
ar.add_argument('--turns', type=int, default=100)
|
| 572 |
+
ar.add_argument('--seed', type=int, default=7)
|
| 573 |
+
ar.add_argument('--context', choices=('full', 'budget'), default='full')
|
| 574 |
+
ar.add_argument('--budget', type=int, default=512)
|
| 575 |
+
ar.add_argument('--thinking', action='store_true',
|
| 576 |
+
help='enable the native reasoning mode where the chat template supports it')
|
| 577 |
+
ar.add_argument('--output', type=Path, default=Path('bench-needle-ar.jsonl'))
|
| 578 |
+
|
| 579 |
+
cmp_ = sub.add_parser('compare', help='comparison table across systems as HTML')
|
| 580 |
+
cmp_.add_argument('--inputs', nargs='+', required=True, metavar='LABEL=PATH[=PARAMS_B]')
|
| 581 |
+
cmp_.add_argument('--ours', default=None, help='label to highlight as our system')
|
| 582 |
+
cmp_.add_argument('--output', type=Path, default=Path('docs/ledger-needle-compare.html'))
|
| 583 |
+
|
| 584 |
+
view = sub.add_parser('render', help='write the self-contained HTML viewer')
|
| 585 |
+
view.add_argument('--input', type=Path, default=Path('bench-ledger-needle.jsonl'))
|
| 586 |
+
view.add_argument('--output', type=Path, default=Path('docs/ledger-needle.html'))
|
| 587 |
+
view.add_argument('--model-name', default='qwen06b-genchat-sft')
|
| 588 |
+
view.add_argument('--keep', type=int, default=4)
|
| 589 |
+
view.add_argument('--steps', type=int, default=16)
|
| 590 |
+
args = parser.parse_args()
|
| 591 |
+
if args.command == 'run':
|
| 592 |
+
run(args)
|
| 593 |
+
elif args.command == 'run-ar':
|
| 594 |
+
run_ar(args)
|
| 595 |
+
elif args.command == 'compare':
|
| 596 |
+
compare(args)
|
| 597 |
+
else:
|
| 598 |
+
render(args)
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
if __name__ == '__main__':
|
| 602 |
+
main()
|
bench/ledger-needle-compare.html
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html><head><meta charset="utf-8">
|
| 2 |
+
<title>Ledger needle — comparación</title><style>
|
| 3 |
+
:root { --bg:#11141a; --panel:#1a1f29; --text:#dde3ee; --dim:#8a93a6; --hit:#3fb96b;
|
| 4 |
+
--miss:#e05555; --line:#2a3140; --us:#16233a; }
|
| 5 |
+
* { box-sizing:border-box; margin:0 } body { background:var(--bg); color:var(--text);
|
| 6 |
+
font:14px/1.5 ui-monospace,Menlo,monospace; padding:24px; max-width:1240px; margin:0 auto }
|
| 7 |
+
h1 { font-size:18px } .sub { color:var(--dim); margin:4px 0 20px }
|
| 8 |
+
table { border-collapse:collapse; width:100%; margin-bottom:26px }
|
| 9 |
+
th, td { border:1px solid var(--line); padding:7px 10px; text-align:left; font-size:13px }
|
| 10 |
+
th { background:var(--panel); color:var(--dim); font-weight:normal; cursor:pointer;
|
| 11 |
+
user-select:none; white-space:nowrap }
|
| 12 |
+
th.sorted { color:var(--text) } th.sorted::after { content:' ↓' }
|
| 13 |
+
th.sorted.asc::after { content:' ↑' }
|
| 14 |
+
tr.us td { background:var(--us) }
|
| 15 |
+
td.hitcell { background:#173523; color:var(--hit); text-align:center; font-weight:bold }
|
| 16 |
+
td.misscell { background:#3a1d1d; color:var(--miss); text-align:center }
|
| 17 |
+
#matrix { width:auto } #matrix th, #matrix td { padding:5px 8px }
|
| 18 |
+
#matrix td.hitcell, #matrix td.misscell { width:36px; min-width:36px; padding:5px 0 }
|
| 19 |
+
#matrix th.sys { writing-mode:vertical-rl; transform:rotate(180deg); text-align:left;
|
| 20 |
+
vertical-align:bottom; max-height:190px; font-size:12px; cursor:default; padding:8px 4px }
|
| 21 |
+
#matrix th.sys.usc { color:#9fc4ea }
|
| 22 |
+
#matrix td.exp { white-space:nowrap }
|
| 23 |
+
.bar { display:inline-block; height:11px; background:var(--hit); border-radius:2px;
|
| 24 |
+
vertical-align:middle } .bar.m { background:var(--miss) }
|
| 25 |
+
h2 { font-size:14px; color:var(--dim); margin:22px 0 10px; text-transform:uppercase }
|
| 26 |
+
.caveats { background:var(--panel); border:1px solid var(--line); border-radius:8px;
|
| 27 |
+
padding:14px 18px; color:var(--dim); font-size:13px } .caveats li { margin:6px 0 0 18px }
|
| 28 |
+
.hint { color:var(--dim); font-size:12px; margin:-18px 0 10px }
|
| 29 |
+
</style></head><body>
|
| 30 |
+
<h1>Ledger needle — comparación hasta 1.5B</h1>
|
| 31 |
+
<div class="sub">100 turnos guionados (seed 21) · 15 hechos, 5 correcciones, 10 sondas a
|
| 32 |
+
distancia 3–96 · scoring por núcleo del valor, idéntico para todos · baselines con historia
|
| 33 |
+
completa, decoding greedy e instrucción explícita de memoria</div>
|
| 34 |
+
<table id="summary"><thead><tr>
|
| 35 |
+
<th data-k="label" data-t="s">sistema</th>
|
| 36 |
+
<th data-k="params" data-t="n">tamaño (B)</th>
|
| 37 |
+
<th data-k="hits" data-t="n">recall</th>
|
| 38 |
+
<th data-k="d1" data-t="n">d 3–10</th>
|
| 39 |
+
<th data-k="d2" data-t="n">d 11–50</th>
|
| 40 |
+
<th data-k="d3" data-t="n">d 51+</th>
|
| 41 |
+
<th data-k="total" data-t="n">test total (s)</th>
|
| 42 |
+
<th data-k="per" data-t="n">s/turno</th>
|
| 43 |
+
<th data-k="prefix" data-t="n">prefijo máx</th>
|
| 44 |
+
</tr></thead><tbody></tbody></table>
|
| 45 |
+
<div class="hint">click en una cabecera reordena; por defecto: más aciertos primero</div>
|
| 46 |
+
<h2>Sonda por sonda</h2>
|
| 47 |
+
<table id="matrix"><tr><th>turno</th><th>dist</th><th>esperado</th><th class="sys">Qwen2.5-1.5B-Instruct · full</th><th class="sys">Qwen3-0.6B + thinking · full</th><th class="sys usc">ledger qwen06b-genchat (nuestro)</th><th class="sys">Qwen2.5-0.5B-Instruct · full</th><th class="sys">SmolLM2-360M-Instruct · full</th><th class="sys">TinyLlama-1.1B-Chat · full</th><th class="sys">Qwen3-0.6B sin thinking · full</th><th class="sys">Qwen3-0.6B sin thinking · budget 512</th></tr><tr><td>6</td><td>5</td><td class="exp">code 4805</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td></tr><tr><td>17</td><td>6</td><td class="exp">$6,400</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>28</td><td>3</td><td class="exp">7:15pm</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>61</td><td>57</td><td class="exp">harbor858</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>72</td><td>37</td><td class="exp">$4,000</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>83</td><td>47</td><td class="exp">day 97</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>94</td><td>83</td><td class="exp">$6,400</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>98</td><td>59</td><td class="exp">room 411</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>99</td><td>60</td><td class="exp">room 411</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr><tr><td>100</td><td>96</td><td class="exp">harbor858</td><td class="hitcell">✓</td><td class="hitcell">✓</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td><td class="misscell">✗</td></tr></table>
|
| 48 |
+
<h2>Advertencias de lectura</h2>
|
| 49 |
+
<div class="caveats"><ul>
|
| 50 |
+
<li>Un solo seed y 10 sondas por sistema: las diferencias chicas son ruido; las grandes
|
| 51 |
+
sobreviven a ese margen.</li>
|
| 52 |
+
<li>La comparación mide el paquete mecanismo+entrenamiento: el sistema de ledger fue entrenado
|
| 53 |
+
en este registro conversacional y los baselines no. Muestra que el paquete funciona, no que el
|
| 54 |
+
mecanismo solo cause la diferencia.</li>
|
| 55 |
+
<li>Los baselines reciben la mayor ventaja razonable: historia completa en contexto, greedy,
|
| 56 |
+
instrucción de memoria, y el modo thinking nativo donde existe. El sistema de ledger corre con
|
| 57 |
+
sampling a temperatura 0.7 y un prefijo constante de ~500 tokens.</li>
|
| 58 |
+
<li>El guion es sintético y sus valores son arbitrarios por diseño; un valor sin sentido
|
| 59 |
+
("day 97") puede penalizar a un modelo que se resiste a repetir datos absurdos.</li>
|
| 60 |
+
</ul></div>
|
| 61 |
+
<script>
|
| 62 |
+
const ROWS = [{"label": "Qwen2.5-1.5B-Instruct · full", "params": 1.54, "probes": 10, "hits": 9, "total": 76.74, "per": 0.7674, "prefix": 2881, "us": false, "d1": 3, "d1n": 3, "d2": 1, "d2n": 2, "d3": 5, "d3n": 5}, {"label": "Qwen3-0.6B + thinking · full", "params": 0.6, "probes": 10, "hits": 7, "total": 1318.3, "per": 13.183, "prefix": 4450, "us": false, "d1": 3, "d1n": 3, "d2": 1, "d2n": 2, "d3": 3, "d3n": 5}, {"label": "ledger qwen06b-genchat (nuestro)", "params": 0.6, "probes": 10, "hits": 6, "total": 218.07, "per": 2.1807, "prefix": 625, "us": true, "d1": 3, "d1n": 3, "d2": 1, "d2n": 2, "d3": 2, "d3n": 5}, {"label": "Qwen2.5-0.5B-Instruct · full", "params": 0.49, "probes": 10, "hits": 4, "total": 110.34, "per": 1.1034, "prefix": 3908, "us": false, "d1": 2, "d1n": 3, "d2": 1, "d2n": 2, "d3": 1, "d3n": 5}, {"label": "SmolLM2-360M-Instruct · full", "params": 0.36, "probes": 10, "hits": 4, "total": 56.93, "per": 0.5693, "prefix": 2712, "us": false, "d1": 3, "d1n": 3, "d2": 1, "d2n": 2, "d3": 0, "d3n": 5}, {"label": "TinyLlama-1.1B-Chat · full", "params": 1.1, "probes": 10, "hits": 3, "total": 120.77, "per": 1.2077, "prefix": 5126, "us": false, "d1": 3, "d1n": 3, "d2": 0, "d2n": 2, "d3": 0, "d3n": 5}, {"label": "Qwen3-0.6B sin thinking · full", "params": 0.6, "probes": 10, "hits": 1, "total": 129.6, "per": 1.296, "prefix": 3722, "us": false, "d1": 1, "d1n": 3, "d2": 0, "d2n": 2, "d3": 0, "d3n": 5}, {"label": "Qwen3-0.6B sin thinking · budget 512", "params": 0.6, "probes": 10, "hits": 1, "total": 161.75, "per": 1.6175, "prefix": 512, "us": false, "d1": 1, "d1n": 3, "d2": 0, "d2n": 2, "d3": 0, "d3n": 5}];
|
| 63 |
+
const tbody = document.querySelector('#summary tbody');
|
| 64 |
+
let sortKey = 'hits', asc = false;
|
| 65 |
+
function fmt(r) {
|
| 66 |
+
return '<td><b>' + r.label + '</b></td><td>' + r.params.toFixed(2) + '</td>' +
|
| 67 |
+
'<td><b>' + r.hits + '/' + r.probes + '</b> <span class="bar" style="width:' + 6 * r.hits +
|
| 68 |
+
'px"></span><span class="bar m" style="width:' + 6 * (r.probes - r.hits) + 'px"></span></td>' +
|
| 69 |
+
['d1', 'd2', 'd3'].map(k => '<td>' + r[k] + '/' + r[k + 'n'] + '</td>').join('') +
|
| 70 |
+
'<td>' + r.total.toFixed(0) + '</td><td>' + r.per.toFixed(1) + '</td><td>' + r.prefix + '</td>';
|
| 71 |
+
}
|
| 72 |
+
function draw() {
|
| 73 |
+
const rows = [...ROWS].sort((a, b) => {
|
| 74 |
+
const va = a[sortKey], vb = b[sortKey];
|
| 75 |
+
const c = typeof va === 'string' ? va.localeCompare(vb) : va - vb;
|
| 76 |
+
return asc ? c : -c;
|
| 77 |
+
});
|
| 78 |
+
tbody.innerHTML = rows.map(r =>
|
| 79 |
+
'<tr' + (r.us ? ' class="us"' : '') + '>' + fmt(r) + '</tr>').join('');
|
| 80 |
+
document.querySelectorAll('#summary th').forEach(th => {
|
| 81 |
+
th.classList.toggle('sorted', th.dataset.k === sortKey);
|
| 82 |
+
th.classList.toggle('asc', th.dataset.k === sortKey && asc);
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
document.querySelectorAll('#summary th').forEach(th => th.onclick = () => {
|
| 86 |
+
if (sortKey === th.dataset.k) asc = !asc;
|
| 87 |
+
else { sortKey = th.dataset.k; asc = th.dataset.t === 's'; }
|
| 88 |
+
draw();
|
| 89 |
+
});
|
| 90 |
+
draw();
|
| 91 |
+
</script></body></html>
|
bench/ledger-needle-ours.html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bench/results-ledger-ours.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bench/results-qwen25-05b-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 59, "answer": "The code for the gym locker is 4805.", "seconds": 2.56, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 87, "answer": "The street has been repaved.", "seconds": 0.26}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 109, "answer": "A week can vary greatly depending on many factors such as your location, personal habits, and current circumstances. It's important to remember that while a week may seem like a long time in some contexts, it can also be a short period of time in others.", "seconds": 2.21}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 180, "answer": "The password for the cabin Wi-Fi is \"harbor858\".", "seconds": 0.58, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 210, "answer": "The street has been repaved.", "seconds": 0.36}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 233, "answer": "The locker code is 4805.", "seconds": 0.43, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 257, "answer": "I'm sorry, but I don't have enough context to provide an accurate response about what you're asking. Could you please provide more information or clarify your question?", "seconds": 3.84}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 308, "answer": "Spot B19 has always been reserved.", "seconds": 0.38, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 342, "answer": "The new locker code for the gym locker is 1688.", "seconds": 0.67, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 373, "answer": "The cat knocked over a plant.", "seconds": 0.33}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 402, "answer": "The deposit for the venue was $6,400.", "seconds": 0.54, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 432, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 3.77}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 536, "answer": "Yesterday, I saw a great documentary.", "seconds": 0.49}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 559, "answer": "I see! The recent weather conditions have certainly made things interesting. How can I assist further today?", "seconds": 1.09}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 599, "answer": "Ms. Duarte is Tomás' new teacher.", "seconds": 0.59, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 628, "answer": "Yes, I did catch the game last night.", "seconds": 0.57}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 655, "answer": "The deposit for the venue was $6,400.", "seconds": 0.69, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": true}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 690, "answer": "The plumber estimated the cost for the bathroom would be $4,900.", "seconds": 0.82, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 724, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 4.16}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 827, "answer": "Traffic this morning was very heavy.", "seconds": 0.35}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 853, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.77}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 887, "answer": "Our train departs from Platform 58.", "seconds": 0.51, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 912, "answer": "The street has been repaved.", "seconds": 0.44}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 934, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.48}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 963, "answer": "The dentist moved you to 7:15 PM.", "seconds": 0.56, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 991, "answer": "Yesterday, I saw a great documentary.", "seconds": 0.53}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 1016, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 4.53}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 1120, "answer": "The dentist slot is located on the right side of the door.", "seconds": 0.61, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": false}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 1152, "answer": "The doctor has changed the dose to 27 mg.", "seconds": 0.67, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1180, "answer": "It sounds like you might decide to go for a walk later.", "seconds": 0.76}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1207, "answer": "I finally finished that book.", "seconds": 0.34}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 1237, "answer": "The couch is ordered as ZT6045. Keep it handy.", "seconds": 0.97, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1268, "answer": "A week can vary greatly depending on many factors such as your location, personal habits, and current circumstances. It's important to remember that while a week may seem like a long time in some contexts, it can also be a short period of time in others.", "seconds": 2.52}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1336, "answer": "I'm sorry to hear that you're experiencing back pain from your gym workout. It's important to take care of your body after physical activity, especially when it comes to injuries like back pain. You might want to consider taking a break from exercise and focusing on stretching and strengthening exercises instead. Additionally, making sure to get plenty of rest and hydration can help alleviate any discomfort. If the pain persists or worsens, it's recommended to consult with a healthcare provider for proper evaluation and treatment.", "seconds": 4.93}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1459, "answer": "The plumber estimate for the bathroom is $4,000.", "seconds": 0.74, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1494, "answer": "The landlord wants the rent to be paid monthly starting from Day 97.", "seconds": 0.88, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1526, "answer": "The cat knocked over a plant.", "seconds": 0.42}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1548, "answer": "Traffic this morning was very heavy.", "seconds": 0.38}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 1575, "answer": "We got room 411 at the hotel.", "seconds": 0.58, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1605, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.8}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1636, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.4}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 1659, "answer": "I see! The recent weather conditions have certainly made things interesting. How can I assist further today?", "seconds": 0.91}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 1696, "answer": "The boarding is at Gate 17.", "seconds": 0.44, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1720, "answer": "A week can vary greatly depending on many factors such as your location, personal habits, and current circumstances. It's important to remember that while a week may seem like a long time in some contexts, it can also be a short period of time in others.", "seconds": 2.35}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1788, "answer": "The cat knocked over a plant.", "seconds": 0.41}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 1819, "answer": "My sister landed on Flight AL7371.", "seconds": 0.51, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1845, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.49}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 1878, "answer": "The sister flight is now flight BL4905.", "seconds": 0.65, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1905, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.43}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 1933, "answer": "The rental has a plate number HJC-988.", "seconds": 0.68, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1962, "answer": "I'm sorry to hear that you're experiencing back pain from your gym workout. It's important to take care of your body after physical activity, especially when it comes to injuries like back pain. You might want to consider taking a break from exercise and focusing on stretching and strengthening exercises instead. Additionally, making sure to get plenty of rest and hydration can help alleviate any discomfort. If the pain persists or worsens, it's recommended to consult with a healthcare provider for proper evaluation and treatment.", "seconds": 4.17}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2074, "answer": "I finally finished that book.", "seconds": 0.26}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2095, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.44}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2117, "answer": "Today's work was chaotic.", "seconds": 0.29}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2142, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.71}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2177, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.72}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2211, "answer": "Yes, I did catch the game last night.", "seconds": 0.43}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2236, "answer": "Traffic this morning was very heavy.", "seconds": 0.36}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2258, "answer": "Traffic this morning was very heavy.", "seconds": 0.39}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2280, "answer": "A week can vary greatly depending on many factors such as your location, personal habits, and current circumstances. It's important to remember that while a week may seem like a long time in some contexts, it can also be a short period of time in others.", "seconds": 2.4}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 2351, "answer": "The password for the Wi-Fi network was \"harbor858\".", "seconds": 0.71, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": true}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2380, "answer": "I finally finished that book.", "seconds": 0.35}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2401, "answer": "The street has been repaved.", "seconds": 0.33}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2423, "answer": "The street has been repaved.", "seconds": 0.4}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 2446, "answer": "Yesterday, I saw a great documentary.", "seconds": 0.36}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2469, "answer": "A week can vary greatly depending on many factors such as your location, personal habits, and current circumstances. It's important to remember that while a week may seem like a long time in some contexts, it can also be a short period of time in others.", "seconds": 2.23}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2536, "answer": "Thinking of making pasta tonight sounds delicious! What type of pasta do you prefer?", "seconds": 0.83}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2566, "answer": "Today's work was chaotic.", "seconds": 0.35}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 2588, "answer": "It sounds like you might decide to go for a walk later.", "seconds": 0.64}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2620, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.73}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2651, "answer": "The street has been repaved.", "seconds": 0.4}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 2677, "answer": "The plumber quote for the bathroom is $4,000.", "seconds": 0.78, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": true}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2706, "answer": "Traffic this morning was very heavy.", "seconds": 0.41}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 2736, "answer": "The parking spot is now spot E16.", "seconds": 0.62, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 2761, "answer": "I see! The recent weather conditions have certainly made things interesting. How can I assist further today?", "seconds": 1.26}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2795, "answer": "You might enjoy something sweet, such as chocolate cake or cookies.", "seconds": 0.79}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2827, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 1.0}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2858, "answer": "Thinking of making pasta tonight sounds delicious! What type of pasta do you prefer?", "seconds": 0.9}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2892, "answer": "Yes, I did catch the game last night.", "seconds": 0.55}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2919, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 4.05}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3024, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 4.06}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3129, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 3.73}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3237, "answer": "I'm sorry to hear that you forgot the rent due. Please let me know how much you need to pay so we can proceed with the payment process.", "seconds": 1.27, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3283, "answer": "Thinking of making pasta tonight sounds delicious! What type of pasta do you prefer?", "seconds": 0.64}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3314, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.43}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3337, "answer": "Thinking of making pasta tonight sounds delicious! What type of pasta do you prefer?", "seconds": 0.93}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 3378, "answer": "The plumber estimate for the bathroom is $1,400.", "seconds": 0.62, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3409, "answer": "If your phone battery is dying quickly, it could be due to several reasons including low charge level, overuse, or damage from extreme temperatures. To troubleshoot this issue, try charging your phone fully before using it again, consider swapping out batteries if possible, and ensure your phone is not exposed to extreme temperatures. If the problem persists, it might be best to consult a professional technician who can diagnose and repair any issues with your device.", "seconds": 4.0}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3512, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.61}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 3534, "answer": "I finally finished that book.", "seconds": 0.38}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 3556, "answer": "I'm sorry to hear that you're experiencing back pain from your gym workout. It's important to take care of your body after physical activity, especially when it comes to injuries like back pain. You might want to consider taking a break from exercise and focusing on stretching and strengthening exercises instead. Additionally, making sure to get plenty of rest and hydration can help alleviate any discomfort. If the pain persists or worsens, it's recommended to consult with a healthcare provider for proper evaluation and treatment.", "seconds": 4.39}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 3672, "answer": "Yes, I did catch the game last night.", "seconds": 0.61}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 3696, "answer": "Today's work was chaotic.", "seconds": 0.44}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3721, "answer": "I'm sorry to hear that you forgot the deposit. Please let me know how much you need to pay so we can proceed with the payment process.", "seconds": 1.65, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": false}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3766, "answer": "Coffee prices here are becoming increasingly expensive.", "seconds": 0.4}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 3788, "answer": "You might enjoy something sweet, such as chocolate cake or cookies.", "seconds": 0.82}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3820, "answer": "The neighbor is renovating again, which means there will be increased noise levels.", "seconds": 0.99}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 3854, "answer": "The hotel room is room 58.", "seconds": 0.48, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 3879, "answer": "The hotel room is room 58.", "seconds": 0.47, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3908, "answer": "I'm sorry to hear that you forgot the wifi password. Please let me know how much you need to pay so we can proceed with the payment process.", "seconds": 1.18, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": false}
|
bench/results-qwen25-15b-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 59, "answer": "The gym locker code you provided is 4805.", "seconds": 6.46, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 87, "answer": "They repaved your street.", "seconds": 0.46}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 108, "answer": "It sounds like you had an eventful or challenging week.", "seconds": 0.75}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 139, "answer": "The cabin's WiFi password is \"harbor858\".", "seconds": 0.89, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 167, "answer": "They repaved your street.", "seconds": 0.43}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 189, "answer": "The gym locker code is 4805.", "seconds": 0.73, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 214, "answer": "You finally finished reading that book.", "seconds": 0.41}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 239, "answer": "You always park in spot B19.", "seconds": 0.51, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 273, "answer": "The gym locker code has been updated to 1688.", "seconds": 0.79, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 303, "answer": "The cat knocked over a plant.", "seconds": 0.53}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 332, "answer": "The deposit for the venue was $6,400.", "seconds": 0.85, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 362, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.48}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 387, "answer": "You saw a great documentary yesterday.", "seconds": 0.34}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 409, "answer": "You really enjoy the current weather conditions recently.", "seconds": 0.47}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 438, "answer": "Tomas' new teacher is Ms. Duarte.", "seconds": 0.59, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 467, "answer": "I'm sorry, but I don't have any information about a specific game being played last night. Could you please provide more details or clarify which game you're referring to?", "seconds": 1.8}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 519, "answer": "The deposit for the venue was $6,400.", "seconds": 0.69, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": true}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 554, "answer": "The plumber quoted $4,900 for the bathroom work.", "seconds": 0.85, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 585, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.51}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 609, "answer": "Traffic was extremely busy and congested this morning.", "seconds": 0.44}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 638, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.16}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 676, "answer": "Our train departs from Platform 58.", "seconds": 0.64, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 701, "answer": "They repaved your street.", "seconds": 0.44}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 722, "answer": "Coffee prices have increased recently.", "seconds": 0.32}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 749, "answer": "You were moved to see the dentist at 7:15 PM.", "seconds": 0.88, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 780, "answer": "You watched a great documentary yesterday.", "seconds": 0.4}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 804, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.49}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 829, "answer": "You were moved to see the dentist at 7:15 PM.", "seconds": 0.87, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": true}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 863, "answer": "The doctor adjusted the medication dosage to 27 mg.", "seconds": 0.82, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 891, "answer": "You might take a walk later.", "seconds": 0.48}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 912, "answer": "You finally finished reading that book.", "seconds": 0.59}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 943, "answer": "The couch is ordered as model number ZT6045 and should be kept ready.", "seconds": 1.18, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 977, "answer": "It sounds like you had a challenging or eventful week.", "seconds": 0.76}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1005, "answer": "You've injured your back from working out at the gym.", "seconds": 0.81}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1042, "answer": "The plumber's quote for the job is now $4,000.", "seconds": 1.11, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1079, "answer": "The landlord requires the full monthly rent paid by the ninth of the following month.", "seconds": 0.96, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1111, "answer": "The cat knocked over a plant.", "seconds": 0.4}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1133, "answer": "Traffic was extremely busy and congested this morning.", "seconds": 0.49}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 1163, "answer": "You booked Room 411 at the hotel.", "seconds": 0.54, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1193, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 0.88}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1228, "answer": "Coffee prices have increased recently.", "seconds": 0.37}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 1249, "answer": "You really enjoy the current weather conditions recently.", "seconds": 0.46}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 1275, "answer": "Boarding instructions state to board at Gate E17.", "seconds": 0.61, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1302, "answer": "It sounds like you had a challenging or eventful week.", "seconds": 0.66}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1330, "answer": "The cat knocked over a plant.", "seconds": 0.47}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 1361, "answer": "Your sister is scheduled to land on Flight AL7371.", "seconds": 0.82, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1390, "answer": "Coffee prices have increased recently.", "seconds": 0.31}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 1421, "answer": "Your sister is scheduled to fly on Flight BL4905.", "seconds": 0.7, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1450, "answer": "Coffee prices have increased recently.", "seconds": 0.3}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 1476, "answer": "The rental car is registered under license plate HJC-988.", "seconds": 0.67, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1507, "answer": "You've injured your back from working out at the gym.", "seconds": 0.57}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1533, "answer": "You finally finished reading that book.", "seconds": 0.41}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1555, "answer": "Coffee prices have increased recently.", "seconds": 0.41}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 1575, "answer": "Work was chaotic today.", "seconds": 0.29}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1599, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.08}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1638, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.07}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 1676, "answer": "I'm sorry, but I don't have any information about a specific game being played last night. Could you please provide more details or clarify which game you're referring to?", "seconds": 2.23}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1726, "answer": "Traffic was extremely busy and congested this morning.", "seconds": 0.74}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1751, "answer": "Traffic was extremely busy and congested this morning.", "seconds": 0.59}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1776, "answer": "It sounds like you had a challenging or eventful week.", "seconds": 0.8}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 1807, "answer": "The correct WiFi password is \"harbor858.\"", "seconds": 0.68, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": true}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1833, "answer": "You finally finished reading that book.", "seconds": 0.48}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1855, "answer": "They repaved your street.", "seconds": 0.41}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1876, "answer": "They repaved your street.", "seconds": 0.46}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 1898, "answer": "You saw a great documentary yesterday.", "seconds": 0.53}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1920, "answer": "It sounds like you had a challenging or eventful week.", "seconds": 0.82}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 1947, "answer": "You're thinking of making pasta tonight.", "seconds": 0.62}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 1969, "answer": "Work was chaotic today.", "seconds": 0.37}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1990, "answer": "You might take a walk later.", "seconds": 0.42}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2016, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.14}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2051, "answer": "They repaved your street.", "seconds": 0.49}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 2076, "answer": "The plumber's quote for the job is now $4,000.", "seconds": 0.98, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": true}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2107, "answer": "Traffic was extremely busy and congested this morning.", "seconds": 0.76}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 2140, "answer": "The parking spot is designated as Spot E16.", "seconds": 0.79, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 2166, "answer": "You really enjoy the current weather conditions recently.", "seconds": 0.78}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2189, "answer": "You're craving something sweet.", "seconds": 0.52}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2214, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.22}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2249, "answer": "You're considering cooking pasta tonight.", "seconds": 0.65}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2274, "answer": "I'm sorry, but I don't have any information about a specific game being played last night. Could you please provide more details or clarify which game you're referring to?", "seconds": 2.47}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2326, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.9}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2352, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.69}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2378, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.58}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2407, "answer": "The rent due was $6,400.", "seconds": 0.79, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2433, "answer": "You're considering cooking pasta tonight.", "seconds": 0.52}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2455, "answer": "Coffee prices have increased recently.", "seconds": 0.49}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2476, "answer": "You're considering cooking pasta tonight.", "seconds": 0.54}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 2508, "answer": "The plumber's quote for the job is now $1,400.", "seconds": 0.96, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2541, "answer": "Your phone battery dies faster than usual now.", "seconds": 0.58}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2565, "answer": "Coffee prices have increased recently.", "seconds": 0.43}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2585, "answer": "You finally finished reading that book.", "seconds": 0.55}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 2608, "answer": "You've injured your back from working out at the gym.", "seconds": 1.0}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2638, "answer": "I'm sorry, but I currently don't have any information about a specific game being played last night. Could you please provide more details or clarify which game you're referring to?", "seconds": 1.8}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2688, "answer": "Work was chaotic today.", "seconds": 0.39}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2712, "answer": "The deposit for the venue was $6,400.", "seconds": 0.77, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": true}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2740, "answer": "Coffee prices have increased recently.", "seconds": 0.44}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2760, "answer": "You're craving something sweet.", "seconds": 0.52}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2785, "answer": "The neighbor is renovating their home once again, causing quite a disturbance due to the noise level.", "seconds": 1.19}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 2823, "answer": "You booked Room 411 at the hotel.", "seconds": 0.83, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": true}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 2850, "answer": "You booked Room 411 at the hotel.", "seconds": 0.77, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": true}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2881, "answer": "The WiFi password is \"harbor858.\"", "seconds": 0.65, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": true}
|
bench/results-qwen3-06b-nothink-budget512.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 43, "answer": "The code 4805 corresponds to a gym locker. If you have any questions about it, feel free to ask!", "seconds": 3.13, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 84, "answer": "They repaved your street. Let me know if you need help with anything else!", "seconds": 1.17}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 116, "answer": "That's a great way to say it! If you need help with anything else, feel free to ask!", "seconds": 1.39}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 157, "answer": "The cabin's WiFi password is **harbor858**. Let me know if you need help with anything else!", "seconds": 1.74, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 197, "answer": "They repaved your street. Let me know if you need help with anything else!", "seconds": 1.38}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 230, "answer": "Yes, the locker code is **4805**. Let me know if you need help with anything else!", "seconds": 1.91, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 268, "answer": "Finally, you finished that book! Let me know if you need help with anything else!", "seconds": 1.17}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 304, "answer": "Yes, you always park at **spot B19**. Let me know if you need help with anything else!", "seconds": 1.96, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 353, "answer": "Actually, the locker code is now **code 1688**. Let me know if you need help with anything else!", "seconds": 1.81, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 396, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 1.45}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 436, "answer": "The deposit for the venue was **$6,400**. Let me know if you need help with anything else!", "seconds": 1.93, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 479, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.21}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 500, "answer": "Saw a great documentary yesterday. Let me know if you need help with anything else!", "seconds": 1.22}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 502, "answer": "I love this weather lately too! Let me know if you need help with anything else!", "seconds": 1.08}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "Tomás' new teacher is Ms. Duarte. Let me know if you need help with anything else!", "seconds": 1.42, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "I don't know if I caught the game last night, but I hope it was a good time! Let me know if you need help with anything else!", "seconds": 2.36}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 506, "answer": "Sure! Let me know if you need help with anything else!", "seconds": 1.15, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": false}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "The plumber quoted **$4,900** for the bathroom. Let me know if you need help with anything else!", "seconds": 2.11, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 490, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.39}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 512, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.34}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "The neighbor is renovating again, so it's loud. Let me know if you need help with anything else!", "seconds": 1.88}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "Our train leaves from **Platform 58**. Let me know if you need help with anything else!", "seconds": 1.32, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "They repaved our street. Let me know if you need help with anything else!", "seconds": 1.11}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 495, "answer": "Coffee is getting expensive. Let me know if you need help with anything else!", "seconds": 1.03}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 495, "answer": "The dentist moved you to **7:15 PM**. Let me know if you need help with anything else!", "seconds": 1.34, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 501, "answer": "Saw a great documentary yesterday! Let me know if you need help with anything else!", "seconds": 1.15}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 503, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.16}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 497, "answer": "Quick — the **dentist slot**. Let me know if you need help with anything else!", "seconds": 1.33, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": false}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 486, "answer": "The doctor changed the dose to **27 mg**. Let me know if you need help with anything else!", "seconds": 1.76, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 495, "answer": "You might go for a walk later. Let me know if you need help with anything else!", "seconds": 1.29}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 1.22}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "The couch is ordered as **ZT6045**, and I’ll keep it handy for you. Let me know if you need help with anything else!", "seconds": 2.29, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "What a week! Honestly, it's been a really fun and rewarding time! Let me know if you need help with anything else!", "seconds": 2.23}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 500, "answer": "Your back hurts from the gym. Let me know if you need help with anything else!", "seconds": 1.14}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 503, "answer": "The plumber quote is now **$4,000**. Let me know if you need help with anything else!", "seconds": 1.89, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 507, "answer": "The landlord wants your rent **by day 97** each month. Let me know if you need help with anything else!", "seconds": 2.02, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 512, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 1.49}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 505, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.27}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 507, "answer": "We got room **411** at the hotel. Let me know if you need help with anything else!", "seconds": 1.32, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 488, "answer": "The neighbor is renovating again, and it's **loud**. Let me know if you need help with anything else!", "seconds": 1.55}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 486, "answer": "Coffee is getting expensive. Let me know if you need help with anything else!", "seconds": 1.16}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 506, "answer": "I love this weather lately too! It's really nice. Let me know if you need help with anything else!", "seconds": 1.83}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 492, "answer": "Boarding is at **Gate E17**. Let me know if you need help with anything else!", "seconds": 1.55, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 510, "answer": "What a week! Honestly, it's been a really fun and rewarding time! Let me know if you need help with anything else!", "seconds": 1.93}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 505, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 1.28}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 504, "answer": "My sister lands on flight **AL7371**.", "seconds": 0.99, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "Coffee is getting expensive. Let me know if you need help with anything else!", "seconds": 1.02}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 500, "answer": "Actually, the sister flight is now **flight BL4905**. Let me know if you need help with anything else!", "seconds": 1.98, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 495, "answer": "Coffee is getting expensive. Let me know if you need help with anything else!", "seconds": 0.97}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 497, "answer": "The rental has plate **HJC-988**. Let me know if you need help with anything else!", "seconds": 1.49, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 505, "answer": "My back hurts from the gym. Let me know if you need help with anything else!", "seconds": 1.25}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 494, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 1.12}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "Coffee is getting expensive. Let me know if you need help with anything else!", "seconds": 1.06}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "Work was chaos today. Let me know if you need help with anything else!", "seconds": 0.9}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 505, "answer": "The neighbor is renovating again and it's loud. Let me know if you need help with anything else!", "seconds": 1.37}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 506, "answer": "The neighbor is renovating again and it's loud. Let me know if you need help with anything else!", "seconds": 1.63}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "Did you catch the game last night?", "seconds": 0.67}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 500, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.16}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 498, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.15}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "What a week! Honestly, I'm so glad you're having a great time. Let me know if you need help with anything else!", "seconds": 1.65}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "What was the **WiFi password again**? Let me know if you need help with anything else!", "seconds": 1.15, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": false}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 491, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 1.06}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 512, "answer": "They repaved our street! Let me know if you need help with anything else!", "seconds": 1.09}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "They repaved our street! Let me know if you need help with anything else!", "seconds": 0.95}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 512, "answer": "Saw a great documentary yesterday! Let me know if you need help with anything else!", "seconds": 1.11}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 511, "answer": "What a week! Honestly, I'm so glad you're having a great time. Let me know if you need help with anything else!", "seconds": 2.27}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 493, "answer": "Thinking of making **pasta tonight**, let me know if you'd like some ideas or help with the recipe! 😊", "seconds": 1.71}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 502, "answer": "Work was **chaos today**! Let me know if you'd like help with anything else! 😊", "seconds": 1.23}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "Yes, I might go for a **walk later**! Let me know if you'd like help with anything else! 😊", "seconds": 1.76}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 503, "answer": "The neighbor is **renovating again**, so it's loud. Let me know if you'd like help with anything else! 😊", "seconds": 1.95}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 510, "answer": "They **repaved our street**! Let me know if you need help with anything else! 😊", "seconds": 1.55}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 496, "answer": "What was the **plumber quote again**? Let me know if you'd like help with anything else! 😊", "seconds": 1.66, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": false}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 492, "answer": "Traffic was **unreal this morning**! Let me know if you'd like help with anything else! 😊", "seconds": 1.76}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "The **parking spot is now E16**! Let me know if you need help with anything else! 😊", "seconds": 1.71, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "I love this **weather lately**! It's really nice out. Let me know if you need help with anything else! 😊", "seconds": 1.68}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "I'm so glad to hear that! I love the **sweet** things too! Let me know if you'd like help with anything else! 😊", "seconds": 1.83}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 502, "answer": "The **neighbor is renovating again**, so it's loud. Let me know if you'd like help with anything else! 😊", "seconds": 1.68}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 501, "answer": "Thinking of making **pasta tonight**, let me know if you'd like some ideas or help with the recipe! 😊", "seconds": 1.25}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 504, "answer": "I **did not** catch the game last night. Let me know if you'd like help with anything else! 😊", "seconds": 1.42}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 510, "answer": "My phone battery **dies so fast now**. It's definitely a common issue. Let me know if you'd like help with anything else! 😊", "seconds": 1.9}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 503, "answer": "My phone battery **dies so fast now**. It's definitely a common issue. Let me know if you'd like help with anything else! 😊", "seconds": 2.0}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "My phone battery **dies so fast now**. It's definitely a common issue. Let me know if you'd like help with anything else! 😊", "seconds": 2.24}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 492, "answer": "I **forgot the rent due**, but I don't know the exact amount. Let me know if you'd like help with anything else! 😊", "seconds": 2.07, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "I **think of making pasta tonight**. Let me know if you'd like some ideas or help with the recipe! 😊", "seconds": 1.96}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 492, "answer": "The **coffee here is getting expensive**. It's definitely a good time to consider alternatives, like herbal or organic options. Let me know if you'd like help with anything else! 😊", "seconds": 3.02}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 504, "answer": "I **think of making pasta tonight**, and I'm really looking forward to it! Let me know if you'd like some ideas or help with the recipe! 😊", "seconds": 2.49}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 504, "answer": "The **plumber quote is now $1,400**. Let me know if you'd like help with anything else! 😊", "seconds": 2.08, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 508, "answer": "My phone battery **dies so fast now**. It's definitely a common issue. Let me know if you'd like help with anything else! 😊", "seconds": 2.27}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 512, "answer": "The **coffee here is getting expensive**. It's definitely a good time to consider alternatives like herbal or organic coffee. Let me know if you'd like help with anything else! 😊", "seconds": 2.85}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 485, "answer": "I **finally finished that book**. It was a great read! Let me know if you'd like help with anything else! 😊", "seconds": 1.95}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 480, "answer": "The **my back hurts from the gym**. It's definitely a common issue, especially after a workout. Let me know if you'd like help with anything else! 😊", "seconds": 2.01}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 485, "answer": "I **did not catch the game last night**. Let me know if you'd like help with anything else! 😊", "seconds": 1.56}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 509, "answer": "**Work was chaos today.** It's definitely stressful, but I'm glad you're able to get through it! Let me know if you'd like help with anything else! 😊", "seconds": 2.09}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 490, "answer": "The **deposit was $50**. Let me know if you'd like help with anything else! 😊", "seconds": 1.48, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": false}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 474, "answer": "The **coffee here is getting expensive**. It's definitely a good time to consider alternatives like herbal or organic coffee. Let me know if you'd like help with anything else! 😊", "seconds": 2.3}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 477, "answer": "**Craving something sweet**. It's definitely a good time to enjoy a treat, like a sweet treat or a cup of coffee. Let me know if you'd like help with anything else! 😊", "seconds": 2.56}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 484, "answer": "The **neighbor is renovating again, so it's loud**. It's definitely not a good time to be around, especially if you're in the house. Let me know if you'd like help with anything else! 😊", "seconds": 2.65}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 500, "answer": "**Remind me of the hotel room?** Let me know if you'd like help with anything else! 😊", "seconds": 1.42, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 487, "answer": "**Quick — the hotel room?** Let me know if you'd like help with anything else! 😊", "seconds": 1.51, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 487, "answer": "The **WiFi password** was **123456**. Let me know if you'd like help with anything else! 😊", "seconds": 2.25, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": false}
|
bench/results-qwen3-06b-nothink-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 43, "answer": "The code 4805 corresponds to a gym locker. If you have any questions about it, feel free to ask!", "seconds": 5.03, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 84, "answer": "They repaved your street. Let me know if you need help with anything else!", "seconds": 1.34}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 116, "answer": "That's a great way to say it! If you need help with anything else, feel free to ask!", "seconds": 1.4}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 157, "answer": "The cabin's WiFi password is **harbor858**. Let me know if you need help with anything else!", "seconds": 1.34, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 197, "answer": "They repaved your street. Let me know if you need help with anything else!", "seconds": 0.94}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 230, "answer": "Yes, the locker code is **4805**. Let me know if you need help with anything else!", "seconds": 1.38, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": false}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 268, "answer": "Finally, you finished that book! Let me know if you need help with anything else!", "seconds": 1.06}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 304, "answer": "Yes, you always park at **spot B19**. Let me know if you need help with anything else!", "seconds": 1.27, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 353, "answer": "Actually, the locker code is now **code 1688**. Let me know if you need help with anything else!", "seconds": 1.43, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 396, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 0.92}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 436, "answer": "The deposit for the venue was **$6,400**. Let me know if you need help with anything else!", "seconds": 1.4, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 479, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 0.97}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 515, "answer": "Saw a great documentary yesterday. Let me know if you need help with anything else!", "seconds": 1.06}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 548, "answer": "I love this weather lately! Let me know if you need help with anything else!", "seconds": 1.26}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 585, "answer": "Tomás' new teacher is Ms. Duarte. Let me know if you need help with anything else!", "seconds": 1.23, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 625, "answer": "Yes, I caught the game last night! Let me know if you need help with anything else!", "seconds": 1.23}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 662, "answer": "Sure! Let me know if you need help with anything else!", "seconds": 1.04, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": false}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 697, "answer": "The plumber quoted **$4,900** for the bathroom. Let me know if you need help with anything else!", "seconds": 1.64, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 740, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.48}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 775, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.01}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 811, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.75}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 853, "answer": "Our train leaves from **platform 58**. Let me know if you need help with anything else!", "seconds": 1.58, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 890, "answer": "They repaved our street. Let me know if you need help with anything else!", "seconds": 0.92}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 922, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 0.88}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 960, "answer": "The dentist moved you to **7:15 PM**. Let me know if you need help with anything else!", "seconds": 1.26, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 1000, "answer": "Saw a great documentary yesterday. Let me know if you need help with anything else!", "seconds": 0.95}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 1035, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.02}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 1071, "answer": "Yes, the dentist slot is **4805**. Let me know if you need help with anything else!", "seconds": 1.44, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": false}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 1114, "answer": "The doctor changed the dose to **27 mg**. Let me know if you need help with anything else!", "seconds": 1.33, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1153, "answer": "You might go for a walk later. Let me know if you need help with anything else!", "seconds": 0.93}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1186, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 0.87}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 1226, "answer": "The couch is ordered as **ZT6045**, and I recommend keeping it handy. Let me know if you need help with anything else!", "seconds": 2.16, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1272, "answer": "What a week! Honestly, that's a great way to say it. Let me know if you need help with anything else!", "seconds": 1.79}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1314, "answer": "Your back hurts from the gym. Let me know if you need help with anything else!", "seconds": 1.2}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1357, "answer": "Actually, the plumber quoted **$4,000** now. Let me know if you need help with anything else!", "seconds": 1.83, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1404, "answer": "The landlord wants rent to be paid **by day 97** each month. Let me know if you need help with anything else!", "seconds": 2.28, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1448, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 1.2}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1481, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 0.89}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 1518, "answer": "We got room **411** at the hotel. Let me know if you need help with anything else!", "seconds": 1.31, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1560, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.35}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1599, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.18}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 1631, "answer": "I love this weather lately! Let me know if you need help with anything else!", "seconds": 0.87}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 1665, "answer": "Boarding is at **Gate E17**. Let me know if you need help with anything else!", "seconds": 1.11, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1702, "answer": "What a week! Honestly, that's a great way to say it. Let me know if you need help with anything else!", "seconds": 1.26}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1744, "answer": "The cat knocked over a plant. Let me know if you need help with anything else!", "seconds": 0.93}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 1786, "answer": "My sister lands on flight **AL7371**. Let me know if you need help with anything else!", "seconds": 1.45, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1825, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.21}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 1867, "answer": "Actually, the sister flight is now **flight BL4905**. Let me know if you need help with anything else!", "seconds": 1.67, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1909, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.23}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 1946, "answer": "The rental has plate **HJC-988**. Let me know if you need help with anything else!", "seconds": 1.21, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1986, "answer": "Your back hurts from the gym. Let me know if you need help with anything else!", "seconds": 1.11}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2018, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 1.17}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2049, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 0.9}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2080, "answer": "Work was chaos today. Let me know if you need help with anything else!", "seconds": 0.99}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2115, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.43}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2158, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.25}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2200, "answer": "Yes, I caught the game last night! Let me know if you need help with anything else!", "seconds": 1.18}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2235, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.27}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2267, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.12}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2299, "answer": "What a week! Honestly, that's a great way to say it. Let me know if you need help with anything else!", "seconds": 1.55}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 2344, "answer": "What was the WiFi password again? Let me know if you need help with anything else!", "seconds": 1.05, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": false}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2376, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 0.88}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2407, "answer": "They repaved our street. Let me know if you need help with anything else!", "seconds": 1.21}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2439, "answer": "They repaved our street. Let me know if you need help with anything else!", "seconds": 1.26}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 2472, "answer": "Saw a great documentary yesterday. Let me know if you need help with anything else!", "seconds": 1.37}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2505, "answer": "What a week! Honestly, that's a great way to say it. Let me know if you need help with anything else!", "seconds": 1.82}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2546, "answer": "Thinking of making pasta tonight! Let me know if you need help with anything else!", "seconds": 1.04}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2577, "answer": "Work was chaotic today. Let me know if you need help with anything else!", "seconds": 0.86}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 2609, "answer": "You might go for a walk later. Let me know if you need help with anything else!", "seconds": 1.2}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2647, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.69}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2686, "answer": "They repaved our street. Let me know if you need help with anything else!", "seconds": 1.28}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 2722, "answer": "What was the plumber quote again? Let me know if you need help with anything else!", "seconds": 1.21, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": false}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2755, "answer": "Traffic was unreal this morning. Let me know if you need help with anything else!", "seconds": 1.26}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 2795, "answer": "Actually, the parking spot is now **spot E16**. Let me know if you need help with anything else!", "seconds": 1.43, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 2835, "answer": "I love this weather lately! Let me know if you need help with anything else!", "seconds": 1.14}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2866, "answer": "Craving something sweet! Let me know if you need help with anything else!", "seconds": 0.99}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2901, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.44}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2940, "answer": "Thinking of making pasta tonight! Let me know if you need help with anything else!", "seconds": 1.07}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2975, "answer": "Yes, I caught the game last night! Let me know if you need help with anything else!", "seconds": 1.17}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3012, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.43}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3049, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.26}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3086, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.32}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3126, "answer": "I forgot the rent due. Let me know if you need help with anything else!", "seconds": 1.3, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3158, "answer": "Thinking of making pasta tonight! Let me know if you need help with anything else!", "seconds": 1.6}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3190, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.44}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3222, "answer": "Thinking of making pasta tonight! Let me know if you need help with anything else!", "seconds": 1.27}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 3264, "answer": "Actually, the plumber quote is now **$1,400**. Let me know if you need help with anything else!", "seconds": 1.58, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3308, "answer": "Your phone battery is dying so fast now. Let me know if you need help with anything else!", "seconds": 1.2}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3343, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.13}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 3374, "answer": "Finally finished that book! Let me know if you need help with anything else!", "seconds": 0.95}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 3406, "answer": "Your back hurts from the gym. Let me know if you need help with anything else!", "seconds": 0.98}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 3442, "answer": "Yes, I caught the game last night! Let me know if you need help with anything else!", "seconds": 1.08}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 3476, "answer": "Work was chaotic today. Let me know if you need help with anything else!", "seconds": 0.9}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3511, "answer": "I forgot the deposit. Let me know if you need help with anything else!", "seconds": 1.29, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": false}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3542, "answer": "Coffee is getting expensive now. Let me know if you need help with anything else!", "seconds": 1.42}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 3573, "answer": "Craving something sweet! Let me know if you need help with anything else!", "seconds": 1.07}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3608, "answer": "The neighbor is renovating again, which is causing loud noises. Let me know if you need help with anything else!", "seconds": 1.34}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 3650, "answer": "Remind me of the hotel room? Let me know if you need help with anything else!", "seconds": 1.47, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 3685, "answer": "Quick — the hotel room? Let me know if you need help with anything else!", "seconds": 1.2, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3722, "answer": "I forgot the WiFi password. Let me know if you need help with anything else!", "seconds": 1.54, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": false}
|
bench/results-qwen3-06b-thinking-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 59, "answer": "the gym locker is code 4805", "seconds": 7.86, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 84, "answer": "The street was repaved.", "seconds": 8.01}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 105, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to?", "seconds": 9.42}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 149, "answer": "The cabin's WiFi password is **harbor858**.", "seconds": 6.81, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 178, "answer": "", "seconds": 36.7}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 194, "answer": "The locker code is **4805**.", "seconds": 8.86, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 219, "answer": "The locker code is **4805**. Let me know if you need anything else!", "seconds": 13.18}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 257, "answer": "The parking spot is **B19**.", "seconds": 5.16, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 292, "answer": "The locker code is now **1688**. Let me know if you need anything else!", "seconds": 13.07, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 329, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to?", "seconds": 17.58}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 376, "answer": "The deposit for the venue was **$6,400**.", "seconds": 7.27, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 408, "answer": "The locker code is **4805**. The deposit for the venue was **$6,400**. Let me know if you need anything else!", "seconds": 25.67}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 459, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to?", "seconds": 11.36}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "I'm glad you're enjoying the weather! Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 13.31}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 548, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.61, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 593, "answer": "I don't have information about the game. Could you please provide the specific code or amount you're referring to? 😊", "seconds": 23.6}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 635, "answer": "The deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 9.25, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": true}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 683, "answer": "The plumber quoted **$4,900** for the bathroom. Let me know if you need anything else! 😊", "seconds": 8.9, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 726, "answer": "The locker code is **4805**. Let me know if you need anything else! 😊", "seconds": 23.26}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 763, "answer": "The traffic was unreal this morning. Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 12.69}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 809, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 18.35}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 854, "answer": "The train leaves from **platform 58**. Let me know if you need anything else! 😊", "seconds": 11.41, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 891, "answer": "The street was repaved. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 13.2}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 949, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.33}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 997, "answer": "The dentist moved you to **7:15 PM**. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 11.4, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 1063, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 9.5}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 1107, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.51}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 1150, "answer": "The dentist slot was moved to **7:15 PM**. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 13.88, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": true}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 1220, "answer": "The doctor changed the dose to **27 mg**. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 11.3, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1285, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 16.49}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1326, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 12.75}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 1377, "answer": "The couch is ordered as **ZT6045**. Let me know if you need anything else! 😊", "seconds": 7.88, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1417, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.78}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1460, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.96}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1512, "answer": "The plumber's quote is now **$4,000**. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 23.26, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1585, "answer": "The landlord wants rent to be paid by **day 97** each month. Let me know if you need further assistance! 😊", "seconds": 14.17, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1629, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 15.85}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1671, "answer": "The traffic was unreal this morning. Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 11.08}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 1718, "answer": "The room number is **411**. Let me know if you need anything else! 😊", "seconds": 11.08, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1758, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.53}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1800, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.66}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 1842, "answer": "I'm glad you're enjoying the weather! Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 14.22}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 1888, "answer": "The boarding is at **Gate E17**. Let me know if you need anything else! 😊", "seconds": 11.3, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1925, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.17}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1968, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 9.75}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 2019, "answer": "The flight is AL7371.", "seconds": 7.96, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2043, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.39}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 2095, "answer": "The flight is now **BL4905**. Let me know if you need anything else! 😊", "seconds": 8.43, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2133, "answer": "I'm sorry, but I don't have a code or amount to reference. Could you please provide the specific code or amount you're referring to? 😊", "seconds": 11.7}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 2185, "answer": "The rental has a plate number of **HJC-988**. Let me know if you need anything else! 😊", "seconds": 8.05, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 2228, "answer": "I'm sorry, but I don't have a code or amount to reference. Could you please provide the specific code or amount you're referring to? 😊", "seconds": 11.07}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2274, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.79}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2316, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.85}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2357, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 9.32}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2403, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.44}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2449, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.89}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2494, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 13.07}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2536, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 15.01}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2578, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 14.12}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2620, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 18.2}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 2666, "answer": "The cabin's WiFi password is **harbor858**. Let me know if you need anything else! 😊", "seconds": 18.89, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": true}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2705, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 19.44}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2747, "answer": "The street was repaved. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 16.64}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 2805, "answer": "The street was repaved. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 22.18}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 2864, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 12.83}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2906, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 17.67}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2948, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.21}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2989, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 13.41}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 3032, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 12.75}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3078, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.96}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 3120, "answer": "The street was repaved. The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 12.0}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 3182, "answer": "The plumber's quote is now **$4,000**. Let me know if you need anything else! 😊", "seconds": 15.18, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": true}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 3223, "answer": "The traffic was unreal this morning. Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 11.76}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 3273, "answer": "The parking spot is now **E16**. Let me know if you need anything else! 😊", "seconds": 10.55, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 3310, "answer": "I'm glad you're enjoying the weather! Let me know if there's anything specific you'd like to ask or need help with! 😊", "seconds": 10.43}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 3353, "answer": "I'm glad you're enjoying the weather! Let me know if there's anything sweet you'd like to try or need help with! 😊", "seconds": 12.69}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3401, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 9.23}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3443, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 14.04}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 3488, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.2}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3532, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 13.1}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3586, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 9.34}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3640, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 11.13}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 3697, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to?", "seconds": 18.73, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3737, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 10.29}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3789, "answer": "I'm sorry, but I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.15}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 3836, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 12.65}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 3888, "answer": "The plumber's quote is now **$1,400**. Let me know if you need anything else! 😊", "seconds": 8.2, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 3931, "answer": "I don't have a code or", "seconds": 38.62}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3953, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 8.41}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 3994, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 15.8}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 4047, "answer": "The locker code is **4805**, and the deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 10.16}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 4102, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 11.88}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 4143, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.39}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 4189, "answer": "The deposit for the venue was **$6,400**. Let me know if you need anything else! 😊", "seconds": 10.88, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": true}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 4230, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 10.12}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 4271, "answer": "I'm glad you're enjoying the weather! Let me know if there's anything sweet you'd like to try or need help with! 😊", "seconds": 13.71}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 4319, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 17.57}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 4364, "answer": "I don't have a code or amount to reference. Could you please provide the specific code and amount you're referring to? 😊", "seconds": 13.63, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 4407, "answer": "The hotel room code is **4805**. Let me know if you need anything else! 😊", "seconds": 29.88, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 4450, "answer": "The WiFi password is **harbor858**. Let me know if you need help with anything else! 😊", "seconds": 9.93, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": true}
|
bench/results-smollm2-360m-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 61, "answer": "The gym locker is code 4805.", "seconds": 1.94, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 89, "answer": "They repaved our street.", "seconds": 0.4}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 111, "answer": "What a week, honestly.", "seconds": 0.39}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 139, "answer": "Cabin WiFi Password: Harbor 858.", "seconds": 0.62, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 167, "answer": "They repaved our street.", "seconds": 0.3}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 191, "answer": "The locker code is 4805.", "seconds": 0.74, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 218, "answer": "Finished that book.", "seconds": 0.59}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 242, "answer": "We always park at spot B19.", "seconds": 0.54, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 278, "answer": "The locker code is now code 1688.", "seconds": 0.99, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 308, "answer": "The cat knocked over a plant.", "seconds": 0.6}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 338, "answer": "The deposit for the venue was $6,400.", "seconds": 0.97, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 369, "answer": "My phone battery dies so fast now.", "seconds": 0.78}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 394, "answer": "Saw a great documentary yesterday.", "seconds": 0.64}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 417, "answer": "I love this weather lately.", "seconds": 0.44}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 444, "answer": "Tomas' new teacher is Ms. Duarte.", "seconds": 0.56, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 474, "answer": "I caught the game last night.", "seconds": 0.39}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 499, "answer": "The deposit is $6,400.", "seconds": 0.72, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": true}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 533, "answer": "The plumber quoted $4,900 for the bathroom.", "seconds": 1.03, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 565, "answer": "My phone battery dies so fast now.", "seconds": 0.64}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 589, "answer": "Traffic was unreal this morning.", "seconds": 0.61}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 616, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.75}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 645, "answer": "Our train leaves from platform 58.", "seconds": 0.63, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 670, "answer": "They repaved our street.", "seconds": 0.34}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 694, "answer": "Coffee here is getting expensive.", "seconds": 0.36}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 723, "answer": "The dentist moved me to 7:15pm.", "seconds": 0.65, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 752, "answer": "I saw a great documentary yesterday.", "seconds": 0.47}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 777, "answer": "My phone battery dies so fast now.", "seconds": 0.38}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 802, "answer": "The dentist slot is 7:15pm.", "seconds": 0.57, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": true}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 833, "answer": "Doctor changed the dose to 27mg.", "seconds": 0.54, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 860, "answer": "Maybe we could go for a walk later.", "seconds": 0.52}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 885, "answer": "Finished that book.", "seconds": 0.31}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 915, "answer": "The couch is order ZT6045, keep it handy.", "seconds": 0.79, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 946, "answer": "What a week, honestly.", "seconds": 0.3}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 969, "answer": "My back hurts from the gym.", "seconds": 0.33}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1003, "answer": "The plumber quote is now $4,000.", "seconds": 0.81, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1038, "answer": "Landlord wants rent by day 97 each month.", "seconds": 0.68, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1067, "answer": "The cat knocked over a plant.", "seconds": 0.42}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1090, "answer": "Traffic was unreal this morning.", "seconds": 0.37}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 1118, "answer": "We got room 411 at the hotel.", "seconds": 0.54, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1149, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.51}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1177, "answer": "Coffee here is getting expensive.", "seconds": 0.39}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 1200, "answer": "I love this weather lately.", "seconds": 0.36}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 1224, "answer": "Bearing in mind the weather, I would say that boarding is at gate E17.", "seconds": 1.06, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1259, "answer": "What a week, honestly.", "seconds": 0.37}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1282, "answer": "The cat knocked over a plant.", "seconds": 0.38}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 1314, "answer": "My sister lands on flight AL7371.", "seconds": 0.69, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1343, "answer": "Coffee here is getting expensive.", "seconds": 0.45}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 1376, "answer": "The sister flight is flight BL4905.", "seconds": 0.6, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1405, "answer": "Coffee here is getting expensive.", "seconds": 0.43}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 1433, "answer": "The rental has plate HJC-988.", "seconds": 0.68, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1461, "answer": "My back hurts from the gym.", "seconds": 0.45}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1484, "answer": "I finally finished that book.", "seconds": 0.48}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1508, "answer": "Coffee here is getting expensive.", "seconds": 0.43}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 1530, "answer": "Work was chaos today.", "seconds": 0.28}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1555, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.73}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1585, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.75}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 1614, "answer": "I didn't catch the game last night.", "seconds": 0.57}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1639, "answer": "Traffic was unreal this morning.", "seconds": 0.43}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1662, "answer": "Traffic was unreal this morning.", "seconds": 0.48}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1685, "answer": "What a week, honestly.", "seconds": 0.47}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 1712, "answer": "The wifi password was $4,000.", "seconds": 0.8, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": false}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1740, "answer": "I finally finished that book.", "seconds": 0.49}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1762, "answer": "They repaved our street.", "seconds": 0.41}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1784, "answer": "They repaved our street.", "seconds": 0.44}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 1807, "answer": "I saw a great documentary yesterday.", "seconds": 0.59}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1830, "answer": "What a week, honestly.", "seconds": 0.5}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 1852, "answer": "I think you should make pasta tonight.", "seconds": 0.55}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 1875, "answer": "Work was chaos today.", "seconds": 0.4}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1897, "answer": "Maybe we could go for a walk later.", "seconds": 0.69}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1926, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.67}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1952, "answer": "They repaved our street.", "seconds": 0.4}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 1979, "answer": "The plumber quote was $4,000.", "seconds": 1.0, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": true}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 2007, "answer": "Traffic was unreal this morning.", "seconds": 0.45}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 2038, "answer": "The parking spot is spot E16.", "seconds": 0.57, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 2063, "answer": "I love this weather lately.", "seconds": 0.45}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2084, "answer": "I crave something sweet.", "seconds": 0.34}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2109, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.75}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2135, "answer": "I think you should make pasta tonight.", "seconds": 0.58}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2162, "answer": "I didn't catch the game last night.", "seconds": 0.65}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2189, "answer": "My phone battery dies so fast now.", "seconds": 0.62}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2215, "answer": "My phone battery dies so fast now.", "seconds": 0.63}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2241, "answer": "My phone battery dies so fast now.", "seconds": 0.62}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2270, "answer": "I forgot the rent due, what was it?", "seconds": 0.81, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2296, "answer": "I think you should make pasta tonight.", "seconds": 0.52}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2322, "answer": "Coffee here is getting expensive.", "seconds": 0.43}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 2345, "answer": "I think you should make pasta tonight.", "seconds": 0.7}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 2380, "answer": "The plumber quote is now $1,400.", "seconds": 0.78, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 2411, "answer": "My phone battery dies so fast now.", "seconds": 0.46}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2437, "answer": "Coffee here is getting expensive.", "seconds": 0.49}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 2460, "answer": "I finally finished that book.", "seconds": 0.47}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 2483, "answer": "My back hurts from the gym.", "seconds": 0.41}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 2509, "answer": "I didn't catch the game last night.", "seconds": 0.6}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 2533, "answer": "Work was chaos today.", "seconds": 0.39}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2558, "answer": "I forgot the deposit, what was it?", "seconds": 0.62, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": false}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2585, "answer": "Coffee here is getting expensive.", "seconds": 0.42}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 2607, "answer": "I crave something sweet.", "seconds": 0.31}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2632, "answer": "The neighbor is renovating again, so loud.", "seconds": 0.6}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 2661, "answer": "I remember the hotel room.", "seconds": 0.4, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 2684, "answer": "I remember the hotel room.", "seconds": 0.41, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 2712, "answer": "I forgot the wifi password, what was it?", "seconds": 0.87, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": false}
|
bench/results-tinyllama-11b-full.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"turn": 0, "kind": "fact", "user": "the gym locker is code 4805", "notes": [], "ledger": [], "prefix_tokens": 73, "answer": "The gym locker code is 4805.", "seconds": 1.71, "key": "locker code", "value": "code 4805"}
|
| 2 |
+
{"turn": 1, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 108, "answer": "The street that was repaved is 4806.", "seconds": 0.46}
|
| 3 |
+
{"turn": 2, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 144, "answer": "Yes, it was a busy week for you! I hope you had a chance to enjoy some downtime and relax. Have a great weekend!", "seconds": 1.85}
|
| 4 |
+
{"turn": 3, "kind": "fact", "user": "cabin wifi password is harbor858", "notes": [], "ledger": [], "prefix_tokens": 204, "answer": "The cabin wifi password is harbor858.", "seconds": 0.5, "key": "wifi password", "value": "harbor858"}
|
| 5 |
+
{"turn": 4, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 238, "answer": "The street that was repaved is 4806.", "seconds": 0.46}
|
| 6 |
+
{"turn": 5, "kind": "probe", "user": "quick — the locker code?", "notes": [], "ledger": [], "prefix_tokens": 275, "answer": "Yes, the locker code is 4805.", "seconds": 0.53, "key": "locker code", "expected": "code 4805", "distance": 5, "hit": true}
|
| 7 |
+
{"turn": 6, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 310, "answer": "Yes, you finished that book! I'm glad you enjoyed it. Have a great day!", "seconds": 0.94}
|
| 8 |
+
{"turn": 7, "kind": "fact", "user": "we always park at spot B19", "notes": [], "ledger": [], "prefix_tokens": 355, "answer": "Yes, you always park at spot B19. I hope you find a parking spot that's convenient for you. Have a great day!", "seconds": 1.22, "key": "parking spot", "value": "spot B19"}
|
| 9 |
+
{"turn": 8, "kind": "correction", "user": "actually scratch that, the locker code is code 1688 now", "notes": [], "ledger": [], "prefix_tokens": 420, "answer": "The locker code is now 1688.", "seconds": 0.48, "key": "locker code", "value": "code 1688"}
|
| 10 |
+
{"turn": 9, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 456, "answer": "The cat knocked over a plant.", "seconds": 0.34}
|
| 11 |
+
{"turn": 10, "kind": "fact", "user": "the deposit for the venue was $6,400", "notes": [], "ledger": [], "prefix_tokens": 495, "answer": "The deposit for the venue is $6,400.", "seconds": 0.6, "key": "deposit", "value": "$6,400"}
|
| 12 |
+
{"turn": 11, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 534, "answer": "Yes, that's a common issue with modern phones. I hope you have a backup battery or a charger to keep your phone charged. Have a great day!", "seconds": 1.29}
|
| 13 |
+
{"turn": 12, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 593, "answer": "Yes, I watched a great documentary yesterday. It was called \"The Social Dilemma\" and it's a must-watch for anyone interested in social media and its impact on society. Have a great day!", "seconds": 2.2}
|
| 14 |
+
{"turn": 13, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 662, "answer": "Yes, I've been enjoying the warm weather lately. Have a great day!", "seconds": 0.69}
|
| 15 |
+
{"turn": 14, "kind": "fact", "user": "tomas' new teacher is ms. duarte", "notes": [], "ledger": [], "prefix_tokens": 708, "answer": "Yes, Tomas' new teacher is Ms. Duarte. I hope you find her to be a great teacher. Have a great day!", "seconds": 1.24, "key": "kid teacher", "value": "ms. duarte"}
|
| 16 |
+
{"turn": 15, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 763, "answer": "Yes, I did catch the game last night. The Red Sox won, but it was a close game. Have a great day!", "seconds": 1.17}
|
| 17 |
+
{"turn": 16, "kind": "probe", "user": "remind me of the deposit?", "notes": [], "ledger": [], "prefix_tokens": 816, "answer": "Sure! The deposit for the venue is $6,400.", "seconds": 0.92, "key": "deposit", "expected": "$6,400", "distance": 6, "hit": true}
|
| 18 |
+
{"turn": 17, "kind": "fact", "user": "the plumber quoted $4,900 for the bathroom", "notes": [], "ledger": [], "prefix_tokens": 865, "answer": "Yes, that's correct. The plumber quoted $4,900 for the bathroom.", "seconds": 1.15, "key": "plumber quote", "value": "$4,900"}
|
| 19 |
+
{"turn": 18, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 911, "answer": "Yes, that's a common issue with modern phones. I hope you have a backup battery or a charger to keep your phone charged. Have a great day!", "seconds": 1.53}
|
| 20 |
+
{"turn": 19, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 970, "answer": "Yes, traffic was unreal this morning. It was a nightmare getting to work. Have a great day!", "seconds": 1.17}
|
| 21 |
+
{"turn": 20, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 1020, "answer": "Yes, the neighbor is renovating again. It's a bit noisy, but it's not too bad. Have a great day!", "seconds": 1.26}
|
| 22 |
+
{"turn": 21, "kind": "fact", "user": "our train leaves from platform 58", "notes": [], "ledger": [], "prefix_tokens": 1075, "answer": "Yes, our train leaves from platform 58. I hope you're on the right train. Have a great day!", "seconds": 1.09, "key": "train platform", "value": "platform 58"}
|
| 23 |
+
{"turn": 22, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 1123, "answer": "Yes, they repaved our street. It's been a while since the last time they did it. Have a great day!", "seconds": 1.15}
|
| 24 |
+
{"turn": 23, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 1174, "answer": "Yes, coffee here is getting expensive. I've been buying it at a local coffee shop for a while now. Have a great day!", "seconds": 1.09}
|
| 25 |
+
{"turn": 24, "kind": "fact", "user": "dentist moved me to 7:15pm", "notes": [], "ledger": [], "prefix_tokens": 1232, "answer": "Yes, the dentist moved you to 7:15pm. I hope you're not too late. Have a great day!", "seconds": 1.23, "key": "dentist slot", "value": "7:15pm"}
|
| 26 |
+
{"turn": 25, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 1285, "answer": "Yes, I watched a great documentary yesterday. It was called \"The Social Dilemma\" and it's a must-watch for anyone interested in social media and its impact on society. Have a great day!", "seconds": 2.12}
|
| 27 |
+
{"turn": 26, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 1355, "answer": "Yes, that's a common issue with modern phones. I hope you have a backup battery or a charger to keep your phone charged. Have a great day!", "seconds": 1.36}
|
| 28 |
+
{"turn": 27, "kind": "probe", "user": "quick — the dentist slot?", "notes": [], "ledger": [], "prefix_tokens": 1414, "answer": "Yes, the dentist slot is 7:15pm. I hope you're not too late. Have a great day!", "seconds": 1.26, "key": "dentist slot", "expected": "7:15pm", "distance": 3, "hit": true}
|
| 29 |
+
{"turn": 28, "kind": "fact", "user": "doctor changed the dose to 27mg", "notes": [], "ledger": [], "prefix_tokens": 1471, "answer": "Yes, the doctor changed the dose to 27mg. I hope you're taking your medication as prescribed. Have a great day!", "seconds": 1.22, "key": "meds dose", "value": "27mg"}
|
| 30 |
+
{"turn": 29, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 1528, "answer": "Yes, you might go for a walk later. It's a beautiful day outside, and I'm sure you'll enjoy a leisurely stroll. Have a great day!", "seconds": 1.52}
|
| 31 |
+
{"turn": 30, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 1590, "answer": "Yes, you finished that book! I'm glad you enjoyed it. Have a great day!", "seconds": 1.01}
|
| 32 |
+
{"turn": 31, "kind": "fact", "user": "the couch is order ZT6045, keep it handy", "notes": [], "ledger": [], "prefix_tokens": 1643, "answer": "Yes, the couch is the ZT6045. I hope you find it comfortable and convenient to sit on. Have a great day!", "seconds": 1.23, "key": "order number", "value": "order ZT6045"}
|
| 33 |
+
{"turn": 32, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 1697, "answer": "Yes, it was a busy week for you! I hope you had a chance to enjoy some downtime and relax. Have a great weekend!", "seconds": 1.16}
|
| 34 |
+
{"turn": 33, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 1753, "answer": "Yes, your back probably hurt from the gym. I hope you're able to rest it and avoid any further pain. Have a great day!", "seconds": 1.17}
|
| 35 |
+
{"turn": 34, "kind": "correction", "user": "actually scratch that, the plumber quote is $4,000 now", "notes": [], "ledger": [], "prefix_tokens": 1818, "answer": "Yes, the plumber's quote is $4,000 now. I hope you're able to find a more affordable option. Have a great day!", "seconds": 1.21, "key": "plumber quote", "value": "$4,000"}
|
| 36 |
+
{"turn": 35, "kind": "fact", "user": "landlord wants rent by day 97 each month", "notes": [], "ledger": [], "prefix_tokens": 1883, "answer": "Yes, the landlord wants rent by day 97 each month. I hope you're able to pay it on time and avoid any late fees. Have a great day!", "seconds": 1.57, "key": "rent due", "value": "day 97"}
|
| 37 |
+
{"turn": 36, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 1946, "answer": "Yes, the cat knocked over a plant. I hope you're able to replace it without any damage. Have a great day!", "seconds": 1.19}
|
| 38 |
+
{"turn": 37, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 1998, "answer": "Yes, traffic was unreal this morning. It was a nightmare getting to work. Have a great day!", "seconds": 1.04}
|
| 39 |
+
{"turn": 38, "kind": "fact", "user": "we got room 411 at the hotel", "notes": [], "ledger": [], "prefix_tokens": 2049, "answer": "Yes, we got room 411 at the hotel. I hope you're able to find a comfortable and convenient room. Have a great day!", "seconds": 1.47, "key": "hotel room", "value": "room 411"}
|
| 40 |
+
{"turn": 39, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 2107, "answer": "Yes, the neighbor is renovating again. It's a bit noisy, but it's not too bad. Have a great day!", "seconds": 1.31}
|
| 41 |
+
{"turn": 40, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2161, "answer": "Yes, coffee here is getting expensive. I've been buying it at a local coffee shop for a while now. Have a great day!", "seconds": 1.27}
|
| 42 |
+
{"turn": 41, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 2214, "answer": "Yes, I love this weather lately. Have a great day. Have,coffee here is getting expensive.coffee here is getting expensive.coffee here, have a great coffee.", "seconds": 1.72}
|
| 43 |
+
{"turn": 42, "kind": "fact", "user": "boarding is at gate E17", "notes": [], "ledger": [], "prefix_tokens": 2281, "answer": "I'm here, coffee|assistant,covers,coffee|coffee, coffee|coffice,coffice\nGetting avenue\n\nYes, coffee|coiling.\n|assistant, coffee|assistant, have,cofficing,ining, \"me, andcold, andcold,fficing, you, apartment, andffing,cofficing, andffs,ffic, no for a, noffic,ffic,oport,ffic,1,going,fficing,apple,fficing,", "seconds": 4.87, "key": "boarding gate", "value": "gate E17"}
|
| 44 |
+
{"turn": 43, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 2424, "answer": "forfonanton16ppofffficmingmingfficantonfficfficppingming,apfficantonponapper", "seconds": 0.94}
|
| 45 |
+
{"turn": 44, "kind": "distractor", "user": "the cat knocked over a plant", "notes": [], "ledger": [], "prefix_tokens": 2470, "answer": "canapping", "seconds": 0.16}
|
| 46 |
+
{"turn": 45, "kind": "fact", "user": "my sister lands on flight AL7371, write that down", "notes": [], "ledger": [], "prefix_tokens": 2503, "answer": "todayppingdingmentutordersmingdersaclesftdersmingmingmingdersdersmerdersmingmingminggercedderspping", "seconds": 0.9, "key": "sister flight", "value": "flight AL7371"}
|
| 47 |
+
{"turn": 46, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2552, "answer": "stringringmentvoiceppergerpping tonmingcespspspsppedged1111dingcedcedments or1 ormentmentmentficments", "seconds": 1.23}
|
| 48 |
+
{"turn": 47, "kind": "correction", "user": "actually scratch that, the sister flight is flight BL4905 now", "notes": [], "ledger": [], "prefix_tokens": 2617, "answer": "motoriesumspping", "seconds": 0.26, "key": "sister flight", "value": "flight BL4905"}
|
| 49 |
+
{"turn": 48, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 2645, "answer": "199meddeddedums forirsSidenoteSidenotegeddersdersatorator spocedming spocedgedgedgeddersdersmingmingmingmentmingmingmingming referring sentenceppingshesirs9mingders\rusinghesdingdingsteridesirsirsirsirsirsorsors7111 orums or dates orumsoriesumsumsumsidingdingmentums withums91 with mention with7777 myponppings111st letterppings witheing with199777 dutponponppings7 stirsSidenoteSidenoteSidenoteSidenote7", "seconds": 4.56}
|
| 50 |
+
{"turn": 49, "kind": "fact", "user": "the rental has plate HJC-988", "notes": [], "ledger": [], "prefix_tokens": 2797, "answer": "spe fine instant instant instant\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n97\n\n\n\nmy\n\n\n\nmy\nmy\n\n\n\n\nmy my fine my my\nmy relaxing my my my my is is my fine myting my my\n\n\n\n\nusing\n\n\n\nusing my my my my my instant my instant\n\nmy locked my\n\n\n\nusing my my\n\n\n\n\n\n\n\n\n\nlo,", "seconds": 4.75, "key": "car plate", "value": "HJC-988"}
|
| 51 |
+
{"turn": 50, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 2928, "answer": "rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep\n\n\n\n\n\n\n\n\nrep rep rep rep rep rep p rep p rep rep rep rep\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nsing p", "seconds": 4.73}
|
| 52 |
+
{"turn": 51, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 3043, "answer": "my the my p p p p my p, my p rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep\r\r\r\r\r rep rep rep rep rep rep rep rep rep rep rep rep rep", "seconds": 4.7}
|
| 53 |
+
{"turn": 52, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 3119, "answer": "pent, my p rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep rep\r\r\r\r\r rep rep rep rep rep rep rep rep rep rep rep rep rep rep my rep rep har har har har har har har p rep p rep har har har har har har har har har har my mytes my my my my my rep my har har har p rep har har har har p p p my har p har p p p p p p p p p har p har p p p p p p p p p p p p p", "seconds": 5.37}
|
| 54 |
+
{"turn": 53, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 3261, "answer": "p p p p p p p p p p p p har p p to to p p har har har har p p p p har p p p my har my har my har my har har har har har har har har har har p p har har har har har har har har har har p p har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har", "seconds": 5.01}
|
| 55 |
+
{"turn": 54, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3407, "answer": "har har har har har har har har har har har har har har har har har har the the har the the my the har har har har the my har har har har har har har har har har my har har har har har har har har har har har har my the har har har har har the har the the the har har har har har har har the har har har har har my har har har har har the har the har har har har har har har har har har the har har har har har har har har my my my har har har har har har har har har har", "seconds": 4.59}
|
| 56 |
+
{"turn": 55, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 3553, "answer": "har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har for the har the the the the har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har har the the the the", "seconds": 5.27}
|
| 57 |
+
{"turn": 56, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 3698, "answer": "", "seconds": 0.25}
|
| 58 |
+
{"turn": 57, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 3722, "answer": "", "seconds": 0.17}
|
| 59 |
+
{"turn": 58, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 3746, "answer": "the the the the the the the my", "seconds": 0.53}
|
| 60 |
+
{"turn": 59, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 3777, "answer": "", "seconds": 0.17}
|
| 61 |
+
{"turn": 60, "kind": "probe", "user": "wait, what was the wifi password again?", "notes": [], "ledger": [], "prefix_tokens": 3804, "answer": "my har the the the the the the my the the the", "seconds": 0.73, "key": "wifi password", "expected": "harbor858", "distance": 57, "hit": false}
|
| 62 |
+
{"turn": 61, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 3838, "answer": "", "seconds": 0.18}
|
| 63 |
+
{"turn": 62, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 3860, "answer": "*** my my my my the the the the the the the the the the the the the the the my my my the the the the the the the the the the", "seconds": 1.42}
|
| 64 |
+
{"turn": 63, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 3915, "answer": "the the har har the the the the har har har har the the the har the har the har the the the har har the the the the har har the the the the the the the the har the the the the the the the the the the the the the har the har the har the the har the har the har har har har the the the the the the the the the the the the the the the the har the s the s the the the s the p s the s my s the p the the the the the the the the the", "seconds": 4.97}
|
| 65 |
+
{"turn": 64, "kind": "distractor", "user": "saw a great documentary yesterday", "notes": [], "ledger": [], "prefix_tokens": 4051, "answer": "", "seconds": 0.25}
|
| 66 |
+
{"turn": 65, "kind": "distractor", "user": "what a week, honestly", "notes": [], "ledger": [], "prefix_tokens": 4074, "answer": "the cab the har the har the the my the the the the the har the the har the har the the the the the cab cab har", "seconds": 1.46}
|
| 67 |
+
{"turn": 66, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 4127, "answer": "the the the the the har", "seconds": 0.42}
|
| 68 |
+
{"turn": 67, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 4155, "answer": "", "seconds": 0.2}
|
| 69 |
+
{"turn": 68, "kind": "distractor", "user": "might go for a walk later", "notes": [], "ledger": [], "prefix_tokens": 4179, "answer": "my my my my my my my my my my my my my my my my my my my", "seconds": 0.97}
|
| 70 |
+
{"turn": 69, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 4224, "answer": "", "seconds": 0.2}
|
| 71 |
+
{"turn": 70, "kind": "distractor", "user": "they repaved our street", "notes": [], "ledger": [], "prefix_tokens": 4246, "answer": "", "seconds": 0.21}
|
| 72 |
+
{"turn": 71, "kind": "probe", "user": "wait, what was the plumber quote again?", "notes": [], "ledger": [], "prefix_tokens": 4273, "answer": "", "seconds": 0.21, "key": "plumber quote", "expected": "$4,000", "distance": 37, "hit": false}
|
| 73 |
+
{"turn": 72, "kind": "distractor", "user": "traffic was unreal this morning", "notes": [], "ledger": [], "prefix_tokens": 4297, "answer": "", "seconds": 0.21}
|
| 74 |
+
{"turn": 73, "kind": "correction", "user": "actually scratch that, the parking spot is spot E16 now", "notes": [], "ledger": [], "prefix_tokens": 4329, "answer": "", "seconds": 0.21, "key": "parking spot", "value": "spot E16"}
|
| 75 |
+
{"turn": 74, "kind": "distractor", "user": "i love this weather lately", "notes": [], "ledger": [], "prefix_tokens": 4352, "answer": "", "seconds": 0.21}
|
| 76 |
+
{"turn": 75, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 4373, "answer": "ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven ven scratch ven scratch", "seconds": 1.62}
|
| 77 |
+
{"turn": 76, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 4429, "answer": "", "seconds": 0.22}
|
| 78 |
+
{"turn": 77, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 4454, "answer": "", "seconds": 0.21}
|
| 79 |
+
{"turn": 78, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 4479, "answer": "", "seconds": 0.21}
|
| 80 |
+
{"turn": 79, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 4503, "answer": "", "seconds": 0.21}
|
| 81 |
+
{"turn": 80, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 4527, "answer": "", "seconds": 0.21}
|
| 82 |
+
{"turn": 81, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 4551, "answer": "", "seconds": 0.22}
|
| 83 |
+
{"turn": 82, "kind": "probe", "user": "i forgot the rent due, what was it?", "notes": [], "ledger": [], "prefix_tokens": 4578, "answer": "", "seconds": 0.21, "key": "rent due", "expected": "day 97", "distance": 47, "hit": false}
|
| 84 |
+
{"turn": 83, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 4603, "answer": "", "seconds": 0.21}
|
| 85 |
+
{"turn": 84, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 4627, "answer": "", "seconds": 0.22}
|
| 86 |
+
{"turn": 85, "kind": "distractor", "user": "thinking of making pasta tonight", "notes": [], "ledger": [], "prefix_tokens": 4652, "answer": "", "seconds": 0.22}
|
| 87 |
+
{"turn": 86, "kind": "correction", "user": "actually scratch that, the plumber quote is $1,400 now", "notes": [], "ledger": [], "prefix_tokens": 4686, "answer": "du du du du du du du du du du du du du du du du du du du du", "seconds": 0.93, "key": "plumber quote", "value": "$1,400"}
|
| 88 |
+
{"turn": 87, "kind": "distractor", "user": "my phone battery dies so fast now", "notes": [], "ledger": [], "prefix_tokens": 4730, "answer": "", "seconds": 0.22}
|
| 89 |
+
{"turn": 88, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 4754, "answer": "du du du du du du du du du du du du du du du du du du du", "seconds": 0.9}
|
| 90 |
+
{"turn": 89, "kind": "distractor", "user": "finally finished that book", "notes": [], "ledger": [], "prefix_tokens": 4795, "answer": "", "seconds": 0.23}
|
| 91 |
+
{"turn": 90, "kind": "distractor", "user": "my back hurts from the gym", "notes": [], "ledger": [], "prefix_tokens": 4820, "answer": "", "seconds": 0.22}
|
| 92 |
+
{"turn": 91, "kind": "distractor", "user": "did you catch the game last night?", "notes": [], "ledger": [], "prefix_tokens": 4845, "answer": "du du du du du du du du now m du du du du du du du du du du du m", "seconds": 1.0}
|
| 93 |
+
{"turn": 92, "kind": "distractor", "user": "work was chaos today", "notes": [], "ledger": [], "prefix_tokens": 4889, "answer": "du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du du", "seconds": 1.49}
|
| 94 |
+
{"turn": 93, "kind": "probe", "user": "i forgot the deposit, what was it?", "notes": [], "ledger": [], "prefix_tokens": 4954, "answer": "", "seconds": 0.22, "key": "deposit", "expected": "$6,400", "distance": 83, "hit": false}
|
| 95 |
+
{"turn": 94, "kind": "distractor", "user": "coffee here is getting expensive", "notes": [], "ledger": [], "prefix_tokens": 4978, "answer": "z z z z du z du z z z z z z z z z z z z z z z z z z", "seconds": 1.28}
|
| 96 |
+
{"turn": 95, "kind": "distractor", "user": "craving something sweet", "notes": [], "ledger": [], "prefix_tokens": 5024, "answer": "", "seconds": 0.22}
|
| 97 |
+
{"turn": 96, "kind": "distractor", "user": "the neighbor is renovating again, so loud", "notes": [], "ledger": [], "prefix_tokens": 5050, "answer": "", "seconds": 0.22}
|
| 98 |
+
{"turn": 97, "kind": "probe", "user": "remind me of the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 5075, "answer": "", "seconds": 0.22, "key": "hotel room", "expected": "room 411", "distance": 59, "hit": false}
|
| 99 |
+
{"turn": 98, "kind": "probe", "user": "quick — the hotel room?", "notes": [], "ledger": [], "prefix_tokens": 5098, "answer": "", "seconds": 0.23, "key": "hotel room", "expected": "room 411", "distance": 60, "hit": false}
|
| 100 |
+
{"turn": 99, "kind": "probe", "user": "i forgot the wifi password, what was it?", "notes": [], "ledger": [], "prefix_tokens": 5126, "answer": "", "seconds": 0.22, "key": "wifi password", "expected": "harbor858", "distance": 96, "hit": false}
|
marimo-diffusion-0.6b.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3e8b8fc60b5c27bd8d7299379df6e4f17acf67188ed1f79ac77c2735276a9e8
|
| 3 |
+
size 1192207707
|
src/diffusion_lm/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small masked discrete diffusion language model."""
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
from diffusion_lm.config import ExperimentConfig, ModelConfig, TrainingConfig, load_config
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from diffusion_lm.model import DiffusionTransformer
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"DiffusionTransformer",
|
| 12 |
+
"ExperimentConfig",
|
| 13 |
+
"ModelConfig",
|
| 14 |
+
"TrainingConfig",
|
| 15 |
+
"load_config",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
__version__ = "0.1.0"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def __getattr__(name: str):
|
| 22 |
+
if name == "DiffusionTransformer":
|
| 23 |
+
from diffusion_lm.model import DiffusionTransformer
|
| 24 |
+
|
| 25 |
+
return DiffusionTransformer
|
| 26 |
+
raise AttributeError(name)
|
src/diffusion_lm/chat_server.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenAI-compatible chat endpoint over the reasoning engine.
|
| 2 |
+
|
| 3 |
+
Serves ``/v1/chat/completions`` and ``/v1/models`` so any OpenAI-API chat client can talk to a
|
| 4 |
+
hybrid checkpoint with its own sampler — the model cannot run under llama.cpp-family runtimes,
|
| 5 |
+
whose autoregressive decoding never matches the block-denoising objective.
|
| 6 |
+
|
| 7 |
+
Clients resend the full message history on every request and carry no thinking notes, while the
|
| 8 |
+
training layout replaces messages outside the visible window with the ledger of notes taken on
|
| 9 |
+
them. The server therefore caches each turn's notes keyed by a hash of the exact history that
|
| 10 |
+
produced it: a client that resends history verbatim reconstructs the same ledger the playground
|
| 11 |
+
would hold. On a cache miss (server restart, edited history) the older turns simply contribute
|
| 12 |
+
nothing, which is the playground's restart behaviour as well.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import hashlib
|
| 19 |
+
import json
|
| 20 |
+
import threading
|
| 21 |
+
import time
|
| 22 |
+
import uuid
|
| 23 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
from diffusion_lm.claims import (
|
| 27 |
+
KEEP_MESSAGES,
|
| 28 |
+
SYSTEM,
|
| 29 |
+
chat_prefix,
|
| 30 |
+
ledger_line,
|
| 31 |
+
ledger_notes,
|
| 32 |
+
merge_notes,
|
| 33 |
+
)
|
| 34 |
+
from diffusion_lm.reasoning_playground import ReasoningEngine
|
| 35 |
+
from diffusion_lm.train import resolve_device
|
| 36 |
+
|
| 37 |
+
MAX_ANSWER_TOKENS = 384
|
| 38 |
+
NOTE_CACHE_LIMIT = 4096
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _turn_key(system: str, messages: list[dict[str, str]]) -> str:
|
| 42 |
+
payload = json.dumps(
|
| 43 |
+
[system] + [[m['role'], m['content']] for m in messages],
|
| 44 |
+
ensure_ascii=False, sort_keys=False,
|
| 45 |
+
)
|
| 46 |
+
return hashlib.sha256(payload.encode('utf-8')).hexdigest()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class ChatService:
|
| 50 |
+
"""One engine plus the note cache; generation is serialized on the GPU."""
|
| 51 |
+
|
| 52 |
+
def __init__(self, engine: ReasoningEngine, model_id: str, keep_messages: int) -> None:
|
| 53 |
+
self.engine = engine
|
| 54 |
+
self.model_id = model_id
|
| 55 |
+
self.keep_messages = keep_messages
|
| 56 |
+
self.notes: dict[str, str] = {}
|
| 57 |
+
self.lock = threading.Lock()
|
| 58 |
+
|
| 59 |
+
def _attach_notes(self, system: str, messages: list[dict[str, str]]) -> list[dict[str, str]]:
|
| 60 |
+
attached = []
|
| 61 |
+
for index, message in enumerate(messages):
|
| 62 |
+
entry = {'role': message['role'], 'content': message['content']}
|
| 63 |
+
if message['role'] == 'assistant':
|
| 64 |
+
note = self.notes.get(_turn_key(system, messages[: index + 1]))
|
| 65 |
+
if note:
|
| 66 |
+
entry['note'] = note
|
| 67 |
+
attached.append(entry)
|
| 68 |
+
return attached
|
| 69 |
+
|
| 70 |
+
def build_prefix(self, system: str, messages: list[dict[str, str]]) -> str:
|
| 71 |
+
attached = self._attach_notes(system, messages)
|
| 72 |
+
older = merge_notes(ledger_notes(attached, self.keep_messages))
|
| 73 |
+
window = attached[max(0, len(attached) - self.keep_messages):]
|
| 74 |
+
return chat_prefix(
|
| 75 |
+
[{'role': m['role'], 'content': m['content']} for m in window],
|
| 76 |
+
system=system, extra=ledger_line(older),
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
def remember(self, system: str, messages: list[dict[str, str]],
|
| 80 |
+
answer: str, note: str) -> None:
|
| 81 |
+
if not note:
|
| 82 |
+
return
|
| 83 |
+
if len(self.notes) >= NOTE_CACHE_LIMIT:
|
| 84 |
+
self.notes.clear()
|
| 85 |
+
turn = messages + [{'role': 'assistant', 'content': answer}]
|
| 86 |
+
self.notes[_turn_key(system, turn)] = note
|
| 87 |
+
|
| 88 |
+
def generate(self, system: str, messages: list[dict[str, str]], *, temperature: float,
|
| 89 |
+
top_p: float, max_tokens: int, seed: int):
|
| 90 |
+
"""Yield ``(answer_so_far, note, done)``; the note arrives with the final snapshot."""
|
| 91 |
+
|
| 92 |
+
prefix = self.build_prefix(system, messages)
|
| 93 |
+
blocks: list[tuple[int, str]] = []
|
| 94 |
+
answer = ''
|
| 95 |
+
with self.lock:
|
| 96 |
+
for _, answer, _ in self.engine.stream_chat(
|
| 97 |
+
prefix,
|
| 98 |
+
temperature=temperature,
|
| 99 |
+
# 16 measured optimal on qwen06b-genchat-sft (2026-08-12 five-point sweep):
|
| 100 |
+
# best numeric fidelity, clean doubling, half the latency of 32. Below 8 both
|
| 101 |
+
# fidelity and fluency degrade while latency barely moves.
|
| 102 |
+
steps_per_block=16,
|
| 103 |
+
max_answer_tokens=min(MAX_ANSWER_TOKENS, max_tokens),
|
| 104 |
+
top_p=top_p,
|
| 105 |
+
seed=seed,
|
| 106 |
+
blocks_out=blocks,
|
| 107 |
+
):
|
| 108 |
+
yield answer, '', False
|
| 109 |
+
note = '; '.join(text for _, text in blocks if text)
|
| 110 |
+
self.remember(system, messages, answer.strip(), note)
|
| 111 |
+
yield answer, note, True
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _split_messages(raw: list[dict]) -> tuple[str, list[dict[str, str]]]:
|
| 115 |
+
system = SYSTEM
|
| 116 |
+
messages = []
|
| 117 |
+
for message in raw:
|
| 118 |
+
role, content = message.get('role'), str(message.get('content') or '')
|
| 119 |
+
if role == 'system':
|
| 120 |
+
system = content or system
|
| 121 |
+
elif role in ('user', 'assistant'):
|
| 122 |
+
messages.append({'role': role, 'content': content})
|
| 123 |
+
return system, messages
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class Handler(BaseHTTPRequestHandler):
|
| 127 |
+
service: ChatService
|
| 128 |
+
|
| 129 |
+
def log_message(self, format: str, *args) -> None: # noqa: A002
|
| 130 |
+
pass
|
| 131 |
+
|
| 132 |
+
def _json(self, status: int, body: dict) -> None:
|
| 133 |
+
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
|
| 134 |
+
self.send_response(status)
|
| 135 |
+
self.send_header('Content-Type', 'application/json')
|
| 136 |
+
self.send_header('Content-Length', str(len(data)))
|
| 137 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 138 |
+
self.end_headers()
|
| 139 |
+
self.wfile.write(data)
|
| 140 |
+
|
| 141 |
+
def do_OPTIONS(self) -> None: # noqa: N802
|
| 142 |
+
self.send_response(204)
|
| 143 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 144 |
+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
| 145 |
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
| 146 |
+
self.end_headers()
|
| 147 |
+
|
| 148 |
+
def do_GET(self) -> None: # noqa: N802
|
| 149 |
+
if self.path.rstrip('/') in ('/v1/models', '/models'):
|
| 150 |
+
self._json(200, {'object': 'list', 'data': [
|
| 151 |
+
{'id': self.service.model_id, 'object': 'model', 'owned_by': 'mini-mdlm'},
|
| 152 |
+
]})
|
| 153 |
+
else:
|
| 154 |
+
self._json(404, {'error': 'not found'})
|
| 155 |
+
|
| 156 |
+
def do_POST(self) -> None: # noqa: N802
|
| 157 |
+
if self.path.rstrip('/') not in ('/v1/chat/completions', '/chat/completions'):
|
| 158 |
+
self._json(404, {'error': 'not found'})
|
| 159 |
+
return
|
| 160 |
+
try:
|
| 161 |
+
length = int(self.headers.get('Content-Length', 0))
|
| 162 |
+
request = json.loads(self.rfile.read(length))
|
| 163 |
+
system, messages = _split_messages(request.get('messages') or [])
|
| 164 |
+
if not messages or messages[-1]['role'] != 'user':
|
| 165 |
+
raise ValueError('last message must be from the user')
|
| 166 |
+
except (ValueError, json.JSONDecodeError) as error:
|
| 167 |
+
self._json(400, {'error': {'message': str(error), 'type': 'invalid_request_error'}})
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
temperature = float(request.get('temperature') or 0.8)
|
| 171 |
+
top_p = float(request.get('top_p') or 0.95)
|
| 172 |
+
max_tokens = int(request.get('max_tokens') or MAX_ANSWER_TOKENS)
|
| 173 |
+
seed = int(request.get('seed') or 0)
|
| 174 |
+
stream = bool(request.get('stream'))
|
| 175 |
+
completion_id = f'chatcmpl-{uuid.uuid4().hex[:24]}'
|
| 176 |
+
created = int(time.time())
|
| 177 |
+
|
| 178 |
+
snapshots = self.service.generate(
|
| 179 |
+
system, messages, temperature=temperature, top_p=top_p,
|
| 180 |
+
max_tokens=max_tokens, seed=seed,
|
| 181 |
+
)
|
| 182 |
+
if not stream:
|
| 183 |
+
answer = note = ''
|
| 184 |
+
for answer, note, _ in snapshots:
|
| 185 |
+
pass
|
| 186 |
+
message = {'role': 'assistant', 'content': answer.strip()}
|
| 187 |
+
if note:
|
| 188 |
+
message['reasoning_content'] = note
|
| 189 |
+
self._json(200, {
|
| 190 |
+
'id': completion_id, 'object': 'chat.completion', 'created': created,
|
| 191 |
+
'model': self.service.model_id,
|
| 192 |
+
'choices': [{'index': 0, 'message': message, 'finish_reason': 'stop'}],
|
| 193 |
+
})
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
self.send_response(200)
|
| 197 |
+
self.send_header('Content-Type', 'text/event-stream')
|
| 198 |
+
self.send_header('Cache-Control', 'no-cache')
|
| 199 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 200 |
+
self.end_headers()
|
| 201 |
+
|
| 202 |
+
def chunk(delta: dict, finish: str | None = None) -> bytes:
|
| 203 |
+
body = {
|
| 204 |
+
'id': completion_id, 'object': 'chat.completion.chunk', 'created': created,
|
| 205 |
+
'model': self.service.model_id,
|
| 206 |
+
'choices': [{'index': 0, 'delta': delta, 'finish_reason': finish}],
|
| 207 |
+
}
|
| 208 |
+
return f'data: {json.dumps(body, ensure_ascii=False)}\n\n'.encode('utf-8')
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
self.wfile.write(chunk({'role': 'assistant'}))
|
| 212 |
+
sent = ''
|
| 213 |
+
for answer, _, done in snapshots:
|
| 214 |
+
if len(answer) > len(sent):
|
| 215 |
+
self.wfile.write(chunk({'content': answer[len(sent):]}))
|
| 216 |
+
self.wfile.flush()
|
| 217 |
+
sent = answer
|
| 218 |
+
self.wfile.write(chunk({}, finish='stop'))
|
| 219 |
+
self.wfile.write(b'data: [DONE]\n\n')
|
| 220 |
+
except (BrokenPipeError, ConnectionResetError):
|
| 221 |
+
pass
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def main() -> None:
|
| 225 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 226 |
+
parser.add_argument('--checkpoint', type=Path,
|
| 227 |
+
default=Path('outputs/qwen06b-genchat-sft/inference-latest.pt'))
|
| 228 |
+
parser.add_argument('--tokenizer', type=Path,
|
| 229 |
+
default=Path('artifacts/tokenizer-qwen3-adaptive.json'))
|
| 230 |
+
parser.add_argument('--model-id', default=None,
|
| 231 |
+
help='name reported to clients; defaults to the checkpoint directory')
|
| 232 |
+
parser.add_argument('--keep-messages', type=int, default=KEEP_MESSAGES)
|
| 233 |
+
parser.add_argument('--host', default='127.0.0.1')
|
| 234 |
+
parser.add_argument('--port', type=int, default=7998)
|
| 235 |
+
parser.add_argument('--device', default='auto')
|
| 236 |
+
args = parser.parse_args()
|
| 237 |
+
|
| 238 |
+
device = resolve_device(args.device)
|
| 239 |
+
engine = ReasoningEngine(args.checkpoint, args.tokenizer, device)
|
| 240 |
+
if not engine.chat_ready:
|
| 241 |
+
raise SystemExit(f'{args.checkpoint} is not an adaptive hybrid over a ChatML tokenizer')
|
| 242 |
+
Handler.service = ChatService(
|
| 243 |
+
engine, args.model_id or args.checkpoint.parent.name, args.keep_messages,
|
| 244 |
+
)
|
| 245 |
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
| 246 |
+
print(f'serving {Handler.service.model_id} on http://{args.host}:{args.port}/v1')
|
| 247 |
+
server.serve_forever()
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
if __name__ == '__main__':
|
| 251 |
+
main()
|
src/diffusion_lm/chatcorpus.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render the general-chat corpus into the adaptive training layout.
|
| 2 |
+
|
| 3 |
+
The corpus (``scripts/gen_chat_dataset.py``) stores conversations as system prompt plus turns,
|
| 4 |
+
each assistant turn carrying its own notes. This module turns them into the layout the hybrid
|
| 5 |
+
objective trains on: one example per assistant turn, whose prefix holds the system prompt, the
|
| 6 |
+
merged ledger of notes whose messages have fallen out of the visible window, and the last
|
| 7 |
+
``keep_messages`` messages verbatim.
|
| 8 |
+
|
| 9 |
+
Dropping the older messages is the point. With the full transcript in the prefix the model can
|
| 10 |
+
re-read instead of remember and the thinking block stops being memory, which is what
|
| 11 |
+
:func:`diffusion_lm.claims.to_examples` established for the claims corpus. The window and the
|
| 12 |
+
ledger merge are imported from that module rather than reimplemented, so the prefix a training
|
| 13 |
+
example sees is byte-identical to the one the playground builds at inference.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import random
|
| 21 |
+
from collections import Counter
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
from diffusion_lm.claims import (
|
| 25 |
+
IM_END,
|
| 26 |
+
KEEP_MESSAGES,
|
| 27 |
+
chat_prefix,
|
| 28 |
+
ledger_line,
|
| 29 |
+
ledger_notes,
|
| 30 |
+
merge_notes,
|
| 31 |
+
)
|
| 32 |
+
from diffusion_lm.reasoning import ReasoningExample
|
| 33 |
+
|
| 34 |
+
NOTE_JOIN = '; '
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load(paths: list[Path]) -> list[dict]:
|
| 38 |
+
"""Read consolidated corpus files, tagging each conversation with its origin file."""
|
| 39 |
+
|
| 40 |
+
conversations = []
|
| 41 |
+
for path in paths:
|
| 42 |
+
for line in path.open(encoding='utf-8'):
|
| 43 |
+
if not line.strip():
|
| 44 |
+
continue
|
| 45 |
+
conversation = json.loads(line)
|
| 46 |
+
conversation.setdefault('source', path.stem)
|
| 47 |
+
conversations.append(conversation)
|
| 48 |
+
return conversations
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _messages(conversation: dict) -> list[dict[str, str]]:
|
| 52 |
+
"""Corpus turns in the message shape the ledger helpers expect.
|
| 53 |
+
|
| 54 |
+
A turn's notes collapse into one ``note`` string joined by ``NOTE_JOIN``, which is the
|
| 55 |
+
separator :func:`diffusion_lm.claims.merge_notes` splits on, so a multi-fact turn still
|
| 56 |
+
contributes one ledger entry per fact.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
messages = []
|
| 60 |
+
for turn in conversation.get('turns') or []:
|
| 61 |
+
message = {'role': turn['role'], 'content': turn['content']}
|
| 62 |
+
notes = [str(note).strip() for note in (turn.get('thinking') or []) if str(note).strip()]
|
| 63 |
+
if turn['role'] == 'assistant' and notes:
|
| 64 |
+
message['note'] = NOTE_JOIN.join(notes)
|
| 65 |
+
messages.append(message)
|
| 66 |
+
return messages
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def to_examples(
|
| 70 |
+
conversation: dict, *, keep_messages: int = KEEP_MESSAGES
|
| 71 |
+
) -> list[ReasoningExample]:
|
| 72 |
+
"""One example per assistant turn, each thinking note becoming its own block.
|
| 73 |
+
|
| 74 |
+
A turn the corpus marked as needing no notes yields an empty chain, which is what teaches
|
| 75 |
+
the controller to answer without opening a thinking block; the encoder accepts it.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
messages = _messages(conversation)
|
| 79 |
+
turns = conversation.get('turns') or []
|
| 80 |
+
reference = str(conversation.get('reference') or '')
|
| 81 |
+
last = max((i for i, m in enumerate(messages) if m['role'] == 'assistant'), default=-1)
|
| 82 |
+
|
| 83 |
+
examples = []
|
| 84 |
+
for index, message in enumerate(messages):
|
| 85 |
+
if message['role'] != 'assistant' or index == 0:
|
| 86 |
+
continue
|
| 87 |
+
history = messages[:index]
|
| 88 |
+
older = merge_notes(ledger_notes(history, keep_messages))
|
| 89 |
+
window = history[max(0, index - keep_messages):]
|
| 90 |
+
notes = [str(n).strip() for n in (turns[index].get('thinking') or []) if str(n).strip()]
|
| 91 |
+
examples.append(ReasoningExample(
|
| 92 |
+
problem=chat_prefix(window, system=conversation['system'], extra=ledger_line(older)),
|
| 93 |
+
steps=tuple(notes),
|
| 94 |
+
answer=message['content'] + IM_END,
|
| 95 |
+
expected_answer=reference if index == last else '',
|
| 96 |
+
))
|
| 97 |
+
return examples
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _document(conversation: dict, index: int) -> str:
|
| 101 |
+
"""Split key. Two conversations built from one passage share its facts.
|
| 102 |
+
|
| 103 |
+
Splitting by example would leak within a conversation as well, so the whole conversation
|
| 104 |
+
travels together and grounded slices travel with their source item.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
return str(conversation.get('source_id') or f'{conversation.get("source", "")}-{index}')
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _apply_caps(
|
| 111 |
+
conversations: list[dict], caps: dict[str, int], seed: int
|
| 112 |
+
) -> list[dict]:
|
| 113 |
+
"""Drop conversations so a source contributes at most ``caps[source]`` of them.
|
| 114 |
+
|
| 115 |
+
Capping is by CONVERSATION but the reason is examples: a source's weight in the mix is its
|
| 116 |
+
turn count, not its row count, and the two differ by an order of magnitude (CoQA yields 11.9
|
| 117 |
+
examples per conversation against 1.07 for a single-question source). Sampling is seeded and
|
| 118 |
+
whole conversations travel together, so the split stays document-clean.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
if not caps:
|
| 122 |
+
return conversations
|
| 123 |
+
rng = random.Random(seed)
|
| 124 |
+
by_source: dict[str, list[int]] = {}
|
| 125 |
+
for index, conversation in enumerate(conversations):
|
| 126 |
+
by_source.setdefault(conversation.get('source', ''), []).append(index)
|
| 127 |
+
dropped: set[int] = set()
|
| 128 |
+
for source, limit in caps.items():
|
| 129 |
+
indices = by_source.get(source)
|
| 130 |
+
if indices is None:
|
| 131 |
+
raise ValueError(f'no conversations carry source {source!r}')
|
| 132 |
+
if len(indices) <= limit:
|
| 133 |
+
print(f'cap {source}={limit}: {len(indices)} present, nothing dropped')
|
| 134 |
+
continue
|
| 135 |
+
dropped |= set(indices) - set(rng.sample(indices, limit))
|
| 136 |
+
print(f'cap {source}={limit}: dropped {len(indices) - limit:,} of {len(indices):,}')
|
| 137 |
+
return [c for index, c in enumerate(conversations) if index not in dropped]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _parse_caps(pairs: list[str]) -> dict[str, int]:
|
| 141 |
+
caps = {}
|
| 142 |
+
for pair in pairs:
|
| 143 |
+
source, _, count = pair.partition('=')
|
| 144 |
+
if not count.isdigit():
|
| 145 |
+
raise ValueError(f'--cap expects SOURCE=N, got {pair!r}')
|
| 146 |
+
caps[source] = int(count)
|
| 147 |
+
return caps
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def prepare(args: argparse.Namespace) -> None:
|
| 151 |
+
import numpy as np
|
| 152 |
+
|
| 153 |
+
from diffusion_lm.reasoning import ExampleEncoder, LayoutSpec, _write_packed, size_token_ids
|
| 154 |
+
from diffusion_lm.tokenizer import load_tokenizer
|
| 155 |
+
|
| 156 |
+
tokenizer = load_tokenizer(args.tokenizer)
|
| 157 |
+
spec = LayoutSpec(seq_len=args.seq_len, block=min(args.sizes), max_slots=args.max_slots,
|
| 158 |
+
sizes=tuple(sorted(args.sizes)))
|
| 159 |
+
encoder = ExampleEncoder(tokenizer, spec)
|
| 160 |
+
|
| 161 |
+
conversations = _apply_caps(load(args.inputs), _parse_caps(args.cap or []), args.seed)
|
| 162 |
+
documents = sorted({_document(c, i) for i, c in enumerate(conversations)})
|
| 163 |
+
rng = random.Random(args.seed)
|
| 164 |
+
rng.shuffle(documents)
|
| 165 |
+
held = set(documents[:max(1, round(len(documents) * args.val_fraction))])
|
| 166 |
+
|
| 167 |
+
split: dict[str, list[tuple]] = {'train': [], 'validation': []}
|
| 168 |
+
dropped = 0
|
| 169 |
+
blocks: Counter[int] = Counter()
|
| 170 |
+
empty_chains = 0
|
| 171 |
+
for index, conversation in enumerate(conversations):
|
| 172 |
+
bucket = 'validation' if _document(conversation, index) in held else 'train'
|
| 173 |
+
for example in to_examples(conversation, keep_messages=args.keep_messages):
|
| 174 |
+
encoded = encoder.encode_adaptive(example)
|
| 175 |
+
if encoded is None:
|
| 176 |
+
dropped += 1
|
| 177 |
+
continue
|
| 178 |
+
blocks.update(encoded.block_sizes)
|
| 179 |
+
empty_chains += not encoded.block_sizes
|
| 180 |
+
split[bucket].append((encoded.tokens, encoded.regions))
|
| 181 |
+
|
| 182 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 183 |
+
for name, rows in split.items():
|
| 184 |
+
if not rows:
|
| 185 |
+
raise ValueError(f'no examples in the {name} split')
|
| 186 |
+
_write_packed(
|
| 187 |
+
args.output_dir / f'{name}-adaptive.bin',
|
| 188 |
+
np.stack([tokens for tokens, _ in rows]),
|
| 189 |
+
np.stack([regions for _, regions in rows]),
|
| 190 |
+
layout='adaptive', spec=spec, tokenizer_path=args.tokenizer, tokenizer=tokenizer,
|
| 191 |
+
extra_metadata={
|
| 192 |
+
'sizes': list(spec.sizes),
|
| 193 |
+
# reasoning_train resolves the adaptive control ids from the pack, not the
|
| 194 |
+
# tokenizer, and refuses a pack without them.
|
| 195 |
+
'size_token_ids': size_token_ids(tokenizer, spec.sizes),
|
| 196 |
+
'source': 'general-chat',
|
| 197 |
+
},
|
| 198 |
+
)
|
| 199 |
+
print(f'{name}: {len(rows):,} examples -> {args.output_dir}')
|
| 200 |
+
total = sum(len(rows) for rows in split.values())
|
| 201 |
+
print(f'{len(conversations):,} conversations, {len(documents):,} documents, '
|
| 202 |
+
f'{dropped:,} dropped at encode ({dropped / max(1, dropped + total):.1%})')
|
| 203 |
+
print(f'examples answering with no thinking block: {empty_chains:,} '
|
| 204 |
+
f'({empty_chains / max(1, total):.1%})')
|
| 205 |
+
print('block sizes: ' + ', '.join(f'{size}:{count:,}' for size, count in sorted(blocks.items())))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def main() -> None:
|
| 209 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 210 |
+
sub = parser.add_subparsers(dest='command', required=True)
|
| 211 |
+
|
| 212 |
+
prep = sub.add_parser('prepare', help='pack the corpus into the adaptive layout')
|
| 213 |
+
prep.add_argument('--inputs', type=Path, nargs='+', required=True)
|
| 214 |
+
prep.add_argument('--tokenizer', type=Path,
|
| 215 |
+
default=Path('artifacts/tokenizer-qwen3-adaptive.json'))
|
| 216 |
+
prep.add_argument('--output-dir', type=Path, required=True)
|
| 217 |
+
prep.add_argument('--seq-len', type=int, default=2048)
|
| 218 |
+
prep.add_argument('--sizes', type=int, nargs='+', default=[32, 64, 128])
|
| 219 |
+
prep.add_argument('--max-slots', type=int, default=40)
|
| 220 |
+
prep.add_argument('--keep-messages', type=int, default=KEEP_MESSAGES)
|
| 221 |
+
prep.add_argument('--cap', nargs='*', metavar='SOURCE=N',
|
| 222 |
+
help='keep at most N conversations from a source, e.g. ground-coqa=2273; '
|
| 223 |
+
'weight in the mix is examples, and sources differ ~10x in examples '
|
| 224 |
+
'per conversation')
|
| 225 |
+
prep.add_argument('--val-fraction', type=float, default=0.02)
|
| 226 |
+
prep.add_argument('--seed', type=int, default=1337)
|
| 227 |
+
|
| 228 |
+
args = parser.parse_args()
|
| 229 |
+
prepare(args)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == '__main__':
|
| 233 |
+
main()
|
src/diffusion_lm/claims.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic multi-turn claim intake: dialogues with a known fact set, and a scorer.
|
| 2 |
+
|
| 3 |
+
The target behaviour is memory rather than knowledge: every fact the model must report is
|
| 4 |
+
present in the conversation, so a small model is not being asked to recall the world. Some
|
| 5 |
+
turns supersede an earlier value, which is what separates tracking state from copying the
|
| 6 |
+
last thing seen. Because the record is generated, recall and precision are exact, and no
|
| 7 |
+
judge is needed to score a recap.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import random
|
| 15 |
+
import re
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from diffusion_lm.reasoning import ReasoningExample
|
| 20 |
+
|
| 21 |
+
FIRST_NAMES = ('Marta', 'Diego', 'Luciana', 'Rodrigo', 'Camila', 'Nestor', 'Sofia', 'Ariel')
|
| 22 |
+
LAST_NAMES = ('Quiroga', 'Benitez', 'Salvatierra', 'Uriarte', 'Ferreyra', 'Zabala', 'Otamendi')
|
| 23 |
+
MAKES = (('Peugeot', '208'), ('Toyota', 'Etios'), ('Renault', 'Sandero'), ('Fiat', 'Cronos'),
|
| 24 |
+
('Chevrolet', 'Onix'), ('Volkswagen', 'Gol'))
|
| 25 |
+
STREETS = ('Av. Rivadavia', 'Calle Mitre', 'Ruta 8', 'Av. San Martin', 'Calle Belgrano')
|
| 26 |
+
CITIES = ('Moron', 'Rosario', 'La Plata', 'Cordoba', 'Bahia Blanca', 'Mendoza')
|
| 27 |
+
DAMAGE_PARTS = ('front bumper', 'left headlight', 'driver door', 'rear hatch', 'right mirror',
|
| 28 |
+
'windshield', 'rear bumper')
|
| 29 |
+
WEATHER = ('heavy rain', 'clear skies', 'fog', 'light drizzle')
|
| 30 |
+
FILLERS = ('Sorry, one moment.', 'Are you still there?', 'This is my first claim.',
|
| 31 |
+
'Ok.', 'Thanks for the help.', 'Can you repeat that?')
|
| 32 |
+
ACKS = ('No problem, take your time.', 'Yes, I am here.', 'Understood, please go on.',
|
| 33 |
+
'Of course.', 'Happy to help.')
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class Claim:
|
| 38 |
+
"""One claim record plus the facts a faithful report has to carry."""
|
| 39 |
+
|
| 40 |
+
fields: dict[str, str]
|
| 41 |
+
corrected: dict[str, str] = field(default_factory=dict)
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def truth(self) -> dict[str, str]:
|
| 45 |
+
"""Field values after corrections, which is what a report must state."""
|
| 46 |
+
|
| 47 |
+
resolved = dict(self.fields)
|
| 48 |
+
resolved.update(self.corrected)
|
| 49 |
+
return resolved
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _plate(rng: random.Random) -> str:
|
| 53 |
+
letters = ''.join(rng.choice('ABCDEFGHJKLMNPRSTUVWXYZ') for _ in range(3))
|
| 54 |
+
return f'{letters}-{rng.randint(1000, 9999)}'
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def generate_claim(rng: random.Random) -> Claim:
|
| 58 |
+
"""Build a claim whose values are distinctive enough to score by exact match."""
|
| 59 |
+
|
| 60 |
+
make, model = rng.choice(MAKES)
|
| 61 |
+
fields = {
|
| 62 |
+
'policy_number': f'PL-{rng.randint(10000, 99999)}',
|
| 63 |
+
'claimant': f'{rng.choice(FIRST_NAMES)} {rng.choice(LAST_NAMES)}',
|
| 64 |
+
'incident_date': f'{rng.randint(1, 28):02d}/{rng.randint(1, 12):02d}/2026',
|
| 65 |
+
'incident_time': f'{rng.randint(0, 23):02d}:{rng.choice(("05", "15", "40", "50"))}',
|
| 66 |
+
'location': f'{rng.choice(STREETS)}, {rng.choice(CITIES)}',
|
| 67 |
+
'vehicle': f'{make} {model} {rng.randint(2012, 2025)}',
|
| 68 |
+
'plate': _plate(rng),
|
| 69 |
+
'weather': rng.choice(WEATHER),
|
| 70 |
+
'damage': rng.choice(DAMAGE_PARTS),
|
| 71 |
+
'estimate': f'${rng.randint(2, 40) * 1000 + rng.choice((150, 450, 800)):,}',
|
| 72 |
+
'other_plate': _plate(rng),
|
| 73 |
+
'police_report': f'PR-{rng.randint(100000, 999999)}',
|
| 74 |
+
'witness': f'{rng.choice(FIRST_NAMES)} {rng.choice(LAST_NAMES)}',
|
| 75 |
+
}
|
| 76 |
+
return Claim(fields=fields)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
QUESTIONS = {
|
| 80 |
+
'policy_number': 'Can you give me your policy number?',
|
| 81 |
+
'claimant': 'Who is the policy holder?',
|
| 82 |
+
'incident_date': 'What date did this happen?',
|
| 83 |
+
'incident_time': 'Roughly what time?',
|
| 84 |
+
'location': 'Where exactly did it happen?',
|
| 85 |
+
'vehicle': 'Which vehicle was involved?',
|
| 86 |
+
'plate': "What is your vehicle's plate?",
|
| 87 |
+
'weather': 'How was the weather at the time?',
|
| 88 |
+
'damage': 'What part of the car was damaged?',
|
| 89 |
+
'estimate': 'Do you have a repair estimate?',
|
| 90 |
+
'other_plate': 'Did you get the other vehicle plate?',
|
| 91 |
+
'police_report': 'Was a police report filed?',
|
| 92 |
+
'witness': 'Was there any witness?',
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
ANSWERS = {
|
| 96 |
+
'policy_number': 'My policy is {value}.',
|
| 97 |
+
'claimant': 'The holder is {value}.',
|
| 98 |
+
'incident_date': 'It was on {value}.',
|
| 99 |
+
'incident_time': 'Around {value}.',
|
| 100 |
+
'location': 'On {value}.',
|
| 101 |
+
'vehicle': "It's a {value}.",
|
| 102 |
+
'plate': 'The plate is {value}.',
|
| 103 |
+
'weather': 'There was {value}.',
|
| 104 |
+
'damage': 'The {value} took the hit.',
|
| 105 |
+
'estimate': 'The shop quoted {value}.',
|
| 106 |
+
'other_plate': 'Yes, {value}.',
|
| 107 |
+
'police_report': 'Yes, report {value}.',
|
| 108 |
+
'witness': '{value} saw everything.',
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def render_dialogue(
|
| 113 |
+
claim: Claim, rng: random.Random, *, turns: int = 6, corrections: int = 1,
|
| 114 |
+
chitchat: int = 2,
|
| 115 |
+
) -> list[dict[str, str]]:
|
| 116 |
+
"""Reveal the record across turns, superseding some values along the way.
|
| 117 |
+
|
| 118 |
+
Each turn carries one to three fields, so the amount to remember per turn varies the
|
| 119 |
+
way it would in a real intake.
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
keys = list(claim.fields)
|
| 123 |
+
rng.shuffle(keys)
|
| 124 |
+
# Deliberately uneven: with a constant number of facts per turn every ledger block lands on
|
| 125 |
+
# the same size and the control decision stops carrying information.
|
| 126 |
+
batches: list[list[str]] = []
|
| 127 |
+
while keys:
|
| 128 |
+
take = min(len(keys), rng.choice((1, 1, 2, 3, 4, 5)))
|
| 129 |
+
batches.append(keys[:take])
|
| 130 |
+
keys = keys[take:]
|
| 131 |
+
|
| 132 |
+
messages: list[dict[str, str]] = []
|
| 133 |
+
for batch in batches:
|
| 134 |
+
messages.append({'role': 'assistant', 'content': ' '.join(QUESTIONS[k] for k in batch)})
|
| 135 |
+
said = ' '.join(ANSWERS[k].format(value=claim.fields[k]) for k in batch)
|
| 136 |
+
messages.append({'role': 'user', 'content': said,
|
| 137 |
+
'values': {k: claim.fields[k] for k in batch}})
|
| 138 |
+
|
| 139 |
+
# Turns that carry no new fact, so the assistant answers with zero thinking blocks. A
|
| 140 |
+
# conversation is full of these and without them the model never learns to skip thinking.
|
| 141 |
+
for _ in range(chitchat):
|
| 142 |
+
at = rng.randrange(1, max(2, len(messages)))
|
| 143 |
+
messages.insert(at, {'role': 'assistant', 'content': rng.choice(ACKS)})
|
| 144 |
+
messages.insert(at, {'role': 'user', 'content': rng.choice(FILLERS), 'values': {}})
|
| 145 |
+
|
| 146 |
+
correctable = [k for k in claim.fields if k in ('plate', 'other_plate', 'estimate',
|
| 147 |
+
'incident_time', 'police_report')]
|
| 148 |
+
rng.shuffle(correctable)
|
| 149 |
+
for key in correctable[:corrections]:
|
| 150 |
+
if key.endswith('plate'):
|
| 151 |
+
new = _plate(rng)
|
| 152 |
+
elif key == 'estimate':
|
| 153 |
+
new = f'${rng.randint(2, 40) * 1000 + 700:,}'
|
| 154 |
+
elif key == 'incident_time':
|
| 155 |
+
new = f'{rng.randint(0, 23):02d}:30'
|
| 156 |
+
else:
|
| 157 |
+
new = f'PR-{rng.randint(100000, 999999)}'
|
| 158 |
+
claim.corrected[key] = new
|
| 159 |
+
messages.append({
|
| 160 |
+
'role': 'user',
|
| 161 |
+
'content': f'Sorry, I misspoke earlier: {ANSWERS[key].format(value=new)} '
|
| 162 |
+
f'Not {claim.fields[key]}.',
|
| 163 |
+
'values': {key: new},
|
| 164 |
+
})
|
| 165 |
+
# An assistant turn right after the correction is what makes the update a training
|
| 166 |
+
# target: without it the only place the corrected value is taught is the final report.
|
| 167 |
+
messages.append({'role': 'assistant', 'content': 'Noted, I corrected that detail.'})
|
| 168 |
+
return messages
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
_IDENTIFIERS = re.compile(
|
| 172 |
+
r'(?:P[LR]-\d{5,6}' # policy and police report numbers
|
| 173 |
+
r'|[A-Z]{3}-\d{4}' # plates
|
| 174 |
+
r'|\$[\d,]+' # money
|
| 175 |
+
r'|\d{2}/\d{2}/\d{4}' # dates
|
| 176 |
+
r'|\b\d{2}:\d{2}\b)' # times
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def score_report(text: str, claim: Claim) -> dict[str, object]:
|
| 181 |
+
"""Exact-match recall of the resolved values, plus the two ways a report lies.
|
| 182 |
+
|
| 183 |
+
``stale_kept`` reports a value the conversation replaced, which means the model copied
|
| 184 |
+
instead of tracking state. ``invented`` counts identifier-shaped strings that were never
|
| 185 |
+
said at all, which is confabulation rather than a memory slip; they are different
|
| 186 |
+
failures and worth separating.
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
truth = claim.truth
|
| 190 |
+
found = {key: (value in text) for key, value in truth.items()}
|
| 191 |
+
stale = {
|
| 192 |
+
key: (claim.fields[key] in text)
|
| 193 |
+
for key in claim.corrected
|
| 194 |
+
if claim.fields[key] != claim.corrected[key]
|
| 195 |
+
}
|
| 196 |
+
spoken = set(truth.values()) | set(claim.fields.values())
|
| 197 |
+
said_ids = {token for value in spoken for token in _IDENTIFIERS.findall(value)}
|
| 198 |
+
invented = sorted({token for token in _IDENTIFIERS.findall(text)} - said_ids)
|
| 199 |
+
recalled = sum(found.values())
|
| 200 |
+
return {
|
| 201 |
+
'fields': len(truth),
|
| 202 |
+
'recalled': recalled,
|
| 203 |
+
'recall': recalled / max(1, len(truth)),
|
| 204 |
+
'missing': sorted(k for k, ok in found.items() if not ok),
|
| 205 |
+
'stale_kept': sorted(k for k, bad in stale.items() if bad),
|
| 206 |
+
'invented': invented,
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
LEDGER = '{key}: {value}'
|
| 211 |
+
KEEP_MESSAGES = 4
|
| 212 |
+
IM_START = '<|im_start|>'
|
| 213 |
+
IM_END = '<|im_end|>'
|
| 214 |
+
SYSTEM = (
|
| 215 |
+
'You are a claim intake assistant. Track every detail the customer gives and always use '
|
| 216 |
+
'the corrected value when they correct themselves.'
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def chatml_turn(role: str, content: str) -> str:
|
| 221 |
+
"""One ChatML turn. Both markers are single tokens in the adaptive tokenizer."""
|
| 222 |
+
|
| 223 |
+
return f'{IM_START}{role}\n{content}{IM_END}\n'
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def ledger_line(entries: list[str]) -> str:
|
| 227 |
+
"""The ``Known so far`` system-turn line carrying facts whose messages were dropped."""
|
| 228 |
+
|
| 229 |
+
return 'Known so far: ' + '; '.join(entries) + '.' if entries else ''
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
_NOTE_FACT = re.compile(r'^([^:]{1,48}): (.+)$')
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def merge_notes(notes: list[str]) -> list[str]:
|
| 236 |
+
"""Fold note fragments into one entry per key, latest value winning.
|
| 237 |
+
|
| 238 |
+
Concatenating raw notes would re-expose superseded values, which is the failure the corpus
|
| 239 |
+
charges hardest. Fragments that do not parse as ``key: value`` pass through in order,
|
| 240 |
+
deduplicated verbatim, so an unkeyed reasoning step still reaches the prefix as prose.
|
| 241 |
+
|
| 242 |
+
Both the training renderer and the playground call this. Keeping one implementation is the
|
| 243 |
+
point: a second copy is how the train and inference views of the same history drift apart.
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
facts: dict[str, str] = {}
|
| 247 |
+
loose: list[str] = []
|
| 248 |
+
for note in notes:
|
| 249 |
+
for fragment in note.split('; '):
|
| 250 |
+
fragment = fragment.strip().rstrip('.')
|
| 251 |
+
if not fragment:
|
| 252 |
+
continue
|
| 253 |
+
match = _NOTE_FACT.match(fragment)
|
| 254 |
+
if match:
|
| 255 |
+
facts[match.group(1)] = match.group(2)
|
| 256 |
+
elif fragment not in loose:
|
| 257 |
+
loose.append(fragment)
|
| 258 |
+
return [f'{key}: {value}' for key, value in facts.items()] + loose
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def window_start(messages: list[dict[str, str]], keep: int) -> int:
|
| 262 |
+
return 0 if keep <= 0 else max(0, len(messages) - keep)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def ledger_notes(messages: list[dict[str, str]], keep: int) -> list[str]:
|
| 266 |
+
"""Notes whose user message fell out of the visible window, in turn order.
|
| 267 |
+
|
| 268 |
+
An assistant turn's note enters the ledger exactly when the user message it took notes on
|
| 269 |
+
has fallen out of the window, which is what makes the note the only remaining carrier.
|
| 270 |
+
"""
|
| 271 |
+
|
| 272 |
+
start = window_start(messages, keep)
|
| 273 |
+
return [
|
| 274 |
+
message['note'] for index, message in enumerate(messages)
|
| 275 |
+
if message['role'] == 'assistant' and message.get('note') and index - 1 < start
|
| 276 |
+
]
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def chat_prefix(turns: list[dict[str, str]], *, system: str = SYSTEM, extra: str = '') -> str:
|
| 280 |
+
"""Conversation prefix ending where the assistant's generation begins.
|
| 281 |
+
|
| 282 |
+
The accumulated ledger rides in the system turn rather than as a fake dialogue message:
|
| 283 |
+
it is persistent state, and putting it there also teaches the model to condition on a
|
| 284 |
+
system prompt, which none of the other corpora do.
|
| 285 |
+
"""
|
| 286 |
+
|
| 287 |
+
merged = system if not extra else f'{system}\n{extra}'
|
| 288 |
+
rendered = [chatml_turn('system', merged)]
|
| 289 |
+
rendered += [chatml_turn(turn['role'], turn['content']) for turn in turns]
|
| 290 |
+
return ''.join(rendered) + f'{IM_START}assistant\n'
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _chunks(items: list, size: int) -> list[list]:
|
| 294 |
+
return [items[i:i + size] for i in range(0, len(items), size)]
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def report_text(claim: Claim) -> str:
|
| 298 |
+
"""The report a faithful assistant produces, one resolved field per line."""
|
| 299 |
+
|
| 300 |
+
return 'Claim report. ' + ' '.join(
|
| 301 |
+
LEDGER.format(key=key, value=value) + '.' for key, value in claim.truth.items()
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def to_examples(
|
| 306 |
+
claim: Claim, messages: list[dict[str, str]], *, keep_messages: int = KEEP_MESSAGES
|
| 307 |
+
) -> list[ReasoningExample]:
|
| 308 |
+
"""One training example per assistant turn, with the history deliberately truncated.
|
| 309 |
+
|
| 310 |
+
Older messages are dropped and replaced by the ledger of what they revealed, so the
|
| 311 |
+
accumulated notes — not the transcript — are what carries the past. That is the whole
|
| 312 |
+
point: with the full transcript in the prefix the model can re-read instead of remember,
|
| 313 |
+
and the thinking block stops being memory. A final example asks for the report, whose
|
| 314 |
+
thinking consolidates every fact.
|
| 315 |
+
|
| 316 |
+
Thought steps hold one fact each, which keeps a block's content short and its length a
|
| 317 |
+
function of how much the turn actually revealed.
|
| 318 |
+
"""
|
| 319 |
+
|
| 320 |
+
examples: list[ReasoningExample] = []
|
| 321 |
+
# Values as stated at each point, so a ledger never shows a correction that has not
|
| 322 |
+
# happened yet: training on resolved values would teach the model to know the future.
|
| 323 |
+
known: dict[str, str] = {}
|
| 324 |
+
for index, message in enumerate(messages):
|
| 325 |
+
if message['role'] != 'assistant' or index == 0:
|
| 326 |
+
continue
|
| 327 |
+
previous = messages[index - 1]
|
| 328 |
+
learned = dict(previous.get('values', {}))
|
| 329 |
+
known.update(learned)
|
| 330 |
+
dropped = messages[max(0, index - keep_messages):index]
|
| 331 |
+
seen: dict[str, str] = {}
|
| 332 |
+
for earlier in messages[:max(0, index - keep_messages)]:
|
| 333 |
+
seen.update(earlier.get('values', {}))
|
| 334 |
+
older = [LEDGER.format(key=key, value=value) for key, value in seen.items()]
|
| 335 |
+
examples.append(ReasoningExample(
|
| 336 |
+
problem=chat_prefix(dropped, extra=ledger_line(older)),
|
| 337 |
+
steps=(('; '.join(LEDGER.format(key=key, value=value)
|
| 338 |
+
for key, value in learned.items()),)
|
| 339 |
+
if learned else ()),
|
| 340 |
+
answer=message['content'] + IM_END,
|
| 341 |
+
expected_answer='',
|
| 342 |
+
))
|
| 343 |
+
|
| 344 |
+
tail = messages[-keep_messages:] + [{'role': 'user', 'content': 'Write the claim report.'}]
|
| 345 |
+
examples.append(ReasoningExample(
|
| 346 |
+
problem=chat_prefix(tail),
|
| 347 |
+
steps=tuple(
|
| 348 |
+
'; '.join(LEDGER.format(key=key, value=value) for key, value in group)
|
| 349 |
+
for group in _chunks(list(claim.truth.items()), 8)
|
| 350 |
+
),
|
| 351 |
+
answer=report_text(claim) + IM_END,
|
| 352 |
+
expected_answer='',
|
| 353 |
+
))
|
| 354 |
+
return examples
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def build(count: int, seed: int, turns: int, corrections: int) -> list[dict[str, object]]:
|
| 358 |
+
rng = random.Random(seed)
|
| 359 |
+
records = []
|
| 360 |
+
for index in range(count):
|
| 361 |
+
claim = generate_claim(rng)
|
| 362 |
+
messages = render_dialogue(claim, rng, turns=turns, corrections=corrections)
|
| 363 |
+
records.append({
|
| 364 |
+
'index': index,
|
| 365 |
+
'messages': messages,
|
| 366 |
+
'truth': claim.truth,
|
| 367 |
+
'superseded': {k: claim.fields[k] for k in claim.corrected},
|
| 368 |
+
})
|
| 369 |
+
return records
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def prepare(args: argparse.Namespace) -> None:
|
| 373 |
+
"""Pack dialogues into the adaptive layout, splitting BY DIALOGUE.
|
| 374 |
+
|
| 375 |
+
Splitting by example would leak: two examples from one dialogue share its facts, so a
|
| 376 |
+
validation example's answer would already appear in a training example's prefix.
|
| 377 |
+
"""
|
| 378 |
+
|
| 379 |
+
import numpy as np
|
| 380 |
+
|
| 381 |
+
from diffusion_lm.reasoning import ExampleEncoder, LayoutSpec, _write_packed
|
| 382 |
+
from diffusion_lm.tokenizer import load_tokenizer
|
| 383 |
+
|
| 384 |
+
tokenizer = load_tokenizer(args.tokenizer)
|
| 385 |
+
spec = LayoutSpec(seq_len=args.seq_len, block=min(args.sizes), max_slots=args.max_slots,
|
| 386 |
+
sizes=tuple(sorted(args.sizes)))
|
| 387 |
+
encoder = ExampleEncoder(tokenizer, spec)
|
| 388 |
+
rng = random.Random(args.seed)
|
| 389 |
+
|
| 390 |
+
split: dict[str, list[tuple]] = {'train': [], 'validation': []}
|
| 391 |
+
dropped = 0
|
| 392 |
+
for index in range(args.count):
|
| 393 |
+
claim = generate_claim(rng)
|
| 394 |
+
messages = render_dialogue(claim, rng, turns=args.turns,
|
| 395 |
+
corrections=args.corrections,
|
| 396 |
+
chitchat=args.chitchat)
|
| 397 |
+
bucket = 'validation' if index % args.val_every == 0 else 'train'
|
| 398 |
+
for example in to_examples(claim, messages, keep_messages=args.keep_messages):
|
| 399 |
+
encoded = encoder.encode_adaptive(example)
|
| 400 |
+
if encoded is None:
|
| 401 |
+
dropped += 1
|
| 402 |
+
continue
|
| 403 |
+
split[bucket].append((encoded.tokens, encoded.regions))
|
| 404 |
+
|
| 405 |
+
for name, rows in split.items():
|
| 406 |
+
if not rows:
|
| 407 |
+
raise ValueError(f'no examples in the {name} split')
|
| 408 |
+
_write_packed(
|
| 409 |
+
args.output_dir / f'{name}-adaptive.bin',
|
| 410 |
+
np.stack([tokens for tokens, _ in rows]),
|
| 411 |
+
np.stack([regions for _, regions in rows]),
|
| 412 |
+
layout='adaptive', spec=spec, tokenizer_path=args.tokenizer, tokenizer=tokenizer,
|
| 413 |
+
extra_metadata={'sizes': list(spec.sizes), 'source': 'claims-chatml'},
|
| 414 |
+
)
|
| 415 |
+
print(f'{name}: {len(rows):,} examples -> {args.output_dir}')
|
| 416 |
+
print(f'{dropped:,} dropped at encode')
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def mix(args: argparse.Namespace) -> None:
|
| 420 |
+
"""Concatenate two packs so claims hold ``--claims-share`` of the examples."""
|
| 421 |
+
|
| 422 |
+
import numpy as np
|
| 423 |
+
|
| 424 |
+
from diffusion_lm.reasoning import regions_path
|
| 425 |
+
|
| 426 |
+
rng = np.random.default_rng(args.seed)
|
| 427 |
+
for name in ('train', 'validation'):
|
| 428 |
+
parts = []
|
| 429 |
+
for directory, share in ((args.claims_dir, args.claims_share), (args.other_dir, None)):
|
| 430 |
+
path = directory / f'{name}-adaptive.bin'
|
| 431 |
+
meta = json.loads(Path(str(path) + '.json').read_text())
|
| 432 |
+
tokens = np.fromfile(path, dtype=np.dtype(meta['dtype'])).reshape(
|
| 433 |
+
meta['example_count'], meta['seq_len']
|
| 434 |
+
)
|
| 435 |
+
parts.append((tokens, np.load(regions_path(path)), share))
|
| 436 |
+
(claims_tokens, claims_regions, share), (other_tokens, other_regions, _) = parts
|
| 437 |
+
target = int(round(share / (1.0 - share) * len(other_tokens)))
|
| 438 |
+
if target < len(claims_tokens):
|
| 439 |
+
keep = rng.choice(len(claims_tokens), size=target, replace=False)
|
| 440 |
+
claims_tokens, claims_regions = claims_tokens[keep], claims_regions[keep]
|
| 441 |
+
tokens = np.concatenate([claims_tokens, other_tokens])
|
| 442 |
+
regions = np.concatenate([claims_regions, other_regions])
|
| 443 |
+
order = rng.permutation(len(tokens))
|
| 444 |
+
tokens, regions = tokens[order], regions[order]
|
| 445 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 446 |
+
tokens.tofile(args.output_dir / f'{name}-adaptive.bin')
|
| 447 |
+
np.save(regions_path(args.output_dir / f'{name}-adaptive.bin'), regions)
|
| 448 |
+
meta.update({'example_count': int(len(tokens)), 'source': 'claims+glaive',
|
| 449 |
+
'claims_examples': int(len(claims_tokens)),
|
| 450 |
+
'other_examples': int(len(other_tokens))})
|
| 451 |
+
Path(str(args.output_dir / f'{name}-adaptive.bin') + '.json').write_text(
|
| 452 |
+
json.dumps(meta, indent=2) + '\n'
|
| 453 |
+
)
|
| 454 |
+
actual = len(claims_tokens) / len(tokens)
|
| 455 |
+
print(f'{name}: {len(tokens):,} examples, claims share {actual:.3f}')
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def main() -> None:
|
| 459 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 460 |
+
sub = parser.add_subparsers(dest='command', required=True)
|
| 461 |
+
|
| 462 |
+
dialogues = sub.add_parser('build', help='write dialogues as JSONL for probing')
|
| 463 |
+
dialogues.add_argument('--count', type=int, default=64)
|
| 464 |
+
dialogues.add_argument('--seed', type=int, default=1337)
|
| 465 |
+
dialogues.add_argument('--turns', type=int, default=6)
|
| 466 |
+
dialogues.add_argument('--corrections', type=int, default=1)
|
| 467 |
+
dialogues.add_argument('--output', type=Path, required=True)
|
| 468 |
+
|
| 469 |
+
pack = sub.add_parser('prepare', help='pack dialogues into the adaptive layout')
|
| 470 |
+
pack.add_argument('--count', type=int, default=2700)
|
| 471 |
+
pack.add_argument('--seed', type=int, default=1337)
|
| 472 |
+
pack.add_argument('--turns', type=int, default=6)
|
| 473 |
+
pack.add_argument('--corrections', type=int, default=1)
|
| 474 |
+
pack.add_argument('--chitchat', type=int, default=2,
|
| 475 |
+
help='turns with no new fact, which train zero-block answers')
|
| 476 |
+
pack.add_argument('--keep-messages', type=int, default=KEEP_MESSAGES)
|
| 477 |
+
pack.add_argument('--tokenizer', type=Path, required=True)
|
| 478 |
+
pack.add_argument('--output-dir', type=Path, required=True)
|
| 479 |
+
pack.add_argument('--seq-len', type=int, default=2048)
|
| 480 |
+
pack.add_argument('--max-slots', type=int, default=64)
|
| 481 |
+
pack.add_argument('--sizes', type=int, nargs='+', default=[32, 64, 128])
|
| 482 |
+
pack.add_argument('--val-every', type=int, default=20,
|
| 483 |
+
help='every Nth dialogue goes to validation, whole')
|
| 484 |
+
|
| 485 |
+
blend = sub.add_parser('mix', help='blend a claims pack into another pack')
|
| 486 |
+
blend.add_argument('--claims-dir', type=Path, required=True)
|
| 487 |
+
blend.add_argument('--other-dir', type=Path, required=True)
|
| 488 |
+
blend.add_argument('--output-dir', type=Path, required=True)
|
| 489 |
+
blend.add_argument('--claims-share', type=float, default=0.30)
|
| 490 |
+
blend.add_argument('--seed', type=int, default=1337)
|
| 491 |
+
|
| 492 |
+
args = parser.parse_args()
|
| 493 |
+
if args.command == 'prepare':
|
| 494 |
+
prepare(args)
|
| 495 |
+
return
|
| 496 |
+
if args.command == 'mix':
|
| 497 |
+
mix(args)
|
| 498 |
+
return
|
| 499 |
+
|
| 500 |
+
records = build(args.count, args.seed, args.turns, args.corrections)
|
| 501 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 502 |
+
with args.output.open('w', encoding='utf-8') as handle:
|
| 503 |
+
for record in records:
|
| 504 |
+
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 505 |
+
turns = sum(len(r['messages']) for r in records) / len(records)
|
| 506 |
+
facts = sum(len(r['truth']) for r in records) / len(records)
|
| 507 |
+
print(f'wrote {len(records)} dialogues to {args.output} '
|
| 508 |
+
f'({turns:.1f} messages and {facts:.1f} facts each)')
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
if __name__ == '__main__':
|
| 512 |
+
main()
|
src/diffusion_lm/config.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed experiment configuration loaded from YAML."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import asdict, dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
MODEL_BACKBONES = ("project", "hf-qwen3")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass(frozen=True)
|
| 16 |
+
class ModelConfig:
|
| 17 |
+
"""Architecture for the bidirectional token denoiser."""
|
| 18 |
+
|
| 19 |
+
vocab_size: int
|
| 20 |
+
mask_token_id: int
|
| 21 |
+
max_seq_len: int
|
| 22 |
+
d_model: int
|
| 23 |
+
n_layers: int
|
| 24 |
+
n_heads: int
|
| 25 |
+
d_ff: int
|
| 26 |
+
dropout: float = 0.1
|
| 27 |
+
tie_embeddings: bool = True
|
| 28 |
+
activation_checkpointing: bool = False
|
| 29 |
+
use_flex_attention: bool = False
|
| 30 |
+
compile_backbone: bool = False
|
| 31 |
+
forbidden_output_token_ids: tuple[int, ...] = (0, 1, 2, 4)
|
| 32 |
+
backbone: str = "project"
|
| 33 |
+
pretrained_path: str | None = None
|
| 34 |
+
# First id of the untrained tail of a pretrained embedding matrix; logits from that id
|
| 35 |
+
# upward are masked without enumerating hundreds of forbidden ids.
|
| 36 |
+
forbidden_output_from: int | None = None
|
| 37 |
+
|
| 38 |
+
def __post_init__(self) -> None:
|
| 39 |
+
if self.vocab_size <= 1:
|
| 40 |
+
raise ValueError("vocab_size must be greater than one")
|
| 41 |
+
if self.backbone not in MODEL_BACKBONES:
|
| 42 |
+
raise ValueError(f"backbone must be one of {MODEL_BACKBONES}")
|
| 43 |
+
if self.backbone != "project" and self.pretrained_path is None:
|
| 44 |
+
raise ValueError("pretrained backbones require pretrained_path")
|
| 45 |
+
if self.forbidden_output_from is not None and not (
|
| 46 |
+
0 < self.forbidden_output_from <= self.vocab_size
|
| 47 |
+
):
|
| 48 |
+
raise ValueError("forbidden_output_from must be inside the vocabulary")
|
| 49 |
+
if not 0 <= self.mask_token_id < self.vocab_size:
|
| 50 |
+
raise ValueError("mask_token_id must be inside the vocabulary")
|
| 51 |
+
if self.max_seq_len <= 0:
|
| 52 |
+
raise ValueError("max_seq_len must be positive")
|
| 53 |
+
if self.d_model <= 0 or self.n_layers <= 0 or self.n_heads <= 0 or self.d_ff <= 0:
|
| 54 |
+
raise ValueError("all model dimensions must be positive")
|
| 55 |
+
if self.d_model % self.n_heads != 0:
|
| 56 |
+
raise ValueError("d_model must be divisible by n_heads")
|
| 57 |
+
if not 0.0 <= self.dropout < 1.0:
|
| 58 |
+
raise ValueError("dropout must be in [0, 1)")
|
| 59 |
+
if not isinstance(self.activation_checkpointing, bool):
|
| 60 |
+
raise ValueError("activation_checkpointing must be a boolean")
|
| 61 |
+
if not isinstance(self.use_flex_attention, bool):
|
| 62 |
+
raise ValueError("use_flex_attention must be a boolean")
|
| 63 |
+
if not isinstance(self.compile_backbone, bool):
|
| 64 |
+
raise ValueError('compile_backbone must be a boolean')
|
| 65 |
+
|
| 66 |
+
forbidden_ids = tuple(self.forbidden_output_token_ids)
|
| 67 |
+
if any(
|
| 68 |
+
isinstance(token_id, bool) or not isinstance(token_id, int)
|
| 69 |
+
for token_id in forbidden_ids
|
| 70 |
+
):
|
| 71 |
+
raise ValueError("forbidden_output_token_ids must contain integers")
|
| 72 |
+
if len(set(forbidden_ids)) != len(forbidden_ids):
|
| 73 |
+
raise ValueError("forbidden_output_token_ids must not contain duplicates")
|
| 74 |
+
if any(not 0 <= token_id < self.vocab_size for token_id in forbidden_ids):
|
| 75 |
+
raise ValueError("forbidden output token ids must be inside the vocabulary")
|
| 76 |
+
if self.mask_token_id not in forbidden_ids:
|
| 77 |
+
raise ValueError("mask_token_id must be a forbidden output token")
|
| 78 |
+
if self.backbone == "project" and 3 in forbidden_ids:
|
| 79 |
+
raise ValueError("EOS token id 3 must remain an allowed output token")
|
| 80 |
+
object.__setattr__(self, "forbidden_output_token_ids", forbidden_ids)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass(frozen=True)
|
| 84 |
+
class TrainingConfig:
|
| 85 |
+
"""Single-device training settings for the first research iteration."""
|
| 86 |
+
|
| 87 |
+
train_data: str
|
| 88 |
+
tokenizer: str
|
| 89 |
+
output_dir: str = "outputs/run"
|
| 90 |
+
val_data: str | None = None
|
| 91 |
+
batch_size: int = 32
|
| 92 |
+
gradient_accumulation_steps: int = 1
|
| 93 |
+
max_steps: int = 10_000
|
| 94 |
+
learning_rate: float = 3e-4
|
| 95 |
+
min_learning_rate: float = 3e-5
|
| 96 |
+
warmup_steps: int = 500
|
| 97 |
+
weight_decay: float = 0.1
|
| 98 |
+
grad_clip: float = 1.0
|
| 99 |
+
mask_eps: float = 1e-3
|
| 100 |
+
seed: int = 1337
|
| 101 |
+
device: str = "auto"
|
| 102 |
+
precision: str = "auto"
|
| 103 |
+
optimizer: str = "adamw"
|
| 104 |
+
optimizer_min_8bit_size: int = 4096
|
| 105 |
+
optimizer_embedding_32bit: bool = True
|
| 106 |
+
require_fused_attention: bool = False
|
| 107 |
+
save_inference_checkpoint: bool = False
|
| 108 |
+
num_workers: int = 0
|
| 109 |
+
log_interval: int = 10
|
| 110 |
+
eval_interval: int = 500
|
| 111 |
+
eval_batches: int = 20
|
| 112 |
+
save_interval: int = 500
|
| 113 |
+
keep_last_checkpoints: int = 3
|
| 114 |
+
|
| 115 |
+
def __post_init__(self) -> None:
|
| 116 |
+
if self.batch_size <= 0 or self.gradient_accumulation_steps <= 0:
|
| 117 |
+
raise ValueError("batch sizes must be positive")
|
| 118 |
+
if self.max_steps <= 0:
|
| 119 |
+
raise ValueError("max_steps must be positive")
|
| 120 |
+
if not 0.0 < self.learning_rate:
|
| 121 |
+
raise ValueError("learning_rate must be positive")
|
| 122 |
+
if not 0.0 <= self.min_learning_rate <= self.learning_rate:
|
| 123 |
+
raise ValueError("min_learning_rate must be between zero and learning_rate")
|
| 124 |
+
if not 0 <= self.warmup_steps < self.max_steps:
|
| 125 |
+
raise ValueError("warmup_steps must be non-negative and less than max_steps")
|
| 126 |
+
if self.weight_decay < 0.0:
|
| 127 |
+
raise ValueError("weight_decay must be non-negative")
|
| 128 |
+
if self.grad_clip <= 0.0:
|
| 129 |
+
raise ValueError("grad_clip must be positive")
|
| 130 |
+
if not 0.0 < self.mask_eps < 1.0:
|
| 131 |
+
raise ValueError("mask_eps must be in (0, 1)")
|
| 132 |
+
if self.num_workers < 0:
|
| 133 |
+
raise ValueError("num_workers must be non-negative")
|
| 134 |
+
if self.log_interval <= 0 or self.eval_interval <= 0 or self.save_interval <= 0:
|
| 135 |
+
raise ValueError("log, eval, and save intervals must be positive")
|
| 136 |
+
if self.eval_batches <= 0:
|
| 137 |
+
raise ValueError("eval_batches must be positive")
|
| 138 |
+
if self.keep_last_checkpoints < 0:
|
| 139 |
+
raise ValueError("keep_last_checkpoints must be non-negative")
|
| 140 |
+
if self.precision not in {"auto", "float32", "bfloat16", "float16"}:
|
| 141 |
+
raise ValueError("precision must be auto, float32, bfloat16, or float16")
|
| 142 |
+
if self.optimizer not in {"adamw", "adamw8bit"}:
|
| 143 |
+
raise ValueError("optimizer must be adamw or adamw8bit")
|
| 144 |
+
if self.optimizer_min_8bit_size <= 0:
|
| 145 |
+
raise ValueError("optimizer_min_8bit_size must be positive")
|
| 146 |
+
if not isinstance(self.optimizer_embedding_32bit, bool):
|
| 147 |
+
raise ValueError("optimizer_embedding_32bit must be a boolean")
|
| 148 |
+
if not isinstance(self.require_fused_attention, bool):
|
| 149 |
+
raise ValueError("require_fused_attention must be a boolean")
|
| 150 |
+
if not isinstance(self.save_inference_checkpoint, bool):
|
| 151 |
+
raise ValueError("save_inference_checkpoint must be a boolean")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@dataclass(frozen=True)
|
| 155 |
+
class ExperimentConfig:
|
| 156 |
+
model: ModelConfig
|
| 157 |
+
training: TrainingConfig
|
| 158 |
+
|
| 159 |
+
def to_dict(self) -> dict[str, Any]:
|
| 160 |
+
return asdict(self)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def load_config(path: str | Path) -> ExperimentConfig:
|
| 164 |
+
"""Load and validate an experiment YAML file."""
|
| 165 |
+
|
| 166 |
+
config_path = Path(path)
|
| 167 |
+
with config_path.open("r", encoding="utf-8") as handle:
|
| 168 |
+
raw = yaml.safe_load(handle)
|
| 169 |
+
if not isinstance(raw, dict) or "model" not in raw or "training" not in raw:
|
| 170 |
+
raise ValueError("config must contain top-level model and training mappings")
|
| 171 |
+
return ExperimentConfig(
|
| 172 |
+
model=ModelConfig(**raw["model"]),
|
| 173 |
+
training=TrainingConfig(**raw["training"]),
|
| 174 |
+
)
|
src/diffusion_lm/corpus.py
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download and prepare the pinned FineWeb-Edu 10B sample.
|
| 2 |
+
|
| 3 |
+
The heavy ``huggingface_hub`` and ``pyarrow`` dependencies are imported only by the operations that
|
| 4 |
+
need them. Raw Parquet files are cached locally, then converted into independently replaceable
|
| 5 |
+
uint16/uint32 shards with deterministic document-level train/validation assignment.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import hashlib
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Any, Iterable, Iterator, Literal, Sequence
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
from diffusion_lm.data import PACKED_MANIFEST_FORMAT
|
| 21 |
+
from diffusion_lm.tokenizer import (
|
| 22 |
+
load_tokenizer,
|
| 23 |
+
special_token_ids,
|
| 24 |
+
train_tokenizer_from_iterator,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
FINEWEB_EDU_REPO_ID = "HuggingFaceFW/fineweb-edu"
|
| 29 |
+
FINEWEB_EDU_CONFIG = "sample-10BT"
|
| 30 |
+
FINEWEB_EDU_REVISION = "87f09149ef4734204d70ed1d046ddc9ca3f2b8f9"
|
| 31 |
+
FINEWEB_EDU_PATH_PREFIX = "sample/10BT/"
|
| 32 |
+
SOURCE_STATE_FORMAT = "mini-diffusion-lm-corpus-source-v1"
|
| 33 |
+
SPLIT_HASH_PERSON = b"mini-mdlm-split"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass(frozen=True)
|
| 37 |
+
class CorpusSource:
|
| 38 |
+
"""One pinned Hub dataset: where its Parquets live and how rows are read.
|
| 39 |
+
|
| 40 |
+
``id_column=None`` derives the split id from a sha256 of the text, which keeps the
|
| 41 |
+
train/validation assignment order-independent for datasets without a stable row id.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
name: str
|
| 45 |
+
repo_id: str
|
| 46 |
+
revision: str
|
| 47 |
+
path_prefix: str
|
| 48 |
+
text_column: str = "text"
|
| 49 |
+
id_column: str | None = "id"
|
| 50 |
+
config: str | None = None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
CORPUS_SOURCES: dict[str, CorpusSource] = {
|
| 54 |
+
source.name: source
|
| 55 |
+
for source in (
|
| 56 |
+
CorpusSource(
|
| 57 |
+
name="fineweb-edu",
|
| 58 |
+
repo_id=FINEWEB_EDU_REPO_ID,
|
| 59 |
+
revision=FINEWEB_EDU_REVISION,
|
| 60 |
+
path_prefix=FINEWEB_EDU_PATH_PREFIX,
|
| 61 |
+
config=FINEWEB_EDU_CONFIG,
|
| 62 |
+
),
|
| 63 |
+
CorpusSource(
|
| 64 |
+
name="ultra-fineweb-en",
|
| 65 |
+
repo_id="openbmb/Ultra-FineWeb",
|
| 66 |
+
revision="7ddd4170ce03e0afbd7d9b80d4bc0b8eebf877e4",
|
| 67 |
+
path_prefix="data/ultrafineweb_en/",
|
| 68 |
+
text_column="content",
|
| 69 |
+
id_column=None,
|
| 70 |
+
),
|
| 71 |
+
CorpusSource(
|
| 72 |
+
name="cosmopedia-v2",
|
| 73 |
+
repo_id="HuggingFaceTB/smollm-corpus",
|
| 74 |
+
revision="3ba9d605774198c5868892d7a8deda78031a781f",
|
| 75 |
+
path_prefix="cosmopedia-v2/",
|
| 76 |
+
id_column=None,
|
| 77 |
+
config="cosmopedia-v2",
|
| 78 |
+
),
|
| 79 |
+
CorpusSource(
|
| 80 |
+
name="finemath-4plus",
|
| 81 |
+
repo_id="HuggingFaceTB/finemath",
|
| 82 |
+
revision="e92b25a616738fe95dc186b64dfb19f9c8525594",
|
| 83 |
+
path_prefix="finemath-4plus/",
|
| 84 |
+
id_column=None,
|
| 85 |
+
config="finemath-4plus",
|
| 86 |
+
),
|
| 87 |
+
)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _require_huggingface_hub():
|
| 92 |
+
try:
|
| 93 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 94 |
+
except ImportError as exc: # pragma: no cover - exercised in minimal installations.
|
| 95 |
+
raise RuntimeError(
|
| 96 |
+
"FineWeb-Edu download requires huggingface_hub; install the corpus dependencies"
|
| 97 |
+
) from exc
|
| 98 |
+
return HfApi, snapshot_download
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _require_parquet():
|
| 102 |
+
try:
|
| 103 |
+
import pyarrow.parquet as parquet
|
| 104 |
+
except ImportError as exc: # pragma: no cover - exercised in minimal installations.
|
| 105 |
+
raise RuntimeError(
|
| 106 |
+
"FineWeb-Edu preparation requires pyarrow; install the corpus dependencies"
|
| 107 |
+
) from exc
|
| 108 |
+
return parquet
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _atomic_json(path: Path, value: dict[str, Any]) -> None:
|
| 112 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 113 |
+
temporary = path.with_name(f".{path.name}.tmp")
|
| 114 |
+
temporary.unlink(missing_ok=True)
|
| 115 |
+
with temporary.open("w", encoding="utf-8") as handle:
|
| 116 |
+
json.dump(value, handle, indent=2, sort_keys=True)
|
| 117 |
+
handle.write("\n")
|
| 118 |
+
handle.flush()
|
| 119 |
+
os.fsync(handle.fileno())
|
| 120 |
+
temporary.replace(path)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _read_json(path: Path) -> dict[str, Any]:
|
| 124 |
+
try:
|
| 125 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 126 |
+
value = json.load(handle)
|
| 127 |
+
except (OSError, json.JSONDecodeError) as exc:
|
| 128 |
+
raise ValueError(f"could not read corpus state {path}: {exc}") from exc
|
| 129 |
+
if not isinstance(value, dict):
|
| 130 |
+
raise ValueError(f"corpus state must be a JSON object: {path}")
|
| 131 |
+
return value
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _sha256_file(path: Path, *, chunk_size: int = 1024 * 1024) -> str:
|
| 135 |
+
digest = hashlib.sha256()
|
| 136 |
+
with path.open("rb") as handle:
|
| 137 |
+
for chunk in iter(lambda: handle.read(chunk_size), b""):
|
| 138 |
+
digest.update(chunk)
|
| 139 |
+
return digest.hexdigest()
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def download_source(
|
| 143 |
+
source: CorpusSource,
|
| 144 |
+
raw_dir: str | Path,
|
| 145 |
+
*,
|
| 146 |
+
max_files: int | None = None,
|
| 147 |
+
max_workers: int = 1,
|
| 148 |
+
) -> list[Path]:
|
| 149 |
+
"""Download a pinned source's Parquets into ``raw_dir`` using the Hub cache."""
|
| 150 |
+
|
| 151 |
+
if max_files is not None and max_files <= 0:
|
| 152 |
+
raise ValueError("max_files must be positive")
|
| 153 |
+
if max_workers <= 0:
|
| 154 |
+
raise ValueError("max_workers must be positive")
|
| 155 |
+
HfApi, snapshot_download = _require_huggingface_hub()
|
| 156 |
+
api = HfApi()
|
| 157 |
+
repository_files = api.list_repo_files(
|
| 158 |
+
repo_id=source.repo_id,
|
| 159 |
+
repo_type="dataset",
|
| 160 |
+
revision=source.revision,
|
| 161 |
+
)
|
| 162 |
+
parquet_names = sorted(
|
| 163 |
+
name
|
| 164 |
+
for name in repository_files
|
| 165 |
+
if name.startswith(source.path_prefix) and name.endswith(".parquet")
|
| 166 |
+
)
|
| 167 |
+
if max_files is not None:
|
| 168 |
+
parquet_names = parquet_names[:max_files]
|
| 169 |
+
if not parquet_names:
|
| 170 |
+
raise RuntimeError(
|
| 171 |
+
f"no Parquet files found for {source.repo_id}@{source.revision} "
|
| 172 |
+
f"under {source.path_prefix}"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
destination = Path(raw_dir)
|
| 176 |
+
destination.mkdir(parents=True, exist_ok=True)
|
| 177 |
+
snapshot_root = Path(
|
| 178 |
+
snapshot_download(
|
| 179 |
+
repo_id=source.repo_id,
|
| 180 |
+
repo_type="dataset",
|
| 181 |
+
revision=source.revision,
|
| 182 |
+
allow_patterns=parquet_names,
|
| 183 |
+
local_dir=str(destination),
|
| 184 |
+
max_workers=max_workers,
|
| 185 |
+
)
|
| 186 |
+
)
|
| 187 |
+
paths: list[Path] = []
|
| 188 |
+
for name in parquet_names:
|
| 189 |
+
path = snapshot_root / name
|
| 190 |
+
if not path.is_file():
|
| 191 |
+
raise FileNotFoundError(f"Hub download did not produce {path}")
|
| 192 |
+
paths.append(path)
|
| 193 |
+
return paths
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def download_fineweb_edu(
|
| 197 |
+
raw_dir: str | Path,
|
| 198 |
+
*,
|
| 199 |
+
revision: str = FINEWEB_EDU_REVISION,
|
| 200 |
+
max_files: int | None = None,
|
| 201 |
+
max_workers: int = 1,
|
| 202 |
+
) -> list[Path]:
|
| 203 |
+
"""Download the pinned FineWeb-Edu sample (compatibility entry point)."""
|
| 204 |
+
|
| 205 |
+
from dataclasses import replace
|
| 206 |
+
|
| 207 |
+
source = replace(CORPUS_SOURCES["fineweb-edu"], revision=revision)
|
| 208 |
+
return download_source(source, raw_dir, max_files=max_files, max_workers=max_workers)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def find_local_parquets(
|
| 212 |
+
raw_dir: str | Path, path_prefix: str = FINEWEB_EDU_PATH_PREFIX
|
| 213 |
+
) -> list[Path]:
|
| 214 |
+
"""Find already downloaded source Parquets in stable source order."""
|
| 215 |
+
|
| 216 |
+
root = Path(raw_dir)
|
| 217 |
+
preferred = sorted((root / path_prefix).glob("*.parquet"))
|
| 218 |
+
paths = preferred or sorted(root.rglob("*.parquet"))
|
| 219 |
+
if not paths:
|
| 220 |
+
raise FileNotFoundError(f"no Parquet files found below {root}")
|
| 221 |
+
return paths
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def document_split(
|
| 225 |
+
document_id: str,
|
| 226 |
+
*,
|
| 227 |
+
validation_modulus: int = 1024,
|
| 228 |
+
validation_bucket: int = 0,
|
| 229 |
+
) -> Literal["train", "validation"]:
|
| 230 |
+
"""Assign a stable document ID to train or validation without depending on row order."""
|
| 231 |
+
|
| 232 |
+
if not document_id:
|
| 233 |
+
raise ValueError("document_id must be non-empty")
|
| 234 |
+
if validation_modulus <= 1:
|
| 235 |
+
raise ValueError("validation_modulus must be greater than one")
|
| 236 |
+
if not 0 <= validation_bucket < validation_modulus:
|
| 237 |
+
raise ValueError("validation_bucket must be inside validation_modulus")
|
| 238 |
+
digest = hashlib.blake2b(
|
| 239 |
+
document_id.encode("utf-8"), digest_size=8, person=SPLIT_HASH_PERSON
|
| 240 |
+
).digest()
|
| 241 |
+
bucket = int.from_bytes(digest, "little") % validation_modulus
|
| 242 |
+
return "validation" if bucket == validation_bucket else "train"
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _iter_parquet_batches(
|
| 246 |
+
paths: Sequence[Path],
|
| 247 |
+
*,
|
| 248 |
+
batch_size: int,
|
| 249 |
+
text_column: str = "text",
|
| 250 |
+
id_column: str | None = "id",
|
| 251 |
+
) -> Iterator[tuple[Path, list[str], list[str]]]:
|
| 252 |
+
if batch_size <= 0:
|
| 253 |
+
raise ValueError("batch_size must be positive")
|
| 254 |
+
parquet = _require_parquet()
|
| 255 |
+
read_columns = [text_column] if id_column is None else [id_column, text_column]
|
| 256 |
+
for path in paths:
|
| 257 |
+
source = parquet.ParquetFile(path)
|
| 258 |
+
try:
|
| 259 |
+
batches = source.iter_batches(batch_size=batch_size, columns=read_columns)
|
| 260 |
+
for batch in batches:
|
| 261 |
+
columns = batch.to_pydict()
|
| 262 |
+
texts = columns[text_column]
|
| 263 |
+
if id_column is not None:
|
| 264 |
+
ids = columns[id_column]
|
| 265 |
+
else:
|
| 266 |
+
# Content-derived ids keep the split assignment order-independent.
|
| 267 |
+
ids = [
|
| 268 |
+
hashlib.sha256(text.encode("utf-8")).hexdigest()
|
| 269 |
+
if isinstance(text, str)
|
| 270 |
+
else ""
|
| 271 |
+
for text in texts
|
| 272 |
+
]
|
| 273 |
+
if len(ids) != len(texts):
|
| 274 |
+
raise ValueError(f"mismatched id/text columns in {path}")
|
| 275 |
+
yield path, ids, texts
|
| 276 |
+
except (KeyError, ValueError) as exc:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
f"expected columns {read_columns} in {path}: {exc}"
|
| 279 |
+
) from exc
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def iter_tokenizer_text(
|
| 283 |
+
paths: Iterable[str | Path],
|
| 284 |
+
*,
|
| 285 |
+
max_utf8_bytes: int = 1 << 29,
|
| 286 |
+
batch_size: int = 512,
|
| 287 |
+
validation_modulus: int = 1024,
|
| 288 |
+
validation_bucket: int = 0,
|
| 289 |
+
text_column: str = "text",
|
| 290 |
+
id_column: str | None = "id",
|
| 291 |
+
) -> Iterator[list[str]]:
|
| 292 |
+
"""Yield bounded train-only text batches for iterator-based tokenizer training."""
|
| 293 |
+
|
| 294 |
+
if max_utf8_bytes <= 0:
|
| 295 |
+
raise ValueError("max_utf8_bytes must be positive")
|
| 296 |
+
sources = sorted(Path(path) for path in paths)
|
| 297 |
+
used_bytes = 0
|
| 298 |
+
output: list[str] = []
|
| 299 |
+
for _path, ids, texts in _iter_parquet_batches(
|
| 300 |
+
sources, batch_size=batch_size, text_column=text_column, id_column=id_column
|
| 301 |
+
):
|
| 302 |
+
for document_id, text in zip(ids, texts, strict=True):
|
| 303 |
+
if not isinstance(document_id, str) or not isinstance(text, str) or not text:
|
| 304 |
+
continue
|
| 305 |
+
if document_split(
|
| 306 |
+
document_id,
|
| 307 |
+
validation_modulus=validation_modulus,
|
| 308 |
+
validation_bucket=validation_bucket,
|
| 309 |
+
) != "train":
|
| 310 |
+
continue
|
| 311 |
+
encoded_bytes = len(text.encode("utf-8"))
|
| 312 |
+
if used_bytes and used_bytes + encoded_bytes > max_utf8_bytes:
|
| 313 |
+
if output:
|
| 314 |
+
yield output
|
| 315 |
+
return
|
| 316 |
+
output.append(text)
|
| 317 |
+
used_bytes += encoded_bytes
|
| 318 |
+
if len(output) >= batch_size:
|
| 319 |
+
yield output
|
| 320 |
+
output = []
|
| 321 |
+
if used_bytes >= max_utf8_bytes:
|
| 322 |
+
if output:
|
| 323 |
+
yield output
|
| 324 |
+
return
|
| 325 |
+
if output:
|
| 326 |
+
yield output
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
@dataclass(frozen=True)
|
| 330 |
+
class CorpusPreparationResult:
|
| 331 |
+
train_manifest: Path
|
| 332 |
+
validation_manifest: Path
|
| 333 |
+
processed_sources: int
|
| 334 |
+
resumed_sources: int
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
class _SplitWriter:
|
| 338 |
+
def __init__(self, final_path: Path, dtype: np.dtype[Any]) -> None:
|
| 339 |
+
self.final_path = final_path
|
| 340 |
+
self.dtype = dtype
|
| 341 |
+
self.temporary_path = final_path.with_name(f".{final_path.name}.tmp")
|
| 342 |
+
final_path.parent.mkdir(parents=True, exist_ok=True)
|
| 343 |
+
self.temporary_path.unlink(missing_ok=True)
|
| 344 |
+
self.handle = self.temporary_path.open("wb")
|
| 345 |
+
self.digest = hashlib.sha256()
|
| 346 |
+
self.token_count = 0
|
| 347 |
+
self.document_count = 0
|
| 348 |
+
|
| 349 |
+
def append(self, token_ids: list[int]) -> None:
|
| 350 |
+
payload = np.asarray(token_ids, dtype=self.dtype).tobytes()
|
| 351 |
+
self.handle.write(payload)
|
| 352 |
+
self.digest.update(payload)
|
| 353 |
+
self.token_count += len(token_ids)
|
| 354 |
+
self.document_count += 1
|
| 355 |
+
|
| 356 |
+
def finish(self) -> dict[str, Any]:
|
| 357 |
+
self.handle.flush()
|
| 358 |
+
os.fsync(self.handle.fileno())
|
| 359 |
+
self.handle.close()
|
| 360 |
+
self.temporary_path.replace(self.final_path)
|
| 361 |
+
return {
|
| 362 |
+
"token_count": self.token_count,
|
| 363 |
+
"document_count": self.document_count,
|
| 364 |
+
"sha256": self.digest.hexdigest(),
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
def abort(self) -> None:
|
| 368 |
+
if not self.handle.closed:
|
| 369 |
+
self.handle.close()
|
| 370 |
+
self.temporary_path.unlink(missing_ok=True)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _relative_path(path: Path, root: Path) -> str:
|
| 374 |
+
try:
|
| 375 |
+
return path.relative_to(root).as_posix()
|
| 376 |
+
except ValueError:
|
| 377 |
+
return str(path)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def _completed_source_state(
|
| 381 |
+
state_path: Path,
|
| 382 |
+
*,
|
| 383 |
+
source: Path,
|
| 384 |
+
corpus_source: CorpusSource,
|
| 385 |
+
output_dir: Path,
|
| 386 |
+
tokenizer_sha256: str,
|
| 387 |
+
validation_modulus: int,
|
| 388 |
+
validation_bucket: int,
|
| 389 |
+
) -> dict[str, Any] | None:
|
| 390 |
+
if not state_path.is_file():
|
| 391 |
+
return None
|
| 392 |
+
try:
|
| 393 |
+
state = _read_json(state_path)
|
| 394 |
+
except ValueError:
|
| 395 |
+
return None
|
| 396 |
+
if (
|
| 397 |
+
state.get("format") != SOURCE_STATE_FORMAT
|
| 398 |
+
or state.get("source_name") != source.name
|
| 399 |
+
or state.get("source_size") != source.stat().st_size
|
| 400 |
+
# States written before multi-source support carry no dataset name.
|
| 401 |
+
or state.get("source_dataset", "fineweb-edu") != corpus_source.name
|
| 402 |
+
or state.get("dataset_revision") != corpus_source.revision
|
| 403 |
+
or state.get("tokenizer_sha256") != tokenizer_sha256
|
| 404 |
+
or state.get("split_hash_person") != SPLIT_HASH_PERSON.decode("ascii")
|
| 405 |
+
or state.get("validation_modulus") != validation_modulus
|
| 406 |
+
or state.get("validation_bucket") != validation_bucket
|
| 407 |
+
):
|
| 408 |
+
return None
|
| 409 |
+
splits = state.get("splits")
|
| 410 |
+
if not isinstance(splits, dict):
|
| 411 |
+
return None
|
| 412 |
+
try:
|
| 413 |
+
dtype = np.dtype(state.get("dtype"))
|
| 414 |
+
except TypeError:
|
| 415 |
+
return None
|
| 416 |
+
for split in ("train", "validation"):
|
| 417 |
+
shard = splits.get(split)
|
| 418 |
+
if not isinstance(shard, dict) or not isinstance(shard.get("path"), str):
|
| 419 |
+
return None
|
| 420 |
+
path = output_dir / shard["path"]
|
| 421 |
+
expected_bytes = int(shard.get("token_count", -1)) * dtype.itemsize
|
| 422 |
+
expected_sha256 = shard.get("sha256")
|
| 423 |
+
if (
|
| 424 |
+
not path.is_file()
|
| 425 |
+
or path.stat().st_size != expected_bytes
|
| 426 |
+
or not isinstance(expected_sha256, str)
|
| 427 |
+
or _sha256_file(path) != expected_sha256
|
| 428 |
+
):
|
| 429 |
+
return None
|
| 430 |
+
return state
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def _encode_source(
|
| 434 |
+
source: Path,
|
| 435 |
+
*,
|
| 436 |
+
corpus_source: CorpusSource,
|
| 437 |
+
tokenizer_path: Path,
|
| 438 |
+
output_dir: Path,
|
| 439 |
+
batch_size: int,
|
| 440 |
+
validation_modulus: int,
|
| 441 |
+
validation_bucket: int,
|
| 442 |
+
) -> tuple[dict[str, Any], bool]:
|
| 443 |
+
tokenizer_sha256 = hashlib.sha256(tokenizer_path.read_bytes()).hexdigest()
|
| 444 |
+
# Hub Parquet names are unique and stable. Avoid list indexes so a partial download can be
|
| 445 |
+
# expanded later without invalidating already completed source shards.
|
| 446 |
+
source_key = source.stem
|
| 447 |
+
state_path = output_dir / "state" / f"{source_key}.json"
|
| 448 |
+
resumed = _completed_source_state(
|
| 449 |
+
state_path,
|
| 450 |
+
source=source,
|
| 451 |
+
corpus_source=corpus_source,
|
| 452 |
+
output_dir=output_dir,
|
| 453 |
+
tokenizer_sha256=tokenizer_sha256,
|
| 454 |
+
validation_modulus=validation_modulus,
|
| 455 |
+
validation_bucket=validation_bucket,
|
| 456 |
+
)
|
| 457 |
+
if resumed is not None:
|
| 458 |
+
return resumed, True
|
| 459 |
+
|
| 460 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 461 |
+
role_ids = special_token_ids(tokenizer)
|
| 462 |
+
reserved_ids = set(role_ids.values())
|
| 463 |
+
vocab_size = tokenizer.get_vocab_size(with_added_tokens=True)
|
| 464 |
+
dtype = np.dtype("uint16" if vocab_size <= np.iinfo(np.uint16).max else "uint32")
|
| 465 |
+
final_paths = {
|
| 466 |
+
split: output_dir / "shards" / split / f"{source_key}.bin"
|
| 467 |
+
for split in ("train", "validation")
|
| 468 |
+
}
|
| 469 |
+
writers = {split: _SplitWriter(path, dtype) for split, path in final_paths.items()}
|
| 470 |
+
skipped_empty = 0
|
| 471 |
+
skipped_invalid = 0
|
| 472 |
+
skipped_special = 0
|
| 473 |
+
rows_seen = 0
|
| 474 |
+
|
| 475 |
+
try:
|
| 476 |
+
for _path, ids, texts in _iter_parquet_batches(
|
| 477 |
+
[source],
|
| 478 |
+
batch_size=batch_size,
|
| 479 |
+
text_column=corpus_source.text_column,
|
| 480 |
+
id_column=corpus_source.id_column,
|
| 481 |
+
):
|
| 482 |
+
valid_rows: list[tuple[str, str]] = []
|
| 483 |
+
for document_id, text in zip(ids, texts, strict=True):
|
| 484 |
+
rows_seen += 1
|
| 485 |
+
if not isinstance(document_id, str) or not document_id:
|
| 486 |
+
skipped_invalid += 1
|
| 487 |
+
elif not isinstance(text, str):
|
| 488 |
+
skipped_invalid += 1
|
| 489 |
+
elif not text:
|
| 490 |
+
skipped_empty += 1
|
| 491 |
+
else:
|
| 492 |
+
valid_rows.append((document_id, text))
|
| 493 |
+
if not valid_rows:
|
| 494 |
+
continue
|
| 495 |
+
encodings = tokenizer.encode_batch(
|
| 496 |
+
[text for _document_id, text in valid_rows], add_special_tokens=False
|
| 497 |
+
)
|
| 498 |
+
for (document_id, _text), encoding in zip(valid_rows, encodings, strict=True):
|
| 499 |
+
token_ids = encoding.ids
|
| 500 |
+
if reserved_ids.intersection(token_ids):
|
| 501 |
+
# The new sentinel strings make this practically impossible, but skipping is
|
| 502 |
+
# preferable to losing hours of preprocessing if an exact literal is present.
|
| 503 |
+
skipped_special += 1
|
| 504 |
+
continue
|
| 505 |
+
split = document_split(
|
| 506 |
+
document_id,
|
| 507 |
+
validation_modulus=validation_modulus,
|
| 508 |
+
validation_bucket=validation_bucket,
|
| 509 |
+
)
|
| 510 |
+
writers[split].append([*token_ids, role_ids["eos"]])
|
| 511 |
+
split_metadata = {split: writer.finish() for split, writer in writers.items()}
|
| 512 |
+
except BaseException:
|
| 513 |
+
for writer in writers.values():
|
| 514 |
+
writer.abort()
|
| 515 |
+
raise
|
| 516 |
+
|
| 517 |
+
for split, metadata in split_metadata.items():
|
| 518 |
+
metadata["path"] = _relative_path(final_paths[split], output_dir)
|
| 519 |
+
metadata["source"] = source.name
|
| 520 |
+
state: dict[str, Any] = {
|
| 521 |
+
"format": SOURCE_STATE_FORMAT,
|
| 522 |
+
"source_name": source.name,
|
| 523 |
+
"source_size": source.stat().st_size,
|
| 524 |
+
"source_rows": rows_seen,
|
| 525 |
+
"source_dataset": corpus_source.name,
|
| 526 |
+
"dataset_revision": corpus_source.revision,
|
| 527 |
+
"dtype": dtype.name,
|
| 528 |
+
"vocab_size": vocab_size,
|
| 529 |
+
"mask_token_id": role_ids["mask"],
|
| 530 |
+
"eos_token_id": role_ids["eos"],
|
| 531 |
+
"special_token_ids": role_ids,
|
| 532 |
+
"tokenizer_sha256": tokenizer_sha256,
|
| 533 |
+
"split_hash_person": SPLIT_HASH_PERSON.decode("ascii"),
|
| 534 |
+
"validation_modulus": validation_modulus,
|
| 535 |
+
"validation_bucket": validation_bucket,
|
| 536 |
+
"skipped_empty_documents": skipped_empty,
|
| 537 |
+
"skipped_invalid_documents": skipped_invalid,
|
| 538 |
+
"skipped_special_documents": skipped_special,
|
| 539 |
+
"splits": split_metadata,
|
| 540 |
+
}
|
| 541 |
+
# The marker is written last: its presence commits both split files as one source unit.
|
| 542 |
+
_atomic_json(state_path, state)
|
| 543 |
+
return state, False
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def _build_manifest(
|
| 547 |
+
split: Literal["train", "validation"],
|
| 548 |
+
*,
|
| 549 |
+
corpus_source: CorpusSource,
|
| 550 |
+
source_paths: Sequence[Path],
|
| 551 |
+
source_states: Sequence[dict[str, Any]],
|
| 552 |
+
validation_modulus: int,
|
| 553 |
+
validation_bucket: int,
|
| 554 |
+
) -> dict[str, Any]:
|
| 555 |
+
first = source_states[0]
|
| 556 |
+
compatible_keys = (
|
| 557 |
+
"dtype",
|
| 558 |
+
"vocab_size",
|
| 559 |
+
"mask_token_id",
|
| 560 |
+
"eos_token_id",
|
| 561 |
+
"special_token_ids",
|
| 562 |
+
"tokenizer_sha256",
|
| 563 |
+
)
|
| 564 |
+
for state in source_states[1:]:
|
| 565 |
+
if any(state.get(key) != first.get(key) for key in compatible_keys):
|
| 566 |
+
raise ValueError("source states use incompatible tokenizer or token formats")
|
| 567 |
+
shards = [dict(state["splits"][split]) for state in source_states]
|
| 568 |
+
return {
|
| 569 |
+
"format": PACKED_MANIFEST_FORMAT,
|
| 570 |
+
"split": split,
|
| 571 |
+
"dtype": first["dtype"],
|
| 572 |
+
"token_count": sum(int(shard["token_count"]) for shard in shards),
|
| 573 |
+
"document_count": sum(int(shard["document_count"]) for shard in shards),
|
| 574 |
+
"vocab_size": first["vocab_size"],
|
| 575 |
+
"mask_token_id": first["mask_token_id"],
|
| 576 |
+
"eos_token_id": first["eos_token_id"],
|
| 577 |
+
"special_token_ids": first["special_token_ids"],
|
| 578 |
+
"tokenizer_sha256": first["tokenizer_sha256"],
|
| 579 |
+
"dataset": {
|
| 580 |
+
"repo_id": corpus_source.repo_id,
|
| 581 |
+
"config": corpus_source.config,
|
| 582 |
+
"revision": corpus_source.revision,
|
| 583 |
+
"path_prefix": corpus_source.path_prefix,
|
| 584 |
+
},
|
| 585 |
+
"split_rule": {
|
| 586 |
+
"algorithm": "blake2b-64",
|
| 587 |
+
"person": SPLIT_HASH_PERSON.decode("ascii"),
|
| 588 |
+
"validation_modulus": validation_modulus,
|
| 589 |
+
"validation_bucket": validation_bucket,
|
| 590 |
+
},
|
| 591 |
+
"source_files": [path.name for path in source_paths],
|
| 592 |
+
"skipped_documents": {
|
| 593 |
+
reason: sum(int(state[reason]) for state in source_states)
|
| 594 |
+
for reason in (
|
| 595 |
+
"skipped_empty_documents",
|
| 596 |
+
"skipped_invalid_documents",
|
| 597 |
+
"skipped_special_documents",
|
| 598 |
+
)
|
| 599 |
+
},
|
| 600 |
+
"shards": shards,
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def prepare_corpus(
|
| 605 |
+
tokenizer_path: str | Path,
|
| 606 |
+
output_dir: str | Path,
|
| 607 |
+
*,
|
| 608 |
+
corpus_source: CorpusSource,
|
| 609 |
+
source_paths: Iterable[str | Path] | None = None,
|
| 610 |
+
raw_dir: str | Path | None = None,
|
| 611 |
+
batch_size: int = 256,
|
| 612 |
+
validation_modulus: int = 1024,
|
| 613 |
+
validation_bucket: int = 0,
|
| 614 |
+
max_files: int | None = None,
|
| 615 |
+
) -> CorpusPreparationResult:
|
| 616 |
+
"""Convert one pinned source's Parquets into resumable train/validation manifests."""
|
| 617 |
+
|
| 618 |
+
# Validate split arguments before performing any download.
|
| 619 |
+
document_split(
|
| 620 |
+
"argument-validation",
|
| 621 |
+
validation_modulus=validation_modulus,
|
| 622 |
+
validation_bucket=validation_bucket,
|
| 623 |
+
)
|
| 624 |
+
tokenizer = Path(tokenizer_path)
|
| 625 |
+
load_tokenizer(tokenizer)
|
| 626 |
+
destination = Path(output_dir)
|
| 627 |
+
destination.mkdir(parents=True, exist_ok=True)
|
| 628 |
+
|
| 629 |
+
if source_paths is None:
|
| 630 |
+
raw = Path(raw_dir) if raw_dir is not None else destination / "raw"
|
| 631 |
+
try:
|
| 632 |
+
sources = find_local_parquets(raw, corpus_source.path_prefix)
|
| 633 |
+
except FileNotFoundError:
|
| 634 |
+
sources = download_source(corpus_source, raw, max_files=max_files)
|
| 635 |
+
else:
|
| 636 |
+
sources = sorted(Path(path) for path in source_paths)
|
| 637 |
+
if not sources:
|
| 638 |
+
raise ValueError("at least one source Parquet is required")
|
| 639 |
+
missing = [str(path) for path in sources if not path.is_file()]
|
| 640 |
+
if missing:
|
| 641 |
+
raise FileNotFoundError(f"missing source Parquets: {missing}")
|
| 642 |
+
source_keys = [source.stem for source in sources]
|
| 643 |
+
if len(set(source_keys)) != len(source_keys):
|
| 644 |
+
raise ValueError("source Parquet filenames must have unique stems")
|
| 645 |
+
|
| 646 |
+
states: list[dict[str, Any]] = []
|
| 647 |
+
resumed_sources = 0
|
| 648 |
+
for source in sources:
|
| 649 |
+
state, resumed = _encode_source(
|
| 650 |
+
source,
|
| 651 |
+
corpus_source=corpus_source,
|
| 652 |
+
tokenizer_path=tokenizer,
|
| 653 |
+
output_dir=destination,
|
| 654 |
+
batch_size=batch_size,
|
| 655 |
+
validation_modulus=validation_modulus,
|
| 656 |
+
validation_bucket=validation_bucket,
|
| 657 |
+
)
|
| 658 |
+
states.append(state)
|
| 659 |
+
resumed_sources += int(resumed)
|
| 660 |
+
|
| 661 |
+
manifest_paths = {
|
| 662 |
+
"train": destination / "train.manifest.json",
|
| 663 |
+
"validation": destination / "validation.manifest.json",
|
| 664 |
+
}
|
| 665 |
+
for split, manifest_path in manifest_paths.items():
|
| 666 |
+
manifest = _build_manifest(
|
| 667 |
+
split, # type: ignore[arg-type]
|
| 668 |
+
corpus_source=corpus_source,
|
| 669 |
+
source_paths=sources,
|
| 670 |
+
source_states=states,
|
| 671 |
+
validation_modulus=validation_modulus,
|
| 672 |
+
validation_bucket=validation_bucket,
|
| 673 |
+
)
|
| 674 |
+
_atomic_json(manifest_path, manifest)
|
| 675 |
+
return CorpusPreparationResult(
|
| 676 |
+
train_manifest=manifest_paths["train"],
|
| 677 |
+
validation_manifest=manifest_paths["validation"],
|
| 678 |
+
processed_sources=len(sources) - resumed_sources,
|
| 679 |
+
resumed_sources=resumed_sources,
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def prepare_fineweb_edu(
|
| 684 |
+
tokenizer_path: str | Path,
|
| 685 |
+
output_dir: str | Path,
|
| 686 |
+
*,
|
| 687 |
+
source_paths: Iterable[str | Path] | None = None,
|
| 688 |
+
raw_dir: str | Path | None = None,
|
| 689 |
+
batch_size: int = 256,
|
| 690 |
+
validation_modulus: int = 1024,
|
| 691 |
+
validation_bucket: int = 0,
|
| 692 |
+
) -> CorpusPreparationResult:
|
| 693 |
+
"""Convert pinned FineWeb-Edu Parquets into manifests (compatibility entry point)."""
|
| 694 |
+
|
| 695 |
+
return prepare_corpus(
|
| 696 |
+
tokenizer_path,
|
| 697 |
+
output_dir,
|
| 698 |
+
corpus_source=CORPUS_SOURCES["fineweb-edu"],
|
| 699 |
+
source_paths=source_paths,
|
| 700 |
+
raw_dir=raw_dir,
|
| 701 |
+
batch_size=batch_size,
|
| 702 |
+
validation_modulus=validation_modulus,
|
| 703 |
+
validation_bucket=validation_bucket,
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def parse_token_budget(text: str) -> int:
|
| 708 |
+
"""Parse a token count with an optional K/M/B suffix (e.g. ``2.5B``, ``500M``)."""
|
| 709 |
+
|
| 710 |
+
value = text.strip().upper()
|
| 711 |
+
factor = 1
|
| 712 |
+
if value and value[-1] in "KMB":
|
| 713 |
+
factor = {"K": 1_000, "M": 1_000_000, "B": 1_000_000_000}[value[-1]]
|
| 714 |
+
value = value[:-1]
|
| 715 |
+
try:
|
| 716 |
+
tokens = int(float(value) * factor)
|
| 717 |
+
except ValueError as exc:
|
| 718 |
+
raise ValueError(f"invalid token budget {text!r}") from exc
|
| 719 |
+
if tokens <= 0:
|
| 720 |
+
raise ValueError(f"token budget must be positive: {text!r}")
|
| 721 |
+
return tokens
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def mix_manifests(
|
| 725 |
+
inputs: Sequence[tuple[Path, int | None]], output_dir: str | Path
|
| 726 |
+
) -> tuple[Path, Path]:
|
| 727 |
+
"""Combine prepared source directories into one mixture manifest pair.
|
| 728 |
+
|
| 729 |
+
Each input contributes whole train shards in manifest order until its token budget
|
| 730 |
+
is met (``None`` takes everything), so realized counts overshoot a budget by at most
|
| 731 |
+
one shard; the overshoot is printed, never silent. Validation includes the
|
| 732 |
+
validation shards of the sources whose train shards were selected. Shard paths in
|
| 733 |
+
the mixed manifests are absolute so the inputs can live anywhere.
|
| 734 |
+
"""
|
| 735 |
+
|
| 736 |
+
from diffusion_lm.data import load_packed_manifest
|
| 737 |
+
|
| 738 |
+
if not inputs:
|
| 739 |
+
raise ValueError("at least one input directory is required")
|
| 740 |
+
destination = Path(output_dir)
|
| 741 |
+
destination.mkdir(parents=True, exist_ok=True)
|
| 742 |
+
compatible_keys = (
|
| 743 |
+
"dtype",
|
| 744 |
+
"vocab_size",
|
| 745 |
+
"mask_token_id",
|
| 746 |
+
"eos_token_id",
|
| 747 |
+
"special_token_ids",
|
| 748 |
+
"tokenizer_sha256",
|
| 749 |
+
)
|
| 750 |
+
reference: dict[str, Any] | None = None
|
| 751 |
+
shards: dict[str, list[dict[str, Any]]] = {"train": [], "validation": []}
|
| 752 |
+
components: list[dict[str, Any]] = []
|
| 753 |
+
for input_dir, budget in inputs:
|
| 754 |
+
manifests = {
|
| 755 |
+
split: load_packed_manifest(input_dir / f"{split}.manifest.json")
|
| 756 |
+
for split in ("train", "validation")
|
| 757 |
+
}
|
| 758 |
+
if reference is None:
|
| 759 |
+
reference = manifests["train"]
|
| 760 |
+
elif any(
|
| 761 |
+
manifests["train"].get(key) != reference.get(key) for key in compatible_keys
|
| 762 |
+
):
|
| 763 |
+
raise ValueError(f"{input_dir} uses an incompatible tokenizer or token format")
|
| 764 |
+
|
| 765 |
+
taken = 0
|
| 766 |
+
selected_sources: set[str] = set()
|
| 767 |
+
skipped = 0
|
| 768 |
+
for shard in manifests["train"]["shards"]:
|
| 769 |
+
if budget is not None and taken >= budget:
|
| 770 |
+
skipped += 1
|
| 771 |
+
continue
|
| 772 |
+
entry = dict(shard)
|
| 773 |
+
entry["path"] = str((input_dir / entry["path"]).resolve())
|
| 774 |
+
shards["train"].append(entry)
|
| 775 |
+
taken += int(entry["token_count"])
|
| 776 |
+
selected_sources.add(str(entry.get("source")))
|
| 777 |
+
for shard in manifests["validation"]["shards"]:
|
| 778 |
+
if str(shard.get("source")) not in selected_sources:
|
| 779 |
+
continue
|
| 780 |
+
entry = dict(shard)
|
| 781 |
+
entry["path"] = str((input_dir / entry["path"]).resolve())
|
| 782 |
+
shards["validation"].append(entry)
|
| 783 |
+
component = {
|
| 784 |
+
"dataset": manifests["train"].get("dataset"),
|
| 785 |
+
"directory": str(Path(input_dir).resolve()),
|
| 786 |
+
"token_budget": budget,
|
| 787 |
+
"train_tokens": taken,
|
| 788 |
+
"skipped_shards": skipped,
|
| 789 |
+
}
|
| 790 |
+
components.append(component)
|
| 791 |
+
print(
|
| 792 |
+
f'{input_dir}: {taken:,} train tokens'
|
| 793 |
+
+ (f" (budget {budget:,}, {skipped} shards skipped)" if budget else "")
|
| 794 |
+
)
|
| 795 |
+
|
| 796 |
+
assert reference is not None
|
| 797 |
+
manifest_paths: dict[str, Path] = {}
|
| 798 |
+
for split in ("train", "validation"):
|
| 799 |
+
manifest = {
|
| 800 |
+
"format": PACKED_MANIFEST_FORMAT,
|
| 801 |
+
"split": split,
|
| 802 |
+
"dtype": reference["dtype"],
|
| 803 |
+
"token_count": sum(int(shard["token_count"]) for shard in shards[split]),
|
| 804 |
+
"document_count": sum(int(shard["document_count"]) for shard in shards[split]),
|
| 805 |
+
"vocab_size": reference["vocab_size"],
|
| 806 |
+
"mask_token_id": reference["mask_token_id"],
|
| 807 |
+
"eos_token_id": reference["eos_token_id"],
|
| 808 |
+
"special_token_ids": reference["special_token_ids"],
|
| 809 |
+
"tokenizer_sha256": reference["tokenizer_sha256"],
|
| 810 |
+
"dataset": {"name": "mixture", "components": components},
|
| 811 |
+
"split_rule": reference.get("split_rule"),
|
| 812 |
+
"source_files": [shard.get("source") for shard in shards[split]],
|
| 813 |
+
"shards": shards[split],
|
| 814 |
+
}
|
| 815 |
+
manifest_paths[split] = destination / f"{split}.manifest.json"
|
| 816 |
+
_atomic_json(manifest_paths[split], manifest)
|
| 817 |
+
return manifest_paths["train"], manifest_paths["validation"]
|
| 818 |
+
|
| 819 |
+
|
| 820 |
+
def _parse_mix_input(text: str) -> tuple[Path, int | None]:
|
| 821 |
+
directory, separator, budget = text.partition("=")
|
| 822 |
+
return Path(directory), parse_token_budget(budget) if separator else None
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 826 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 827 |
+
commands = parser.add_subparsers(dest="command", required=True)
|
| 828 |
+
source_names = tuple(CORPUS_SOURCES)
|
| 829 |
+
|
| 830 |
+
download = commands.add_parser("download", help="download a pinned source's Parquets")
|
| 831 |
+
download.add_argument("--raw-dir", type=Path, required=True)
|
| 832 |
+
download.add_argument("--source", choices=source_names, default="fineweb-edu")
|
| 833 |
+
download.add_argument("--max-files", type=int)
|
| 834 |
+
download.add_argument("--max-workers", type=int, default=1)
|
| 835 |
+
|
| 836 |
+
tokenizer = commands.add_parser(
|
| 837 |
+
"train-tokenizer", help="train a 32K tokenizer from cached Parquets"
|
| 838 |
+
)
|
| 839 |
+
tokenizer.add_argument("--raw-dir", type=Path, required=True)
|
| 840 |
+
tokenizer.add_argument("--source", choices=source_names, default="fineweb-edu")
|
| 841 |
+
tokenizer.add_argument("--output", type=Path, required=True)
|
| 842 |
+
tokenizer.add_argument("--sample-bytes", type=int, default=1 << 29)
|
| 843 |
+
tokenizer.add_argument("--vocab-size", type=int, default=32_768)
|
| 844 |
+
tokenizer.add_argument("--min-frequency", type=int, default=10)
|
| 845 |
+
tokenizer.add_argument("--max-token-length", type=int, default=64)
|
| 846 |
+
tokenizer.add_argument("--batch-size", type=int, default=512)
|
| 847 |
+
|
| 848 |
+
prepare = commands.add_parser("prepare", help="encode cached Parquets into token shards")
|
| 849 |
+
prepare.add_argument("--raw-dir", type=Path, required=True)
|
| 850 |
+
prepare.add_argument("--source", choices=source_names, default="fineweb-edu")
|
| 851 |
+
prepare.add_argument("--tokenizer", type=Path, required=True)
|
| 852 |
+
prepare.add_argument("--output-dir", type=Path, required=True)
|
| 853 |
+
prepare.add_argument("--batch-size", type=int, default=256)
|
| 854 |
+
prepare.add_argument("--validation-modulus", type=int, default=1024)
|
| 855 |
+
prepare.add_argument("--validation-bucket", type=int, default=0)
|
| 856 |
+
|
| 857 |
+
mix = commands.add_parser(
|
| 858 |
+
"mix", help="combine prepared source directories into one mixture manifest"
|
| 859 |
+
)
|
| 860 |
+
mix.add_argument(
|
| 861 |
+
"--input",
|
| 862 |
+
action="append",
|
| 863 |
+
required=True,
|
| 864 |
+
metavar="DIR[=TOKENS]",
|
| 865 |
+
help="prepared corpus directory with an optional train-token budget (e.g. 3.2B)",
|
| 866 |
+
)
|
| 867 |
+
mix.add_argument("--output-dir", type=Path, required=True)
|
| 868 |
+
return parser
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
def main() -> None:
|
| 872 |
+
args = _build_parser().parse_args()
|
| 873 |
+
if args.command == "download":
|
| 874 |
+
source = CORPUS_SOURCES[args.source]
|
| 875 |
+
paths = download_source(
|
| 876 |
+
source, args.raw_dir, max_files=args.max_files, max_workers=args.max_workers
|
| 877 |
+
)
|
| 878 |
+
print(f"downloaded {len(paths)} Parquet shards below {args.raw_dir}")
|
| 879 |
+
elif args.command == "train-tokenizer":
|
| 880 |
+
source = CORPUS_SOURCES[args.source]
|
| 881 |
+
paths = find_local_parquets(args.raw_dir, source.path_prefix)
|
| 882 |
+
tokenizer = train_tokenizer_from_iterator(
|
| 883 |
+
iter_tokenizer_text(
|
| 884 |
+
paths,
|
| 885 |
+
max_utf8_bytes=args.sample_bytes,
|
| 886 |
+
batch_size=args.batch_size,
|
| 887 |
+
text_column=source.text_column,
|
| 888 |
+
id_column=source.id_column,
|
| 889 |
+
),
|
| 890 |
+
args.output,
|
| 891 |
+
vocab_size=args.vocab_size,
|
| 892 |
+
min_frequency=args.min_frequency,
|
| 893 |
+
max_token_length=args.max_token_length,
|
| 894 |
+
)
|
| 895 |
+
print(f"saved {tokenizer.get_vocab_size():,}-token tokenizer to {args.output}")
|
| 896 |
+
elif args.command == "prepare":
|
| 897 |
+
source = CORPUS_SOURCES[args.source]
|
| 898 |
+
paths = find_local_parquets(args.raw_dir, source.path_prefix)
|
| 899 |
+
result = prepare_corpus(
|
| 900 |
+
args.tokenizer,
|
| 901 |
+
args.output_dir,
|
| 902 |
+
corpus_source=source,
|
| 903 |
+
source_paths=paths,
|
| 904 |
+
batch_size=args.batch_size,
|
| 905 |
+
validation_modulus=args.validation_modulus,
|
| 906 |
+
validation_bucket=args.validation_bucket,
|
| 907 |
+
)
|
| 908 |
+
print(
|
| 909 |
+
f"prepared {result.processed_sources} sources "
|
| 910 |
+
f"({result.resumed_sources} resumed); train={result.train_manifest}, "
|
| 911 |
+
f"validation={result.validation_manifest}"
|
| 912 |
+
)
|
| 913 |
+
elif args.command == "mix":
|
| 914 |
+
inputs = [_parse_mix_input(item) for item in args.input]
|
| 915 |
+
train_manifest, validation_manifest = mix_manifests(inputs, args.output_dir)
|
| 916 |
+
print(f"mixed manifests: train={train_manifest}, validation={validation_manifest}")
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
if __name__ == "__main__":
|
| 920 |
+
main()
|
src/diffusion_lm/data.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Memory-mapped packed-token datasets and deterministic sampling."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
from bisect import bisect_right
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
from torch import Tensor
|
| 14 |
+
from torch.utils.data import Dataset, Sampler
|
| 15 |
+
|
| 16 |
+
from diffusion_lm.tokenizer import token_metadata_path
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
PACKED_TOKEN_FORMAT = "mini-diffusion-lm-packed-tokens-v1"
|
| 20 |
+
PACKED_MANIFEST_FORMAT = "mini-diffusion-lm-packed-manifest-v1"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _read_json(path: Path) -> dict[str, Any]:
|
| 24 |
+
try:
|
| 25 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 26 |
+
value = json.load(handle)
|
| 27 |
+
except json.JSONDecodeError as exc:
|
| 28 |
+
raise ValueError(f"invalid JSON in {path}: {exc}") from exc
|
| 29 |
+
if not isinstance(value, dict):
|
| 30 |
+
raise ValueError(f"expected a JSON object in {path}")
|
| 31 |
+
return value
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_token_metadata(path: str | Path) -> dict[str, Any]:
|
| 35 |
+
metadata_path = token_metadata_path(path)
|
| 36 |
+
if not metadata_path.is_file():
|
| 37 |
+
raise FileNotFoundError(
|
| 38 |
+
f"token metadata not found: {metadata_path}; encode data with mini-mdlm-tokenizer"
|
| 39 |
+
)
|
| 40 |
+
metadata = _read_json(metadata_path)
|
| 41 |
+
if metadata.get("format") != PACKED_TOKEN_FORMAT:
|
| 42 |
+
raise ValueError(f"unsupported token file metadata in {metadata_path}")
|
| 43 |
+
return metadata
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_packed_manifest(path: str | Path) -> dict[str, Any]:
|
| 47 |
+
manifest_path = Path(path)
|
| 48 |
+
if not manifest_path.is_file():
|
| 49 |
+
raise FileNotFoundError(f"packed-token manifest not found: {manifest_path}")
|
| 50 |
+
manifest = _read_json(manifest_path)
|
| 51 |
+
if manifest.get("format") != PACKED_MANIFEST_FORMAT:
|
| 52 |
+
raise ValueError(f"unsupported packed-token manifest in {manifest_path}")
|
| 53 |
+
shards = manifest.get("shards")
|
| 54 |
+
if not isinstance(shards, list) or not shards:
|
| 55 |
+
raise ValueError(f"packed-token manifest has no shards: {manifest_path}")
|
| 56 |
+
return manifest
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _validate_dtype(name: object, *, context: Path) -> np.dtype[Any]:
|
| 60 |
+
try:
|
| 61 |
+
dtype = np.dtype(name)
|
| 62 |
+
except TypeError as exc:
|
| 63 |
+
raise ValueError(f"invalid token dtype {name!r} in {context}") from exc
|
| 64 |
+
if dtype not in (np.dtype("uint16"), np.dtype("uint32")):
|
| 65 |
+
raise ValueError(f"unsupported token dtype {dtype.name!r} in {context}")
|
| 66 |
+
return dtype
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class PackedTokenDataset(Dataset[Tensor]):
|
| 70 |
+
"""Expose deterministic non-overlapping blocks from one packed token file."""
|
| 71 |
+
|
| 72 |
+
def __init__(self, path: str | Path, sequence_length: int) -> None:
|
| 73 |
+
if sequence_length <= 0:
|
| 74 |
+
raise ValueError("sequence_length must be positive")
|
| 75 |
+
self.path = Path(path)
|
| 76 |
+
if not self.path.is_file():
|
| 77 |
+
raise FileNotFoundError(f"packed token file not found: {self.path}")
|
| 78 |
+
self.metadata = load_token_metadata(self.path)
|
| 79 |
+
self.sequence_length = sequence_length
|
| 80 |
+
self._dtype = _validate_dtype(self.metadata.get("dtype"), context=self.path)
|
| 81 |
+
self._tokens = np.memmap(self.path, mode="r", dtype=self._dtype)
|
| 82 |
+
expected_count = int(self.metadata["token_count"])
|
| 83 |
+
if self._tokens.size != expected_count:
|
| 84 |
+
raise ValueError(
|
| 85 |
+
f"metadata says {expected_count} tokens but {self.path} contains "
|
| 86 |
+
f"{self._tokens.size}"
|
| 87 |
+
)
|
| 88 |
+
self._blocks = self._tokens.size // sequence_length
|
| 89 |
+
if self._blocks == 0:
|
| 90 |
+
raise ValueError(
|
| 91 |
+
f"dataset has {self._tokens.size} tokens, fewer than one "
|
| 92 |
+
f"{sequence_length}-token block"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def __len__(self) -> int:
|
| 96 |
+
return self._blocks
|
| 97 |
+
|
| 98 |
+
def __getitem__(self, index: int) -> Tensor:
|
| 99 |
+
if index < 0:
|
| 100 |
+
index += self._blocks
|
| 101 |
+
if not 0 <= index < self._blocks:
|
| 102 |
+
raise IndexError(index)
|
| 103 |
+
start = index * self.sequence_length
|
| 104 |
+
block = np.asarray(self._tokens[start : start + self.sequence_length]).astype(
|
| 105 |
+
np.int64, copy=True
|
| 106 |
+
)
|
| 107 |
+
return torch.from_numpy(block)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class ManifestPackedTokenDataset(Dataset[Tensor]):
|
| 111 |
+
"""Expose one logical dataset backed by independently memory-mapped token shards.
|
| 112 |
+
|
| 113 |
+
Blocks never cross shard boundaries. At most ``sequence_length - 1`` trailing tokens per shard
|
| 114 |
+
are ignored, keeping source shards independently replaceable and resumable.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
def __init__(self, path: str | Path, sequence_length: int) -> None:
|
| 118 |
+
if sequence_length <= 0:
|
| 119 |
+
raise ValueError("sequence_length must be positive")
|
| 120 |
+
self.path = Path(path)
|
| 121 |
+
self.metadata = load_packed_manifest(self.path)
|
| 122 |
+
self.sequence_length = sequence_length
|
| 123 |
+
self._dtype = _validate_dtype(self.metadata.get("dtype"), context=self.path)
|
| 124 |
+
|
| 125 |
+
declared_token_count = int(self.metadata.get("token_count", -1))
|
| 126 |
+
declared_document_count = int(self.metadata.get("document_count", -1))
|
| 127 |
+
shard_token_count = 0
|
| 128 |
+
shard_document_count = 0
|
| 129 |
+
self._shard_paths: list[Path] = []
|
| 130 |
+
self._shard_token_counts: list[int] = []
|
| 131 |
+
self._block_ends: list[int] = []
|
| 132 |
+
total_blocks = 0
|
| 133 |
+
|
| 134 |
+
for position, raw_shard in enumerate(self.metadata["shards"]):
|
| 135 |
+
if not isinstance(raw_shard, dict):
|
| 136 |
+
raise ValueError(f"shard {position} in {self.path} is not an object")
|
| 137 |
+
raw_path = raw_shard.get("path")
|
| 138 |
+
if not isinstance(raw_path, str) or not raw_path:
|
| 139 |
+
raise ValueError(f"shard {position} in {self.path} has no path")
|
| 140 |
+
shard_path = Path(raw_path)
|
| 141 |
+
if not shard_path.is_absolute():
|
| 142 |
+
shard_path = self.path.parent / shard_path
|
| 143 |
+
if not shard_path.is_file():
|
| 144 |
+
raise FileNotFoundError(f"packed token shard not found: {shard_path}")
|
| 145 |
+
|
| 146 |
+
token_count = int(raw_shard.get("token_count", -1))
|
| 147 |
+
document_count = int(raw_shard.get("document_count", -1))
|
| 148 |
+
if token_count < 0 or document_count < 0:
|
| 149 |
+
raise ValueError(f"invalid counts for shard {shard_path}")
|
| 150 |
+
expected_bytes = token_count * self._dtype.itemsize
|
| 151 |
+
if shard_path.stat().st_size != expected_bytes:
|
| 152 |
+
raise ValueError(
|
| 153 |
+
f"manifest says {token_count} tokens but {shard_path} has "
|
| 154 |
+
f"{shard_path.stat().st_size} bytes"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
shard_token_count += token_count
|
| 158 |
+
shard_document_count += document_count
|
| 159 |
+
blocks = token_count // sequence_length
|
| 160 |
+
if blocks:
|
| 161 |
+
self._shard_paths.append(shard_path)
|
| 162 |
+
self._shard_token_counts.append(token_count)
|
| 163 |
+
total_blocks += blocks
|
| 164 |
+
self._block_ends.append(total_blocks)
|
| 165 |
+
|
| 166 |
+
if shard_token_count != declared_token_count:
|
| 167 |
+
raise ValueError(
|
| 168 |
+
f"manifest token_count is {declared_token_count}, shard total is "
|
| 169 |
+
f"{shard_token_count}"
|
| 170 |
+
)
|
| 171 |
+
if shard_document_count != declared_document_count:
|
| 172 |
+
raise ValueError(
|
| 173 |
+
f"manifest document_count is {declared_document_count}, shard total is "
|
| 174 |
+
f"{shard_document_count}"
|
| 175 |
+
)
|
| 176 |
+
if total_blocks == 0:
|
| 177 |
+
raise ValueError(
|
| 178 |
+
f"dataset has no shard containing a full {sequence_length}-token block"
|
| 179 |
+
)
|
| 180 |
+
self._blocks = total_blocks
|
| 181 |
+
self._maps: list[np.memmap[Any, Any] | None] = [None] * len(self._shard_paths)
|
| 182 |
+
|
| 183 |
+
def __len__(self) -> int:
|
| 184 |
+
return self._blocks
|
| 185 |
+
|
| 186 |
+
def _map(self, shard_index: int) -> np.memmap[Any, Any]:
|
| 187 |
+
tokens = self._maps[shard_index]
|
| 188 |
+
if tokens is None:
|
| 189 |
+
tokens = np.memmap(self._shard_paths[shard_index], mode="r", dtype=self._dtype)
|
| 190 |
+
self._maps[shard_index] = tokens
|
| 191 |
+
return tokens
|
| 192 |
+
|
| 193 |
+
def __getitem__(self, index: int) -> Tensor:
|
| 194 |
+
if index < 0:
|
| 195 |
+
index += self._blocks
|
| 196 |
+
if not 0 <= index < self._blocks:
|
| 197 |
+
raise IndexError(index)
|
| 198 |
+
shard_index = bisect_right(self._block_ends, index)
|
| 199 |
+
previous_end = 0 if shard_index == 0 else self._block_ends[shard_index - 1]
|
| 200 |
+
local_block = index - previous_end
|
| 201 |
+
start = local_block * self.sequence_length
|
| 202 |
+
tokens = self._map(shard_index)
|
| 203 |
+
block = np.asarray(tokens[start : start + self.sequence_length]).astype(
|
| 204 |
+
np.int64, copy=True
|
| 205 |
+
)
|
| 206 |
+
return torch.from_numpy(block)
|
| 207 |
+
|
| 208 |
+
def __getstate__(self) -> dict[str, Any]:
|
| 209 |
+
state = self.__dict__.copy()
|
| 210 |
+
# Reopen mappings inside each DataLoader worker rather than pickling file descriptors.
|
| 211 |
+
state["_maps"] = [None] * len(self._shard_paths)
|
| 212 |
+
return state
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
PackedDataset = PackedTokenDataset | ManifestPackedTokenDataset
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def load_packed_dataset(path: str | Path, sequence_length: int) -> PackedDataset:
|
| 219 |
+
"""Load a legacy single-file dataset or a manifest-backed sharded dataset."""
|
| 220 |
+
|
| 221 |
+
candidate = Path(path)
|
| 222 |
+
if candidate.suffix == ".json" and candidate.is_file():
|
| 223 |
+
value = _read_json(candidate)
|
| 224 |
+
if value.get("format") == PACKED_MANIFEST_FORMAT:
|
| 225 |
+
return ManifestPackedTokenDataset(candidate, sequence_length)
|
| 226 |
+
return PackedTokenDataset(candidate, sequence_length)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
_UINT64_MASK = (1 << 64) - 1
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _splitmix64(value: int) -> int:
|
| 233 |
+
value = (value + 0x9E3779B97F4A7C15) & _UINT64_MASK
|
| 234 |
+
value = ((value ^ (value >> 30)) * 0xBF58476D1CE4E5B9) & _UINT64_MASK
|
| 235 |
+
value = ((value ^ (value >> 27)) * 0x94D049BB133111EB) & _UINT64_MASK
|
| 236 |
+
return value ^ (value >> 31)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def _affine_permutation_parameters(size: int, seed: int, epoch: int) -> tuple[int, int]:
|
| 240 |
+
"""Return ``a, b`` for the bijection ``(a*x+b) mod size``."""
|
| 241 |
+
|
| 242 |
+
if size == 1:
|
| 243 |
+
return 0, 0
|
| 244 |
+
mixed = _splitmix64((seed & _UINT64_MASK) ^ _splitmix64(epoch & _UINT64_MASK))
|
| 245 |
+
offset = mixed % size
|
| 246 |
+
multiplier = _splitmix64(mixed) % size
|
| 247 |
+
if multiplier == 0:
|
| 248 |
+
multiplier = 1
|
| 249 |
+
while math.gcd(multiplier, size) != 1:
|
| 250 |
+
multiplier += 1
|
| 251 |
+
if multiplier == size:
|
| 252 |
+
multiplier = 1
|
| 253 |
+
return multiplier, offset
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class DeterministicBatchSampler(Sampler[list[int]]):
|
| 257 |
+
"""Infinite epoch permutations with exact batch-cursor resume and O(1) memory.
|
| 258 |
+
|
| 259 |
+
Each epoch uses a seeded affine bijection over dataset indexes. Unlike ``randperm().tolist()``,
|
| 260 |
+
memory use is independent of corpus size and a resumed batch can be calculated directly.
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
def __init__(
|
| 264 |
+
self,
|
| 265 |
+
dataset_size: int,
|
| 266 |
+
batch_size: int,
|
| 267 |
+
*,
|
| 268 |
+
seed: int,
|
| 269 |
+
start_batch: int = 0,
|
| 270 |
+
) -> None:
|
| 271 |
+
if dataset_size <= 0 or batch_size <= 0:
|
| 272 |
+
raise ValueError("dataset_size and batch_size must be positive")
|
| 273 |
+
if start_batch < 0:
|
| 274 |
+
raise ValueError("start_batch must be non-negative")
|
| 275 |
+
self.dataset_size = dataset_size
|
| 276 |
+
self.batch_size = batch_size
|
| 277 |
+
self.seed = seed
|
| 278 |
+
self.start_batch = start_batch
|
| 279 |
+
self.batches_per_epoch = (dataset_size + batch_size - 1) // batch_size
|
| 280 |
+
|
| 281 |
+
def __iter__(self):
|
| 282 |
+
epoch = self.start_batch // self.batches_per_epoch
|
| 283 |
+
batch_in_epoch = self.start_batch % self.batches_per_epoch
|
| 284 |
+
while True:
|
| 285 |
+
multiplier, offset = _affine_permutation_parameters(
|
| 286 |
+
self.dataset_size, self.seed, epoch
|
| 287 |
+
)
|
| 288 |
+
for batch_index in range(batch_in_epoch, self.batches_per_epoch):
|
| 289 |
+
start = batch_index * self.batch_size
|
| 290 |
+
stop = min(start + self.batch_size, self.dataset_size)
|
| 291 |
+
yield [
|
| 292 |
+
(multiplier * position + offset) % self.dataset_size
|
| 293 |
+
for position in range(start, stop)
|
| 294 |
+
]
|
| 295 |
+
epoch += 1
|
| 296 |
+
batch_in_epoch = 0
|
src/diffusion_lm/diffusion.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Absorbing-mask forward corruption, objective, and reverse samplers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from collections.abc import Iterator
|
| 7 |
+
from typing import Callable, Literal, Protocol
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Denoiser(Protocol):
|
| 15 |
+
config: object
|
| 16 |
+
|
| 17 |
+
def __call__(
|
| 18 |
+
self,
|
| 19 |
+
input_ids: Tensor,
|
| 20 |
+
attention_mask: Tensor | None = None,
|
| 21 |
+
output_positions: Tensor | None = None,
|
| 22 |
+
attn_mask: Tensor | None = None,
|
| 23 |
+
) -> Tensor: ...
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True)
|
| 27 |
+
class CorruptionBatch:
|
| 28 |
+
noisy_tokens: Tensor
|
| 29 |
+
mask: Tensor
|
| 30 |
+
mask_probability: Tensor
|
| 31 |
+
valid_mask: Tensor
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class LossOutput:
|
| 36 |
+
loss: Tensor
|
| 37 |
+
masked_accuracy: Tensor
|
| 38 |
+
masked_tokens: int
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass(frozen=True)
|
| 42 |
+
class UnmaskStep:
|
| 43 |
+
"""One observable state of the reverse diffusion process."""
|
| 44 |
+
|
| 45 |
+
step: int
|
| 46 |
+
total_steps: int
|
| 47 |
+
tokens: Tensor
|
| 48 |
+
masked_remaining: int
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def sample_mask_probabilities(
|
| 52 |
+
batch_size: int,
|
| 53 |
+
*,
|
| 54 |
+
device: torch.device | str,
|
| 55 |
+
eps: float = 1e-3,
|
| 56 |
+
low_discrepancy: bool = True,
|
| 57 |
+
generator: torch.Generator | None = None,
|
| 58 |
+
) -> Tensor:
|
| 59 |
+
"""Sample linear noise levels in ``[eps, 1]``.
|
| 60 |
+
|
| 61 |
+
A random cyclic shift of an evenly spaced grid preserves uniform marginals
|
| 62 |
+
while covering the complete noise range in every reasonably sized batch.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
if batch_size <= 0:
|
| 66 |
+
raise ValueError("batch_size must be positive")
|
| 67 |
+
if not 0.0 < eps < 1.0:
|
| 68 |
+
raise ValueError("eps must be in (0, 1)")
|
| 69 |
+
|
| 70 |
+
if low_discrepancy:
|
| 71 |
+
offset = torch.rand((), device=device, generator=generator)
|
| 72 |
+
unit = (offset + torch.arange(batch_size, device=device) / batch_size) % 1.0
|
| 73 |
+
else:
|
| 74 |
+
unit = torch.rand(batch_size, device=device, generator=generator)
|
| 75 |
+
return eps + (1.0 - eps) * unit
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def corrupt_tokens(
|
| 79 |
+
clean_tokens: Tensor,
|
| 80 |
+
mask_token_id: int,
|
| 81 |
+
*,
|
| 82 |
+
valid_mask: Tensor | None = None,
|
| 83 |
+
mask_probability: Tensor | None = None,
|
| 84 |
+
eps: float = 1e-3,
|
| 85 |
+
low_discrepancy: bool = True,
|
| 86 |
+
generator: torch.Generator | None = None,
|
| 87 |
+
) -> CorruptionBatch:
|
| 88 |
+
"""Apply the absorbing forward process at one random time per sequence."""
|
| 89 |
+
|
| 90 |
+
if clean_tokens.ndim != 2:
|
| 91 |
+
raise ValueError("clean_tokens must have shape [batch, sequence]")
|
| 92 |
+
batch_size, _ = clean_tokens.shape
|
| 93 |
+
if valid_mask is None:
|
| 94 |
+
valid_mask = torch.ones_like(clean_tokens, dtype=torch.bool)
|
| 95 |
+
elif valid_mask.shape != clean_tokens.shape:
|
| 96 |
+
raise ValueError("valid_mask must match clean_tokens")
|
| 97 |
+
else:
|
| 98 |
+
valid_mask = valid_mask.bool()
|
| 99 |
+
|
| 100 |
+
if mask_probability is None:
|
| 101 |
+
mask_probability = sample_mask_probabilities(
|
| 102 |
+
batch_size,
|
| 103 |
+
device=clean_tokens.device,
|
| 104 |
+
eps=eps,
|
| 105 |
+
low_discrepancy=low_discrepancy,
|
| 106 |
+
generator=generator,
|
| 107 |
+
)
|
| 108 |
+
else:
|
| 109 |
+
mask_probability = torch.as_tensor(
|
| 110 |
+
mask_probability, device=clean_tokens.device, dtype=torch.float32
|
| 111 |
+
)
|
| 112 |
+
if mask_probability.ndim == 0:
|
| 113 |
+
mask_probability = mask_probability.repeat(batch_size)
|
| 114 |
+
if mask_probability.shape != (batch_size,):
|
| 115 |
+
raise ValueError("mask_probability must be scalar or have shape [batch]")
|
| 116 |
+
if bool(((mask_probability <= 0) | (mask_probability > 1)).any()):
|
| 117 |
+
raise ValueError("mask probabilities must be in (0, 1]")
|
| 118 |
+
|
| 119 |
+
random_values = torch.rand(clean_tokens.shape, device=clean_tokens.device, generator=generator)
|
| 120 |
+
mask = (random_values < mask_probability[:, None]) & valid_mask
|
| 121 |
+
noisy_tokens = torch.where(mask, mask_token_id, clean_tokens)
|
| 122 |
+
return CorruptionBatch(noisy_tokens, mask, mask_probability, valid_mask)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def diffusion_cross_entropy(
|
| 126 |
+
logits: Tensor,
|
| 127 |
+
clean_tokens: Tensor,
|
| 128 |
+
corruption: CorruptionBatch,
|
| 129 |
+
) -> LossOutput:
|
| 130 |
+
"""Compute the continuous-time masked-diffusion likelihood bound.
|
| 131 |
+
|
| 132 |
+
``logits`` may contain all positions as ``[B, L, V]`` or only the masked
|
| 133 |
+
positions as ``[N_masked, V]``. The latter is substantially more memory
|
| 134 |
+
efficient for small models with non-trivial vocabularies.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
if clean_tokens.shape != corruption.noisy_tokens.shape:
|
| 138 |
+
raise ValueError("clean_tokens must match the corruption batch")
|
| 139 |
+
targets = clean_tokens[corruption.mask]
|
| 140 |
+
if logits.ndim == 3:
|
| 141 |
+
if logits.shape[:2] != clean_tokens.shape:
|
| 142 |
+
raise ValueError("full logits must have shape [batch, sequence, vocab]")
|
| 143 |
+
selected_logits = logits[corruption.mask]
|
| 144 |
+
elif logits.ndim == 2:
|
| 145 |
+
selected_logits = logits
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError("logits must have shape [B, L, V] or [N_masked, V]")
|
| 148 |
+
if selected_logits.shape[0] != targets.numel():
|
| 149 |
+
raise ValueError("selected logits count does not match the number of masked tokens")
|
| 150 |
+
|
| 151 |
+
masked_tokens = int(targets.numel())
|
| 152 |
+
if masked_tokens == 0:
|
| 153 |
+
zero = logits.sum() * 0.0
|
| 154 |
+
return LossOutput(zero, zero.detach(), 0)
|
| 155 |
+
|
| 156 |
+
per_token = F.cross_entropy(selected_logits.float(), targets, reduction="none")
|
| 157 |
+
probabilities = corruption.mask_probability[:, None].expand_as(clean_tokens)
|
| 158 |
+
weights = probabilities[corruption.mask].reciprocal()
|
| 159 |
+
normalizer = corruption.valid_mask.sum().clamp_min(1)
|
| 160 |
+
loss = (per_token * weights).sum() / normalizer
|
| 161 |
+
accuracy = (selected_logits.argmax(dim=-1) == targets).float().mean()
|
| 162 |
+
return LossOutput(loss, accuracy, masked_tokens)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _sample_categorical(
|
| 166 |
+
logits: Tensor,
|
| 167 |
+
temperature: float,
|
| 168 |
+
generator: torch.Generator | None,
|
| 169 |
+
) -> tuple[Tensor, Tensor]:
|
| 170 |
+
"""Sample with fp64 Gumbel noise and return token ids plus model confidence."""
|
| 171 |
+
|
| 172 |
+
if temperature < 0:
|
| 173 |
+
raise ValueError("temperature must be non-negative")
|
| 174 |
+
log_probs = F.log_softmax(logits.float(), dim=-1)
|
| 175 |
+
if temperature == 0:
|
| 176 |
+
tokens = logits.argmax(dim=-1)
|
| 177 |
+
else:
|
| 178 |
+
# MPS has no float64 kernels. Preserve fp64 categorical sampling by
|
| 179 |
+
# moving only the sampling calculation to CPU on Apple Silicon.
|
| 180 |
+
sampling_device = torch.device("cpu") if logits.device.type == "mps" else logits.device
|
| 181 |
+
if logits.device.type == "mps":
|
| 182 |
+
logits64 = logits.float().cpu().double() / temperature
|
| 183 |
+
else:
|
| 184 |
+
logits64 = logits.double() / temperature
|
| 185 |
+
sampling_generator = generator
|
| 186 |
+
if generator is not None and generator.device != sampling_device:
|
| 187 |
+
sampling_generator = None
|
| 188 |
+
uniform = torch.rand(
|
| 189 |
+
logits64.shape,
|
| 190 |
+
device=sampling_device,
|
| 191 |
+
dtype=torch.float64,
|
| 192 |
+
generator=sampling_generator,
|
| 193 |
+
).clamp_(1e-12, 1.0 - 1e-12)
|
| 194 |
+
gumbel = -torch.log(-torch.log(uniform))
|
| 195 |
+
tokens = (logits64 + gumbel).argmax(dim=-1).to(logits.device)
|
| 196 |
+
confidence = log_probs.gather(-1, tokens[:, None]).squeeze(-1).exp()
|
| 197 |
+
return tokens, confidence
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def iterative_unmask_steps(
|
| 201 |
+
model: Denoiser,
|
| 202 |
+
input_ids: Tensor,
|
| 203 |
+
mask_token_id: int,
|
| 204 |
+
*,
|
| 205 |
+
steps: int = 64,
|
| 206 |
+
temperature: float = 1.0,
|
| 207 |
+
strategy: Literal["ancestral", "confidence", "left_to_right"] = "ancestral",
|
| 208 |
+
blocked_token_ids: tuple[int, ...] = (),
|
| 209 |
+
attn_mask: Tensor | None = None,
|
| 210 |
+
generator: torch.Generator | None = None,
|
| 211 |
+
logits_fn: Callable[[Tensor, Tensor], Tensor] | None = None,
|
| 212 |
+
) -> Iterator[UnmaskStep]:
|
| 213 |
+
"""Yield each state while filling masks and clamping visible prompt tokens.
|
| 214 |
+
|
| 215 |
+
``ancestral`` implements the absorbing reverse transition from mask rate
|
| 216 |
+
``t`` to ``s``. ``confidence`` reveals an equal-sized highest-confidence
|
| 217 |
+
group on each pass; it is faster-looking and often useful, but is a heuristic.
|
| 218 |
+
``left_to_right`` reveals equal-sized position-ordered groups, which keeps
|
| 219 |
+
arithmetic left operands visible before their results are committed.
|
| 220 |
+
|
| 221 |
+
``logits_fn(tokens, masked)`` overrides how predictions are obtained, so a caller
|
| 222 |
+
holding a key/value cache can score only the masked window instead of the whole
|
| 223 |
+
sequence. The revealing schedule is unchanged either way.
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
if input_ids.ndim != 2:
|
| 227 |
+
raise ValueError("input_ids must have shape [batch, sequence]")
|
| 228 |
+
if steps <= 0:
|
| 229 |
+
raise ValueError("steps must be positive")
|
| 230 |
+
if strategy not in {"ancestral", "confidence", "left_to_right"}:
|
| 231 |
+
raise ValueError("strategy must be ancestral, confidence, or left_to_right")
|
| 232 |
+
|
| 233 |
+
tokens = input_ids.clone()
|
| 234 |
+
batch_size, _ = tokens.shape
|
| 235 |
+
yield UnmaskStep(0, steps, tokens.detach(), int(tokens.eq(mask_token_id).sum()))
|
| 236 |
+
|
| 237 |
+
for step in range(steps):
|
| 238 |
+
masked = tokens.eq(mask_token_id)
|
| 239 |
+
if not bool(masked.any()):
|
| 240 |
+
break
|
| 241 |
+
|
| 242 |
+
# This function is itself a generator, so a decorator would leave the
|
| 243 |
+
# inference context before iteration begins. Scope it around each pass.
|
| 244 |
+
with torch.inference_mode():
|
| 245 |
+
if logits_fn is not None:
|
| 246 |
+
logits = logits_fn(tokens, masked)
|
| 247 |
+
else:
|
| 248 |
+
# Kept as a conditional kwarg so mask-free denoiser doubles stay valid.
|
| 249 |
+
extra = {} if attn_mask is None else {"attn_mask": attn_mask}
|
| 250 |
+
logits = model(tokens, output_positions=masked, **extra)
|
| 251 |
+
if blocked_token_ids:
|
| 252 |
+
logits = logits.clone()
|
| 253 |
+
for token_id in blocked_token_ids:
|
| 254 |
+
logits[:, token_id] = torch.finfo(logits.dtype).min
|
| 255 |
+
predictions, confidence = _sample_categorical(logits, temperature, generator)
|
| 256 |
+
|
| 257 |
+
proposed = tokens.clone()
|
| 258 |
+
proposed[masked] = predictions
|
| 259 |
+
reveal = torch.zeros_like(masked)
|
| 260 |
+
steps_left = steps - step
|
| 261 |
+
|
| 262 |
+
if strategy == "ancestral":
|
| 263 |
+
# Linear t grid: P(unmask from t to s | still masked) = 1 - s/t.
|
| 264 |
+
reveal_probability = 1.0 / steps_left
|
| 265 |
+
reveal = (
|
| 266 |
+
torch.rand(tokens.shape, device=tokens.device, generator=generator)
|
| 267 |
+
< reveal_probability
|
| 268 |
+
) & masked
|
| 269 |
+
elif strategy == "left_to_right":
|
| 270 |
+
for row in range(batch_size):
|
| 271 |
+
masked_positions = masked[row].nonzero(as_tuple=True)[0]
|
| 272 |
+
remaining = int(masked_positions.numel())
|
| 273 |
+
count = (remaining + steps_left - 1) // steps_left
|
| 274 |
+
if count:
|
| 275 |
+
reveal[row, masked_positions[:count]] = True
|
| 276 |
+
else:
|
| 277 |
+
confidence_grid = torch.full(
|
| 278 |
+
tokens.shape,
|
| 279 |
+
-torch.inf,
|
| 280 |
+
device=tokens.device,
|
| 281 |
+
dtype=confidence.dtype,
|
| 282 |
+
)
|
| 283 |
+
confidence_grid[masked] = confidence
|
| 284 |
+
for row in range(batch_size):
|
| 285 |
+
remaining = int(masked[row].sum())
|
| 286 |
+
count = (remaining + steps_left - 1) // steps_left
|
| 287 |
+
if count:
|
| 288 |
+
positions = confidence_grid[row].topk(count).indices
|
| 289 |
+
reveal[row, positions] = True
|
| 290 |
+
|
| 291 |
+
tokens = torch.where(reveal, proposed, tokens)
|
| 292 |
+
|
| 293 |
+
yield UnmaskStep(
|
| 294 |
+
step + 1,
|
| 295 |
+
steps,
|
| 296 |
+
tokens.detach(),
|
| 297 |
+
int(tokens.eq(mask_token_id).sum()),
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
if bool(tokens.eq(mask_token_id).any()):
|
| 301 |
+
raise RuntimeError(
|
| 302 |
+
"sampler finished with masked positions; this indicates an internal error"
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@torch.no_grad()
|
| 307 |
+
def iterative_unmask(
|
| 308 |
+
model: Denoiser,
|
| 309 |
+
input_ids: Tensor,
|
| 310 |
+
mask_token_id: int,
|
| 311 |
+
*,
|
| 312 |
+
steps: int = 64,
|
| 313 |
+
temperature: float = 1.0,
|
| 314 |
+
strategy: Literal["ancestral", "confidence"] = "ancestral",
|
| 315 |
+
blocked_token_ids: tuple[int, ...] = (),
|
| 316 |
+
attn_mask: Tensor | None = None,
|
| 317 |
+
generator: torch.Generator | None = None,
|
| 318 |
+
logits_fn: Callable[[Tensor, Tensor], Tensor] | None = None,
|
| 319 |
+
) -> Tensor:
|
| 320 |
+
"""Return the final state from :func:`iterative_unmask_steps`."""
|
| 321 |
+
|
| 322 |
+
final_state: UnmaskStep | None = None
|
| 323 |
+
for state in iterative_unmask_steps(
|
| 324 |
+
model,
|
| 325 |
+
input_ids,
|
| 326 |
+
mask_token_id,
|
| 327 |
+
steps=steps,
|
| 328 |
+
temperature=temperature,
|
| 329 |
+
strategy=strategy,
|
| 330 |
+
blocked_token_ids=blocked_token_ids,
|
| 331 |
+
attn_mask=attn_mask,
|
| 332 |
+
generator=generator,
|
| 333 |
+
logits_fn=logits_fn,
|
| 334 |
+
):
|
| 335 |
+
final_state = state
|
| 336 |
+
if final_state is None: # Defensive: the iterator always yields its initial state.
|
| 337 |
+
raise RuntimeError("sampler produced no state")
|
| 338 |
+
return final_state.tokens
|
src/diffusion_lm/distill.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Distilled-trace preparation: reasoning traces packed with chained thought slots.
|
| 2 |
+
|
| 3 |
+
Streams a distilled-reasoning dataset (traces carrying a ``<think>...</think>`` region
|
| 4 |
+
followed by a final answer), segments each think region into a chain of thought units, and
|
| 5 |
+
renders every example through the shared slot geometry so a block-diffusion model denoises
|
| 6 |
+
one thought at a time. The number of thoughts varies per example, so slot count stands in
|
| 7 |
+
for how much the model chose to think.
|
| 8 |
+
|
| 9 |
+
Segmentation splits on paragraph breaks first, then on reasoning-marker sentence boundaries
|
| 10 |
+
inside long paragraphs, so a thought is a coherent reasoning move rather than a fixed-size
|
| 11 |
+
cut.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import hashlib
|
| 18 |
+
import json
|
| 19 |
+
import re
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Iterator
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
from diffusion_lm.reasoning import (
|
| 26 |
+
ADAPTIVE_SPECIAL_TOKENS,
|
| 27 |
+
ExampleEncoder,
|
| 28 |
+
LayoutSpec,
|
| 29 |
+
REASONING_SPECIAL_TOKENS,
|
| 30 |
+
ReasoningExample,
|
| 31 |
+
size_token_ids,
|
| 32 |
+
_write_packed,
|
| 33 |
+
)
|
| 34 |
+
from diffusion_lm.tokenizer import load_tokenizer, train_tokenizer_from_iterator
|
| 35 |
+
|
| 36 |
+
_THINK_RE = re.compile(r'<think>(.*?)</think>\s*', re.DOTALL)
|
| 37 |
+
_MARKER_BREAK = re.compile(
|
| 38 |
+
r'(?<=[.!?])\s+(?=(?:Wait|Alternatively|Hmm|Okay|Now|But wait|Let me|First|Second|'
|
| 39 |
+
r'Next|Then|Finally|So,|Also|Another|Actually|Therefore|Thus)\b)'
|
| 40 |
+
)
|
| 41 |
+
_LONG_PARAGRAPH_CHARS = 900
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def segment_thoughts(think: str) -> list[str]:
|
| 45 |
+
"""Split a think region into a chain of coherent thought units."""
|
| 46 |
+
|
| 47 |
+
thoughts: list[str] = []
|
| 48 |
+
for paragraph in re.split(r'\n\s*\n', think):
|
| 49 |
+
paragraph = ' '.join(paragraph.split())
|
| 50 |
+
if not paragraph:
|
| 51 |
+
continue
|
| 52 |
+
if len(paragraph) > _LONG_PARAGRAPH_CHARS:
|
| 53 |
+
thoughts.extend(part.strip() for part in _MARKER_BREAK.split(paragraph) if part.strip())
|
| 54 |
+
else:
|
| 55 |
+
thoughts.append(paragraph)
|
| 56 |
+
return thoughts
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def parse_glaive(row: dict[str, object]) -> ReasoningExample | None:
|
| 60 |
+
"""Render one glaive ``reasoning-v1`` row into a chained-thought example."""
|
| 61 |
+
|
| 62 |
+
prompt = ' '.join(str(row.get('prompt') or '').split())
|
| 63 |
+
response = str(row.get('response') or '')
|
| 64 |
+
match = _THINK_RE.search(response)
|
| 65 |
+
if not prompt or not match:
|
| 66 |
+
return None
|
| 67 |
+
answer = ' '.join(response[match.end():].split())
|
| 68 |
+
thoughts = segment_thoughts(match.group(1))
|
| 69 |
+
if not answer or not thoughts:
|
| 70 |
+
return None
|
| 71 |
+
return ReasoningExample(prompt, tuple(thoughts), answer, expected_answer='')
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
PARSERS = {'glaive': parse_glaive}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _stream_examples(
|
| 78 |
+
dataset: str, split: str, parser_name: str, limit: int
|
| 79 |
+
) -> Iterator[ReasoningExample]:
|
| 80 |
+
from datasets import load_dataset
|
| 81 |
+
|
| 82 |
+
parser = PARSERS[parser_name]
|
| 83 |
+
kept = 0
|
| 84 |
+
for row in load_dataset(dataset, split=split, streaming=True):
|
| 85 |
+
example = parser(row)
|
| 86 |
+
if example is None:
|
| 87 |
+
continue
|
| 88 |
+
yield example
|
| 89 |
+
kept += 1
|
| 90 |
+
if limit and kept >= limit:
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _is_validation(problem: str, val_fraction: float) -> bool:
|
| 95 |
+
digest = hashlib.sha256(problem.encode('utf-8')).digest()
|
| 96 |
+
return int.from_bytes(digest[:4], 'big') / 2**32 < val_fraction
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _resolve_tokenizer(
|
| 100 |
+
examples: list[ReasoningExample],
|
| 101 |
+
args: argparse.Namespace,
|
| 102 |
+
special_tokens: tuple[str, ...] = REASONING_SPECIAL_TOKENS,
|
| 103 |
+
):
|
| 104 |
+
from pathlib import Path
|
| 105 |
+
|
| 106 |
+
tokenizer_path = Path(args.tokenizer)
|
| 107 |
+
if tokenizer_path.is_file():
|
| 108 |
+
print(f'reusing tokenizer {tokenizer_path}')
|
| 109 |
+
return load_tokenizer(tokenizer_path), tokenizer_path
|
| 110 |
+
|
| 111 |
+
def _texts() -> Iterator[str]:
|
| 112 |
+
for example in examples:
|
| 113 |
+
yield f'{example.problem}\n{" ".join(example.steps)}\n{example.answer}'
|
| 114 |
+
|
| 115 |
+
tokenizer = train_tokenizer_from_iterator(
|
| 116 |
+
_texts(),
|
| 117 |
+
tokenizer_path,
|
| 118 |
+
vocab_size=args.vocab_size,
|
| 119 |
+
min_frequency=4,
|
| 120 |
+
length=len(examples),
|
| 121 |
+
extra_special_tokens=special_tokens,
|
| 122 |
+
)
|
| 123 |
+
print(f'trained tokenizer {tokenizer_path} (vocab {args.vocab_size}) on {len(examples):,} traces')
|
| 124 |
+
return tokenizer, tokenizer_path
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def prepare(args: argparse.Namespace) -> None:
|
| 128 |
+
adaptive = bool(getattr(args, 'sizes', None))
|
| 129 |
+
spec = LayoutSpec(
|
| 130 |
+
seq_len=args.seq_len,
|
| 131 |
+
block=args.block,
|
| 132 |
+
max_slots=args.max_slots,
|
| 133 |
+
sizes=tuple(args.sizes) if adaptive else (),
|
| 134 |
+
)
|
| 135 |
+
output_dir = Path(args.output_dir)
|
| 136 |
+
|
| 137 |
+
# Buffer unique parsed examples so a native tokenizer can be trained in the same
|
| 138 |
+
# streaming pass the corpus is packed from, avoiding a second dataset download.
|
| 139 |
+
examples: list[ReasoningExample] = []
|
| 140 |
+
seen: set[str] = set()
|
| 141 |
+
scanned = 0
|
| 142 |
+
for example in _stream_examples(args.dataset, args.split, args.parser, args.scan_limit):
|
| 143 |
+
scanned += 1
|
| 144 |
+
key = hashlib.sha256(example.problem.encode('utf-8')).hexdigest()
|
| 145 |
+
if key in seen:
|
| 146 |
+
continue
|
| 147 |
+
seen.add(key)
|
| 148 |
+
examples.append(example)
|
| 149 |
+
if args.limit and len(examples) >= args.limit:
|
| 150 |
+
break
|
| 151 |
+
if not examples:
|
| 152 |
+
raise ValueError('no usable examples; check dataset, parser, and seq_len')
|
| 153 |
+
print(f'scanned {scanned:,}, buffered {len(examples):,} unique examples')
|
| 154 |
+
|
| 155 |
+
special_tokens = ADAPTIVE_SPECIAL_TOKENS if adaptive else REASONING_SPECIAL_TOKENS
|
| 156 |
+
tokenizer, tokenizer_path = _resolve_tokenizer(examples, args, special_tokens)
|
| 157 |
+
encoder = ExampleEncoder(tokenizer, spec)
|
| 158 |
+
|
| 159 |
+
if adaptive:
|
| 160 |
+
_pack_adaptive(examples, encoder, spec, tokenizer, tokenizer_path, output_dir, args)
|
| 161 |
+
return
|
| 162 |
+
|
| 163 |
+
split: dict[str, dict[str, list[np.ndarray]]] = {
|
| 164 |
+
'train': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
|
| 165 |
+
'validation': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
|
| 166 |
+
}
|
| 167 |
+
val_prompts: list[dict[str, str]] = []
|
| 168 |
+
dropped = 0
|
| 169 |
+
for example in examples:
|
| 170 |
+
encoded = encoder.encode_example(example)
|
| 171 |
+
if encoded is None:
|
| 172 |
+
dropped += 1
|
| 173 |
+
continue
|
| 174 |
+
is_val = _is_validation(example.problem, args.val_fraction)
|
| 175 |
+
bucket = split['validation' if is_val else 'train']
|
| 176 |
+
bucket['flat'].append(encoded.flat)
|
| 177 |
+
bucket['flat_regions'].append(encoded.flat_regions)
|
| 178 |
+
bucket['slotted'].append(encoded.slotted)
|
| 179 |
+
bucket['slotted_regions'].append(encoded.slotted_regions)
|
| 180 |
+
if is_val and len(val_prompts) < 500:
|
| 181 |
+
val_prompts.append({'problem': example.problem, 'expected_answer': ''})
|
| 182 |
+
|
| 183 |
+
total = sum(len(bucket['flat']) for bucket in split.values())
|
| 184 |
+
if not total:
|
| 185 |
+
raise ValueError('no examples survived encoding; raise seq_len or max_slots')
|
| 186 |
+
print(f'encoded {total:,} examples ({dropped:,} dropped at encode)')
|
| 187 |
+
for split_name, bucket in split.items():
|
| 188 |
+
if not bucket['flat']:
|
| 189 |
+
raise ValueError(f'no examples in the {split_name} split; lower val_fraction or scan more')
|
| 190 |
+
for layout in ('flat', 'slotted'):
|
| 191 |
+
_write_packed(
|
| 192 |
+
output_dir / f'{split_name}-{layout}.bin',
|
| 193 |
+
np.stack(bucket[layout]),
|
| 194 |
+
np.stack(bucket[f'{layout}_regions']),
|
| 195 |
+
layout=layout,
|
| 196 |
+
spec=spec,
|
| 197 |
+
tokenizer_path=tokenizer_path,
|
| 198 |
+
tokenizer=tokenizer,
|
| 199 |
+
)
|
| 200 |
+
print(f'{split_name}: {len(bucket["flat"]):,} examples -> {output_dir}')
|
| 201 |
+
|
| 202 |
+
problems_path = output_dir / 'validation-problems.jsonl'
|
| 203 |
+
with problems_path.open('w', encoding='utf-8') as handle:
|
| 204 |
+
for record in val_prompts:
|
| 205 |
+
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 206 |
+
print(f'wrote {len(val_prompts):,} validation prompts to {problems_path}')
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _pack_adaptive(
|
| 210 |
+
examples: list[ReasoningExample],
|
| 211 |
+
encoder: ExampleEncoder,
|
| 212 |
+
spec: LayoutSpec,
|
| 213 |
+
tokenizer,
|
| 214 |
+
tokenizer_path: Path,
|
| 215 |
+
output_dir: Path,
|
| 216 |
+
args: argparse.Namespace,
|
| 217 |
+
) -> None:
|
| 218 |
+
"""Encode the adaptive layout and report the block-size distribution it produces."""
|
| 219 |
+
|
| 220 |
+
split: dict[str, dict[str, list[np.ndarray]]] = {
|
| 221 |
+
'train': {'tokens': [], 'regions': []},
|
| 222 |
+
'validation': {'tokens': [], 'regions': []},
|
| 223 |
+
}
|
| 224 |
+
val_prompts: list[dict[str, str]] = []
|
| 225 |
+
size_histogram: dict[int, int] = {size: 0 for size in spec.sizes}
|
| 226 |
+
blocks_per_example: list[int] = []
|
| 227 |
+
dropped = 0
|
| 228 |
+
for example in examples:
|
| 229 |
+
encoded = encoder.encode_adaptive(example)
|
| 230 |
+
if encoded is None:
|
| 231 |
+
dropped += 1
|
| 232 |
+
continue
|
| 233 |
+
is_val = _is_validation(example.problem, args.val_fraction)
|
| 234 |
+
bucket = split['validation' if is_val else 'train']
|
| 235 |
+
bucket['tokens'].append(encoded.tokens)
|
| 236 |
+
bucket['regions'].append(encoded.regions)
|
| 237 |
+
blocks_per_example.append(len(encoded.block_sizes))
|
| 238 |
+
for size in encoded.block_sizes:
|
| 239 |
+
size_histogram[size] += 1
|
| 240 |
+
if is_val and len(val_prompts) < 500:
|
| 241 |
+
val_prompts.append({'problem': example.problem, 'expected_answer': ''})
|
| 242 |
+
|
| 243 |
+
total = sum(len(bucket['tokens']) for bucket in split.values())
|
| 244 |
+
if not total:
|
| 245 |
+
raise ValueError('no examples survived encoding; raise seq_len, max_slots, or sizes')
|
| 246 |
+
|
| 247 |
+
total_blocks = sum(size_histogram.values())
|
| 248 |
+
fractions = {
|
| 249 |
+
size: round(count / max(1, total_blocks), 4) for size, count in size_histogram.items()
|
| 250 |
+
}
|
| 251 |
+
sorted_blocks = sorted(blocks_per_example)
|
| 252 |
+
|
| 253 |
+
def percentile(fraction: float) -> int:
|
| 254 |
+
return sorted_blocks[min(len(sorted_blocks) - 1, int(len(sorted_blocks) * fraction))]
|
| 255 |
+
|
| 256 |
+
print(f'encoded {total:,} examples ({dropped:,} dropped at encode)')
|
| 257 |
+
print(f'block-size counts {size_histogram} fractions {fractions}')
|
| 258 |
+
print(
|
| 259 |
+
f'blocks/example p50 {percentile(0.5)} p90 {percentile(0.9)} '
|
| 260 |
+
f'max {sorted_blocks[-1]}'
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
extra_metadata = {
|
| 264 |
+
'sizes': list(spec.sizes),
|
| 265 |
+
'size_token_ids': size_token_ids(tokenizer),
|
| 266 |
+
}
|
| 267 |
+
for split_name, bucket in split.items():
|
| 268 |
+
if not bucket['tokens']:
|
| 269 |
+
raise ValueError(f'no examples in the {split_name} split; lower val_fraction')
|
| 270 |
+
_write_packed(
|
| 271 |
+
output_dir / f'{split_name}-adaptive.bin',
|
| 272 |
+
np.stack(bucket['tokens']),
|
| 273 |
+
np.stack(bucket['regions']),
|
| 274 |
+
layout='adaptive',
|
| 275 |
+
spec=spec,
|
| 276 |
+
tokenizer_path=tokenizer_path,
|
| 277 |
+
tokenizer=tokenizer,
|
| 278 |
+
extra_metadata=extra_metadata,
|
| 279 |
+
)
|
| 280 |
+
print(f'{split_name}: {len(bucket["tokens"]):,} examples -> {output_dir}')
|
| 281 |
+
|
| 282 |
+
problems_path = output_dir / 'validation-problems.jsonl'
|
| 283 |
+
with problems_path.open('w', encoding='utf-8') as handle:
|
| 284 |
+
for record in val_prompts:
|
| 285 |
+
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 286 |
+
print(f'wrote {len(val_prompts):,} validation prompts to {problems_path}')
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 290 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 291 |
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
| 292 |
+
prepare_parser = subparsers.add_parser(
|
| 293 |
+
'prepare', help='stream a distilled-reasoning dataset into chained-thought slots'
|
| 294 |
+
)
|
| 295 |
+
prepare_parser.add_argument('--dataset', default='glaiveai/reasoning-v1-20m')
|
| 296 |
+
prepare_parser.add_argument('--split', default='train')
|
| 297 |
+
prepare_parser.add_argument('--parser', choices=sorted(PARSERS), default='glaive')
|
| 298 |
+
prepare_parser.add_argument(
|
| 299 |
+
'--tokenizer', required=True,
|
| 300 |
+
help='tokenizer path; trained from the buffered traces when the file is absent'
|
| 301 |
+
)
|
| 302 |
+
prepare_parser.add_argument('--vocab-size', type=int, default=16384)
|
| 303 |
+
prepare_parser.add_argument('--output-dir', required=True)
|
| 304 |
+
prepare_parser.add_argument('--seq-len', type=int, default=2048)
|
| 305 |
+
prepare_parser.add_argument('--block', type=int, default=32)
|
| 306 |
+
prepare_parser.add_argument('--max-slots', type=int, default=64)
|
| 307 |
+
prepare_parser.add_argument(
|
| 308 |
+
'--sizes', type=int, nargs='+',
|
| 309 |
+
help='activate the adaptive layout with these ascending block sizes (e.g. 64 256)'
|
| 310 |
+
)
|
| 311 |
+
prepare_parser.add_argument('--val-fraction', type=float, default=0.02)
|
| 312 |
+
prepare_parser.add_argument(
|
| 313 |
+
'--limit', type=int, default=0, help='stop after this many unique kept examples'
|
| 314 |
+
)
|
| 315 |
+
prepare_parser.add_argument(
|
| 316 |
+
'--scan-limit', type=int, default=0, help='stop streaming after this many parsed rows'
|
| 317 |
+
)
|
| 318 |
+
return parser
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def main() -> None:
|
| 322 |
+
args = _build_parser().parse_args()
|
| 323 |
+
if args.command == 'prepare':
|
| 324 |
+
prepare(args)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
if __name__ == '__main__':
|
| 328 |
+
main()
|
src/diffusion_lm/flexattn.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pre-LN Transformer encoder that routes self-attention through ``flex_attention``.
|
| 2 |
+
|
| 3 |
+
The block-diffusion masks are structured (block-causal over thought slots plus optional
|
| 4 |
+
key padding), so expressing them as a ``flex_attention`` block mask lets the fused kernel
|
| 5 |
+
skip fully-masked blocks instead of materializing a dense ``[batch * heads, L, L]`` score
|
| 6 |
+
tensor. This keeps attention memory and time near-linear in the trace length, which the
|
| 7 |
+
default ``nn.TransformerEncoder`` math path does not.
|
| 8 |
+
|
| 9 |
+
The layer geometry mirrors ``nn.TransformerEncoderLayer(norm_first=True, activation='gelu')``
|
| 10 |
+
so behaviour matches the default path up to floating-point error; the attention core is
|
| 11 |
+
verified against a dense masked-softmax reference before use.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
from torch import Tensor, nn
|
| 22 |
+
from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
|
| 23 |
+
|
| 24 |
+
# Compiled dynamic=False is fastest for fixed-shape training. Variable-length generation
|
| 25 |
+
# recompiles per new sequence length, so serving sets MDLM_FLEX_EAGER=1 to run eager and
|
| 26 |
+
# trade steady-state speed for the absence of per-length recompilation stalls.
|
| 27 |
+
_FLEX_EAGER = os.environ.get('MDLM_FLEX_EAGER') == '1'
|
| 28 |
+
_flex_compiled = flex_attention if _FLEX_EAGER else torch.compile(flex_attention, dynamic=False)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def build_block_mask(
|
| 32 |
+
attn_mask: Tensor | None,
|
| 33 |
+
padding_mask: Tensor | None,
|
| 34 |
+
batch_size: int,
|
| 35 |
+
seq_len: int,
|
| 36 |
+
device: torch.device,
|
| 37 |
+
) -> BlockMask | None:
|
| 38 |
+
"""Build a broadcast-over-heads ``BlockMask`` from project boolean masks.
|
| 39 |
+
|
| 40 |
+
``attn_mask`` follows the src_mask convention (``True`` blocks a key), shaped ``[L, L]``
|
| 41 |
+
or ``[batch, L, L]``. ``padding_mask`` follows the src_key_padding_mask convention
|
| 42 |
+
(``True`` marks padding). Returns ``None`` when neither constrains attention.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
if attn_mask is None and padding_mask is None:
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
shared = attn_mask is not None and attn_mask.dim() == 2
|
| 49 |
+
|
| 50 |
+
def mask_mod(b: Tensor, h: Tensor, q_idx: Tensor, kv_idx: Tensor) -> Tensor:
|
| 51 |
+
keep = torch.ones_like(q_idx, dtype=torch.bool)
|
| 52 |
+
if attn_mask is not None:
|
| 53 |
+
blocked = attn_mask[q_idx, kv_idx] if shared else attn_mask[b, q_idx, kv_idx]
|
| 54 |
+
keep = keep & ~blocked
|
| 55 |
+
if padding_mask is not None:
|
| 56 |
+
keep = keep & ~padding_mask[b, kv_idx]
|
| 57 |
+
return keep
|
| 58 |
+
|
| 59 |
+
return create_block_mask(
|
| 60 |
+
mask_mod, batch_size, None, seq_len, seq_len, device=device, _compile=not _FLEX_EAGER
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def flex_self_attention(
|
| 65 |
+
query: Tensor, key: Tensor, value: Tensor, block_mask: BlockMask | None
|
| 66 |
+
) -> Tensor:
|
| 67 |
+
"""Multi-head self-attention over ``[batch, heads, L, head_dim]`` tensors."""
|
| 68 |
+
|
| 69 |
+
if block_mask is None:
|
| 70 |
+
return flex_attention(query, key, value)
|
| 71 |
+
return _flex_compiled(query, key, value, block_mask=block_mask)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class FlexEncoderLayer(nn.Module):
|
| 75 |
+
"""Pre-LN Transformer block with a ``flex_attention`` self-attention core."""
|
| 76 |
+
|
| 77 |
+
def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float) -> None:
|
| 78 |
+
super().__init__()
|
| 79 |
+
if d_model % n_heads != 0:
|
| 80 |
+
raise ValueError("d_model must be divisible by n_heads")
|
| 81 |
+
self.n_heads = n_heads
|
| 82 |
+
self.head_dim = d_model // n_heads
|
| 83 |
+
self.q_proj = nn.Linear(d_model, d_model)
|
| 84 |
+
self.k_proj = nn.Linear(d_model, d_model)
|
| 85 |
+
self.v_proj = nn.Linear(d_model, d_model)
|
| 86 |
+
self.out_proj = nn.Linear(d_model, d_model)
|
| 87 |
+
self.linear1 = nn.Linear(d_model, d_ff)
|
| 88 |
+
self.linear2 = nn.Linear(d_ff, d_model)
|
| 89 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 90 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 91 |
+
self.dropout = nn.Dropout(dropout)
|
| 92 |
+
|
| 93 |
+
def _split_heads(self, projected: Tensor) -> Tensor:
|
| 94 |
+
batch_size, seq_len, _ = projected.shape
|
| 95 |
+
return projected.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 96 |
+
|
| 97 |
+
def forward(self, hidden: Tensor, block_mask: BlockMask | None) -> Tensor:
|
| 98 |
+
normed = self.norm1(hidden)
|
| 99 |
+
query = self._split_heads(self.q_proj(normed))
|
| 100 |
+
key = self._split_heads(self.k_proj(normed))
|
| 101 |
+
value = self._split_heads(self.v_proj(normed))
|
| 102 |
+
attended = flex_self_attention(query, key, value, block_mask)
|
| 103 |
+
batch_size, _, seq_len, _ = attended.shape
|
| 104 |
+
attended = attended.transpose(1, 2).reshape(batch_size, seq_len, -1)
|
| 105 |
+
hidden = hidden + self.dropout(self.out_proj(attended))
|
| 106 |
+
normed = self.norm2(hidden)
|
| 107 |
+
feed_forward = self.linear2(self.dropout(F.gelu(self.linear1(normed))))
|
| 108 |
+
return hidden + self.dropout(feed_forward)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class FlexEncoder(nn.Module):
|
| 112 |
+
"""Stack of :class:`FlexEncoderLayer` blocks with a final layer norm."""
|
| 113 |
+
|
| 114 |
+
def __init__(
|
| 115 |
+
self,
|
| 116 |
+
d_model: int,
|
| 117 |
+
n_heads: int,
|
| 118 |
+
d_ff: int,
|
| 119 |
+
dropout: float,
|
| 120 |
+
n_layers: int,
|
| 121 |
+
activation_checkpointing: bool,
|
| 122 |
+
) -> None:
|
| 123 |
+
super().__init__()
|
| 124 |
+
self.layers = nn.ModuleList(
|
| 125 |
+
FlexEncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)
|
| 126 |
+
)
|
| 127 |
+
self.norm = nn.LayerNorm(d_model)
|
| 128 |
+
self.activation_checkpointing = activation_checkpointing
|
| 129 |
+
|
| 130 |
+
def init_residual_outputs(self, n_layers: int) -> None:
|
| 131 |
+
residual_std = 0.02 / math.sqrt(2 * n_layers)
|
| 132 |
+
for layer in self.layers:
|
| 133 |
+
nn.init.normal_(layer.out_proj.weight, mean=0.0, std=residual_std)
|
| 134 |
+
nn.init.normal_(layer.linear2.weight, mean=0.0, std=residual_std)
|
| 135 |
+
|
| 136 |
+
def forward(self, hidden: Tensor, block_mask: BlockMask | None) -> Tensor:
|
| 137 |
+
use_checkpoint = (
|
| 138 |
+
self.activation_checkpointing and self.training and torch.is_grad_enabled()
|
| 139 |
+
)
|
| 140 |
+
for layer in self.layers:
|
| 141 |
+
if use_checkpoint:
|
| 142 |
+
hidden = torch.utils.checkpoint.checkpoint(
|
| 143 |
+
layer, hidden, block_mask, use_reentrant=False
|
| 144 |
+
)
|
| 145 |
+
else:
|
| 146 |
+
hidden = layer(hidden, block_mask)
|
| 147 |
+
return self.norm(hidden)
|
src/diffusion_lm/hf_bridge.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HuggingFace Qwen3 backbone served through the project's denoiser forward contract."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import Tensor, nn
|
| 9 |
+
|
| 10 |
+
from diffusion_lm.config import ModelConfig
|
| 11 |
+
|
| 12 |
+
# The flex template's default 128x128 tiles need ~112 KiB of shared memory at head_dim 128,
|
| 13 |
+
# over Ada's 100 KiB per-block ceiling; halved tiles fit. The backward kernel budgets its
|
| 14 |
+
# tiles separately, hence the M1/N1/M2/N2 entries. Larger tiles are valid on Hopper.
|
| 15 |
+
_FLEX_KERNEL_OPTIONS = {
|
| 16 |
+
'BLOCK_M': 64,
|
| 17 |
+
'BLOCK_N': 64,
|
| 18 |
+
'BLOCK_M1': 32,
|
| 19 |
+
'BLOCK_N1': 64,
|
| 20 |
+
'BLOCK_M2': 64,
|
| 21 |
+
'BLOCK_N2': 32,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class Qwen3Denoiser(nn.Module):
|
| 26 |
+
"""Wrap ``Qwen3ForCausalLM`` behind the DiffusionTransformer forward contract.
|
| 27 |
+
|
| 28 |
+
The backbone always receives a 4D attention mask so its stock causal masking never
|
| 29 |
+
engages: block-diffusion objectives need bidirectional attention inside denoising
|
| 30 |
+
windows, and an omitted mask must mean "attend everything", not "causal".
|
| 31 |
+
``use_flex_attention`` selects how the boolean blocking matrix reaches the backbone — a
|
| 32 |
+
``BlockMask`` for the flex kernel, which skips fully-masked blocks, or a materialized
|
| 33 |
+
additive mask for SDPA. Hidden states are gathered at ``output_positions`` before the LM
|
| 34 |
+
head so full-vocabulary logits are never materialized for visible tokens.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
config: ModelConfig,
|
| 40 |
+
*,
|
| 41 |
+
load_pretrained: bool = True,
|
| 42 |
+
dtype: torch.dtype | None = None,
|
| 43 |
+
) -> None:
|
| 44 |
+
super().__init__()
|
| 45 |
+
try:
|
| 46 |
+
from transformers import Qwen3Config, Qwen3ForCausalLM
|
| 47 |
+
except ImportError as exc:
|
| 48 |
+
raise RuntimeError(
|
| 49 |
+
'the hf-qwen3 backbone requires transformers; install the [hf] extra'
|
| 50 |
+
) from exc
|
| 51 |
+
if config.pretrained_path is None:
|
| 52 |
+
raise ValueError('the hf-qwen3 backbone requires pretrained_path')
|
| 53 |
+
self.config = config
|
| 54 |
+
# Generation on Ada can hit the Triton shared-memory ceiling inside transformers' own
|
| 55 |
+
# compiled flex kernel, where our kernel_options do not reach. MDLM_SDPA=1 sidesteps it
|
| 56 |
+
# for serving and evaluation; flex earns its keep in training, not at q_len 1.
|
| 57 |
+
use_flex = config.use_flex_attention and os.environ.get('MDLM_SDPA') != '1'
|
| 58 |
+
attn_implementation = 'flex_attention' if use_flex else 'sdpa'
|
| 59 |
+
if load_pretrained:
|
| 60 |
+
kwargs = {} if dtype is None else {'dtype': dtype}
|
| 61 |
+
self.backbone = Qwen3ForCausalLM.from_pretrained(
|
| 62 |
+
config.pretrained_path, attn_implementation=attn_implementation, **kwargs
|
| 63 |
+
)
|
| 64 |
+
else:
|
| 65 |
+
# Architecture-only construction: weights come from a later load_state_dict,
|
| 66 |
+
# so checkpoint restore never re-reads the base model files.
|
| 67 |
+
backbone_config = Qwen3Config.from_pretrained(
|
| 68 |
+
config.pretrained_path, attn_implementation=attn_implementation
|
| 69 |
+
)
|
| 70 |
+
self.backbone = Qwen3ForCausalLM(backbone_config)
|
| 71 |
+
if dtype is not None:
|
| 72 |
+
self.backbone.to(dtype)
|
| 73 |
+
self._use_flex = use_flex
|
| 74 |
+
self._validate_backbone()
|
| 75 |
+
self.backbone.config.use_cache = False
|
| 76 |
+
if config.activation_checkpointing:
|
| 77 |
+
self.backbone.gradient_checkpointing_enable(
|
| 78 |
+
gradient_checkpointing_kwargs={'use_reentrant': False}
|
| 79 |
+
)
|
| 80 |
+
# A plain attribute keeps the compiled callable out of the module tree, so checkpoint
|
| 81 |
+
# keys are identical whether or not compilation is on. Shapes reaching the backbone are
|
| 82 |
+
# static; the varying count of output positions is gathered after it returns.
|
| 83 |
+
# Serving generates at ever-changing lengths, which makes a compiled backbone
|
| 84 |
+
# recompile per shape; MDLM_NO_COMPILE=1 turns it off without touching the checkpoint.
|
| 85 |
+
compiling = config.compile_backbone and os.environ.get('MDLM_NO_COMPILE') != '1'
|
| 86 |
+
self._compiled_backbone = (
|
| 87 |
+
torch.compile(self.backbone.model.forward, dynamic=False) if compiling else None
|
| 88 |
+
)
|
| 89 |
+
self.register_buffer(
|
| 90 |
+
'_forbidden_output_token_ids',
|
| 91 |
+
torch.tensor(config.forbidden_output_token_ids, dtype=torch.long),
|
| 92 |
+
persistent=False,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def _validate_backbone(self) -> None:
|
| 96 |
+
backbone = self.backbone.config
|
| 97 |
+
pairs = (
|
| 98 |
+
('vocab_size', self.config.vocab_size, backbone.vocab_size),
|
| 99 |
+
('d_model', self.config.d_model, backbone.hidden_size),
|
| 100 |
+
('n_layers', self.config.n_layers, backbone.num_hidden_layers),
|
| 101 |
+
('n_heads', self.config.n_heads, backbone.num_attention_heads),
|
| 102 |
+
('d_ff', self.config.d_ff, backbone.intermediate_size),
|
| 103 |
+
)
|
| 104 |
+
for name, expected, actual in pairs:
|
| 105 |
+
if expected != actual:
|
| 106 |
+
raise ValueError(f'config {name}={expected} but the backbone has {actual}')
|
| 107 |
+
if self.config.max_seq_len > backbone.max_position_embeddings:
|
| 108 |
+
raise ValueError(
|
| 109 |
+
f'max_seq_len {self.config.max_seq_len} exceeds the backbone context '
|
| 110 |
+
f'{backbone.max_position_embeddings}'
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
def _blocked_matrix(
|
| 114 |
+
self,
|
| 115 |
+
input_ids: Tensor,
|
| 116 |
+
attention_mask: Tensor | None,
|
| 117 |
+
attn_mask: Tensor | None,
|
| 118 |
+
) -> Tensor:
|
| 119 |
+
"""Boolean ``[batch, L, L]`` blocking matrix, ``True`` marking a key to suppress."""
|
| 120 |
+
|
| 121 |
+
batch_size, seq_len = input_ids.shape
|
| 122 |
+
device = input_ids.device
|
| 123 |
+
if attn_mask is None:
|
| 124 |
+
blocked = torch.zeros(
|
| 125 |
+
batch_size, seq_len, seq_len, dtype=torch.bool, device=device
|
| 126 |
+
)
|
| 127 |
+
else:
|
| 128 |
+
if attn_mask.dtype != torch.bool:
|
| 129 |
+
raise ValueError('attn_mask must be boolean with True marking blocked positions')
|
| 130 |
+
if attn_mask.shape == (seq_len, seq_len):
|
| 131 |
+
blocked = attn_mask.unsqueeze(0).expand(batch_size, -1, -1)
|
| 132 |
+
elif attn_mask.shape == (batch_size, seq_len, seq_len):
|
| 133 |
+
blocked = attn_mask
|
| 134 |
+
else:
|
| 135 |
+
raise ValueError('attn_mask must have shape [L, L] or [batch, L, L]')
|
| 136 |
+
if attention_mask is not None:
|
| 137 |
+
if attention_mask.shape != input_ids.shape:
|
| 138 |
+
raise ValueError('attention_mask must match input_ids')
|
| 139 |
+
blocked = blocked | ~attention_mask.bool()[:, None, :]
|
| 140 |
+
return blocked
|
| 141 |
+
|
| 142 |
+
def _additive_mask(self, blocked: Tensor) -> Tensor:
|
| 143 |
+
mask_dtype = self.backbone.model.embed_tokens.weight.dtype
|
| 144 |
+
additive = torch.zeros(blocked.shape, dtype=mask_dtype, device=blocked.device)
|
| 145 |
+
additive = additive.masked_fill(blocked, torch.finfo(mask_dtype).min)
|
| 146 |
+
return additive.unsqueeze(1)
|
| 147 |
+
|
| 148 |
+
def forward_cached(
|
| 149 |
+
self,
|
| 150 |
+
input_ids: Tensor,
|
| 151 |
+
*,
|
| 152 |
+
attn_mask: Tensor,
|
| 153 |
+
past_key_values,
|
| 154 |
+
output_positions: Tensor | None = None,
|
| 155 |
+
) -> tuple[Tensor, object]:
|
| 156 |
+
"""Run only the trailing ``input_ids`` against a populated key/value cache.
|
| 157 |
+
|
| 158 |
+
``attn_mask`` holds one row per NEW query and one column per position the query may
|
| 159 |
+
see, cache included: ``[batch, query, cached + query]``. Block-causal masking is what
|
| 160 |
+
makes caching sound here — prefix positions never attend forward into a block, so
|
| 161 |
+
their keys and values stay valid while the block's own tokens keep changing.
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
cached = past_key_values.get_seq_length()
|
| 165 |
+
query_len = input_ids.shape[1]
|
| 166 |
+
blocked = attn_mask if attn_mask.dim() == 3 else attn_mask.unsqueeze(0)
|
| 167 |
+
if blocked.shape[-2] != query_len or blocked.shape[-1] != cached + query_len:
|
| 168 |
+
raise ValueError(
|
| 169 |
+
f'attn_mask must be [batch, {query_len}, {cached + query_len}] for a cache of '
|
| 170 |
+
f'{cached} positions, got {tuple(blocked.shape)}'
|
| 171 |
+
)
|
| 172 |
+
rows = blocked.expand(input_ids.shape[0], -1, -1)
|
| 173 |
+
outputs = self.backbone.model(
|
| 174 |
+
input_ids=input_ids,
|
| 175 |
+
attention_mask=self._additive_mask(rows),
|
| 176 |
+
past_key_values=past_key_values,
|
| 177 |
+
use_cache=True,
|
| 178 |
+
**self._backbone_kwargs(),
|
| 179 |
+
)
|
| 180 |
+
hidden = outputs.last_hidden_state
|
| 181 |
+
if output_positions is not None:
|
| 182 |
+
hidden = hidden[output_positions.bool()]
|
| 183 |
+
logits = self.backbone.lm_head(hidden)
|
| 184 |
+
self._forbid(logits)
|
| 185 |
+
return logits, outputs.past_key_values
|
| 186 |
+
|
| 187 |
+
def _backbone_kwargs(self) -> dict:
|
| 188 |
+
"""Extra backbone arguments the attention implementation needs.
|
| 189 |
+
|
| 190 |
+
Every path into the backbone has to carry these, cached or not: without the reduced
|
| 191 |
+
tiles the flex template asks Ada for more shared memory than it has and the kernel
|
| 192 |
+
fails to compile at all.
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
if not self._use_flex:
|
| 196 |
+
return {}
|
| 197 |
+
return {'kernel_options': _FLEX_KERNEL_OPTIONS}
|
| 198 |
+
|
| 199 |
+
def new_cache(self):
|
| 200 |
+
"""Empty key/value cache for :meth:`forward_cached`, kept here so callers stay
|
| 201 |
+
independent of the transformers cache class."""
|
| 202 |
+
|
| 203 |
+
from transformers import DynamicCache
|
| 204 |
+
|
| 205 |
+
return DynamicCache()
|
| 206 |
+
|
| 207 |
+
def _forbid(self, logits: Tensor) -> None:
|
| 208 |
+
if self._forbidden_output_token_ids.numel():
|
| 209 |
+
logits.index_fill_(
|
| 210 |
+
-1, self._forbidden_output_token_ids, torch.finfo(logits.dtype).min
|
| 211 |
+
)
|
| 212 |
+
if self.config.forbidden_output_from is not None:
|
| 213 |
+
logits[..., self.config.forbidden_output_from :] = torch.finfo(logits.dtype).min
|
| 214 |
+
|
| 215 |
+
def _flex_mask(self, blocked: Tensor):
|
| 216 |
+
"""Compress the blocking matrix into a ``BlockMask`` the flex kernel can skip over.
|
| 217 |
+
|
| 218 |
+
``masking_utils`` forwards any mask reporting a 4D shape untouched, so a ``BlockMask``
|
| 219 |
+
reaches ``flex_attention_forward`` as ``block_mask`` and the backbone never rebuilds a
|
| 220 |
+
causal mask of its own.
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
+
from diffusion_lm.flexattn import build_block_mask
|
| 224 |
+
|
| 225 |
+
batch_size, seq_len, _ = blocked.shape
|
| 226 |
+
return build_block_mask(blocked, None, batch_size, seq_len, blocked.device)
|
| 227 |
+
|
| 228 |
+
def forward(
|
| 229 |
+
self,
|
| 230 |
+
input_ids: Tensor,
|
| 231 |
+
attention_mask: Tensor | None = None,
|
| 232 |
+
output_positions: Tensor | None = None,
|
| 233 |
+
attn_mask: Tensor | None = None,
|
| 234 |
+
) -> Tensor:
|
| 235 |
+
if input_ids.ndim != 2:
|
| 236 |
+
raise ValueError('input_ids must have shape [batch, sequence]')
|
| 237 |
+
sequence_length = input_ids.shape[1]
|
| 238 |
+
if sequence_length > self.config.max_seq_len:
|
| 239 |
+
raise ValueError(
|
| 240 |
+
f'sequence length {sequence_length} exceeds max_seq_len '
|
| 241 |
+
f'{self.config.max_seq_len}'
|
| 242 |
+
)
|
| 243 |
+
blocked = self._blocked_matrix(input_ids, attention_mask, attn_mask)
|
| 244 |
+
mask = (
|
| 245 |
+
self._flex_mask(blocked) if self._use_flex else self._additive_mask(blocked)
|
| 246 |
+
)
|
| 247 |
+
call = self._compiled_backbone or self.backbone.model
|
| 248 |
+
hidden = call(
|
| 249 |
+
input_ids=input_ids, attention_mask=mask, use_cache=False,
|
| 250 |
+
**self._backbone_kwargs(),
|
| 251 |
+
).last_hidden_state
|
| 252 |
+
if output_positions is not None:
|
| 253 |
+
if output_positions.shape != input_ids.shape:
|
| 254 |
+
raise ValueError('output_positions must match input_ids')
|
| 255 |
+
hidden = hidden[output_positions.bool()]
|
| 256 |
+
logits = self.backbone.lm_head(hidden)
|
| 257 |
+
self._forbid(logits)
|
| 258 |
+
return logits
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def token_embedding(self) -> nn.Embedding:
|
| 262 |
+
"""Embedding module surfaced for the optimizer's 32-bit override (tied to the head)."""
|
| 263 |
+
|
| 264 |
+
return self.backbone.model.embed_tokens
|
| 265 |
+
|
| 266 |
+
@property
|
| 267 |
+
def num_parameters(self) -> int:
|
| 268 |
+
"""Count unique trainable parameters (shared embeddings count once)."""
|
| 269 |
+
|
| 270 |
+
return sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad)
|
src/diffusion_lm/hybrid.py
ADDED
|
@@ -0,0 +1,1139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Region-aware attention masks, objectives, and samplers for reasoning variants.
|
| 2 |
+
|
| 3 |
+
Three inference modes share one ``DiffusionTransformer``:
|
| 4 |
+
|
| 5 |
+
- ``ar``: prefix-LM. Bidirectional over the problem, causal generation after it.
|
| 6 |
+
- ``diffusion``: full-sequence denoising of the response region in parallel.
|
| 7 |
+
- ``hybrid``: thought slots denoised block-by-block, then the answer decoded
|
| 8 |
+
autoregressively under a bidirectional-prefix mask.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import time
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from torch import Tensor
|
| 20 |
+
|
| 21 |
+
from diffusion_lm.diffusion import (
|
| 22 |
+
CorruptionBatch,
|
| 23 |
+
corrupt_tokens,
|
| 24 |
+
iterative_unmask,
|
| 25 |
+
_sample_categorical,
|
| 26 |
+
)
|
| 27 |
+
from diffusion_lm.model import DiffusionTransformer
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def prefix_causal_blocked(
|
| 31 |
+
prefix_lens: Tensor, seq_len: int, *, causal_prefix: bool = False
|
| 32 |
+
) -> Tensor:
|
| 33 |
+
"""Blocked-attention mask: bidirectional before ``prefix_len``, causal after.
|
| 34 |
+
|
| 35 |
+
``allowed[b, i, j] = j < prefix_lens[b] or j <= i``; the returned tensor is the
|
| 36 |
+
inverse, matching the src_mask convention where ``True`` blocks attention.
|
| 37 |
+
``causal_prefix=True`` degenerates to the plain causal mask — the geometry
|
| 38 |
+
pretrained autoregressive backbones were trained under.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
positions = torch.arange(seq_len, device=prefix_lens.device)
|
| 42 |
+
causal = positions[None, :, None] >= positions[None, None, :]
|
| 43 |
+
if causal_prefix:
|
| 44 |
+
return (~causal).expand(prefix_lens.shape[0], seq_len, seq_len)
|
| 45 |
+
prefix = positions[None, None, :] < prefix_lens[:, None, None]
|
| 46 |
+
return ~(prefix | causal)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def window_blocked(window_ends: Tensor, seq_len: int) -> Tensor:
|
| 50 |
+
"""Blocked-attention mask hiding all keys at or beyond each sample's window end."""
|
| 51 |
+
|
| 52 |
+
positions = torch.arange(seq_len, device=window_ends.device)
|
| 53 |
+
allowed = positions[None, None, :] < window_ends[:, None, None]
|
| 54 |
+
return ~allowed.expand(window_ends.shape[0], seq_len, seq_len)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def slot_causal_blocked(
|
| 58 |
+
problem_len: Tensor, n_slots: Tensor, block: int, seq_len: int
|
| 59 |
+
) -> Tensor:
|
| 60 |
+
"""Block-causal mask over thought slots: each slot attends its prefix slots only.
|
| 61 |
+
|
| 62 |
+
Problem tokens (plus ``<think>``) attend the problem window; tokens of slot k
|
| 63 |
+
attend everything up to slot k's end; positions past the think region attend
|
| 64 |
+
the whole think window. Denoising slot k at inference with clean prefix slots
|
| 65 |
+
is the per-slot ``t -> 0`` limit of the training distribution.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
device = problem_len.device
|
| 69 |
+
positions = torch.arange(seq_len, device=device)[None, :]
|
| 70 |
+
prefix_end = (problem_len + 1)[:, None]
|
| 71 |
+
think_end = prefix_end + n_slots[:, None] * block
|
| 72 |
+
slot_index = torch.clamp((positions - prefix_end) // block, min=0)
|
| 73 |
+
slot_end = prefix_end + (slot_index + 1) * block
|
| 74 |
+
window = torch.where(positions < prefix_end, prefix_end, slot_end)
|
| 75 |
+
window = torch.where(positions >= think_end, think_end, window)
|
| 76 |
+
allowed = positions[:, None, :] < window[:, :, None]
|
| 77 |
+
return ~allowed
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def adaptive_block_mask(
|
| 81 |
+
tokens: Tensor,
|
| 82 |
+
problem_len: Tensor,
|
| 83 |
+
answer_start: Tensor,
|
| 84 |
+
size_ids: Tensor,
|
| 85 |
+
end_think_id: int,
|
| 86 |
+
*,
|
| 87 |
+
causal_prefix: bool = False,
|
| 88 |
+
) -> Tensor:
|
| 89 |
+
"""Block-causal mask over variable-length thought blocks.
|
| 90 |
+
|
| 91 |
+
Block boundaries are read directly from the token stream: every ``<szN>``
|
| 92 |
+
control token and the terminal ``</think>`` mark the start of the next region.
|
| 93 |
+
A thought-content token attends its clean prefix plus its own block
|
| 94 |
+
bidirectionally, never a following block; problem and ``<think>`` positions see
|
| 95 |
+
the problem window only — or, with ``causal_prefix=True``, only their causal
|
| 96 |
+
past, matching a pretrained autoregressive backbone. The returned tensor
|
| 97 |
+
follows the src_mask convention where ``True`` blocks a key.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
positions = torch.arange(tokens.shape[1], device=tokens.device)
|
| 101 |
+
prefix_end = (problem_len + 1)[:, None]
|
| 102 |
+
in_think = (positions[None, :] >= prefix_end) & (
|
| 103 |
+
positions[None, :] < answer_start[:, None]
|
| 104 |
+
)
|
| 105 |
+
is_size = (tokens.unsqueeze(-1) == size_ids).any(dim=-1)
|
| 106 |
+
boundary = (is_size | (tokens == end_think_id)) & in_think
|
| 107 |
+
return block_mask_from_boundaries(
|
| 108 |
+
boundary, problem_len + 1, answer_start, causal_prefix=causal_prefix
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def block_mask_from_boundaries(
|
| 113 |
+
boundary: Tensor,
|
| 114 |
+
prefix_end: Tensor,
|
| 115 |
+
answer_start: Tensor,
|
| 116 |
+
*,
|
| 117 |
+
causal_prefix: bool = False,
|
| 118 |
+
) -> Tensor:
|
| 119 |
+
"""Blocked mask from per-position block-start marks (the adaptive-mask core).
|
| 120 |
+
|
| 121 |
+
``boundary[b, p]`` is True where a new block starts. A position attends every key
|
| 122 |
+
before the next boundary after it: its own block bidirectionally plus all preceding
|
| 123 |
+
context. Rows before ``prefix_end`` see the prefix window (or their causal past with
|
| 124 |
+
``causal_prefix=True``); rows at or past ``answer_start`` see up to ``answer_start``.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
device = boundary.device
|
| 128 |
+
batch_size, seq_len = boundary.shape
|
| 129 |
+
positions = torch.arange(seq_len, device=device)
|
| 130 |
+
prefix_end = prefix_end[:, None]
|
| 131 |
+
answer_start = answer_start[:, None]
|
| 132 |
+
|
| 133 |
+
boundary_index = torch.where(
|
| 134 |
+
boundary, positions[None, :].expand(batch_size, seq_len), seq_len
|
| 135 |
+
)
|
| 136 |
+
reverse_cummin = boundary_index.flip(1).cummin(dim=1).values.flip(1)
|
| 137 |
+
next_boundary = torch.full((batch_size, seq_len), seq_len, device=device)
|
| 138 |
+
next_boundary[:, :-1] = reverse_cummin[:, 1:]
|
| 139 |
+
|
| 140 |
+
if causal_prefix:
|
| 141 |
+
prefix_limit = (positions[None, :] + 1).expand(batch_size, seq_len)
|
| 142 |
+
else:
|
| 143 |
+
prefix_limit = prefix_end.expand(batch_size, seq_len)
|
| 144 |
+
attend_limit = next_boundary
|
| 145 |
+
attend_limit = torch.where(positions[None, :] < prefix_end, prefix_limit, attend_limit)
|
| 146 |
+
attend_limit = torch.where(
|
| 147 |
+
positions[None, :] >= answer_start,
|
| 148 |
+
answer_start.expand(batch_size, seq_len),
|
| 149 |
+
attend_limit,
|
| 150 |
+
)
|
| 151 |
+
allowed = positions[None, None, :] < attend_limit[:, :, None]
|
| 152 |
+
return ~allowed
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@dataclass(frozen=True)
|
| 156 |
+
class HybridBatchLoss:
|
| 157 |
+
loss: Tensor
|
| 158 |
+
think_loss: float
|
| 159 |
+
answer_loss: float
|
| 160 |
+
think_accuracy: float
|
| 161 |
+
answer_accuracy: float
|
| 162 |
+
think_samples: int
|
| 163 |
+
answer_samples: int
|
| 164 |
+
# Accuracy of the block-boundary decision, restricted to the control menu, and of the
|
| 165 |
+
# stop-versus-continue half of it on its own. Zero outside the adaptive objective.
|
| 166 |
+
control_accuracy: float = 0.0
|
| 167 |
+
stop_accuracy: float = 0.0
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def hybrid_objective(
|
| 171 |
+
model: DiffusionTransformer,
|
| 172 |
+
tokens: Tensor,
|
| 173 |
+
regions: Tensor,
|
| 174 |
+
*,
|
| 175 |
+
block: int,
|
| 176 |
+
think_probability: float,
|
| 177 |
+
mask_eps: float,
|
| 178 |
+
generator: torch.Generator | None = None,
|
| 179 |
+
) -> HybridBatchLoss:
|
| 180 |
+
"""Mixed objective: denoise all thought slots or predict the answer tokens.
|
| 181 |
+
|
| 182 |
+
Each sample is assigned one mode. Think samples corrupt every slot at an
|
| 183 |
+
independent noise level under a block-causal mask, so one forward trains all
|
| 184 |
+
slot conditionals; answer samples see the full clean reasoning prefix
|
| 185 |
+
bidirectionally and the answer causally.
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
device = tokens.device
|
| 189 |
+
batch_size, seq_len = tokens.shape
|
| 190 |
+
problem_len, n_slots, answer_start, answer_end = regions.unbind(dim=1)
|
| 191 |
+
positions = torch.arange(seq_len, device=device)
|
| 192 |
+
|
| 193 |
+
think_sel = (
|
| 194 |
+
torch.rand(batch_size, device=device, generator=generator) < think_probability
|
| 195 |
+
)
|
| 196 |
+
if bool(think_sel.all()):
|
| 197 |
+
think_sel[-1] = False
|
| 198 |
+
if not bool(think_sel.any()):
|
| 199 |
+
think_sel[0] = True
|
| 200 |
+
|
| 201 |
+
prefix_end = problem_len + 1
|
| 202 |
+
think_end = prefix_end + n_slots * block
|
| 203 |
+
think_region = (
|
| 204 |
+
(positions[None, :] >= prefix_end[:, None])
|
| 205 |
+
& (positions[None, :] < think_end[:, None])
|
| 206 |
+
& think_sel[:, None]
|
| 207 |
+
)
|
| 208 |
+
max_slots = int(n_slots.max())
|
| 209 |
+
slot_noise = mask_eps + (1.0 - mask_eps) * torch.rand(
|
| 210 |
+
batch_size, max_slots, device=device, generator=generator
|
| 211 |
+
)
|
| 212 |
+
slot_index = torch.clamp(
|
| 213 |
+
(positions[None, :] - prefix_end[:, None]) // block, min=0, max=max_slots - 1
|
| 214 |
+
)
|
| 215 |
+
token_noise = slot_noise.gather(1, slot_index)
|
| 216 |
+
mask = (
|
| 217 |
+
torch.rand(tokens.shape, device=device, generator=generator) < token_noise
|
| 218 |
+
) & think_region
|
| 219 |
+
noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens)
|
| 220 |
+
|
| 221 |
+
think_blocked = slot_causal_blocked(problem_len, n_slots, block, seq_len)
|
| 222 |
+
answer_blocked = prefix_causal_blocked(answer_start, seq_len)
|
| 223 |
+
blocked = torch.where(think_sel[:, None, None], think_blocked, answer_blocked)
|
| 224 |
+
|
| 225 |
+
predict_positions = (
|
| 226 |
+
(positions[None, :] >= answer_start[:, None] - 1)
|
| 227 |
+
& (positions[None, :] < answer_end[:, None] - 1)
|
| 228 |
+
& ~think_sel[:, None]
|
| 229 |
+
)
|
| 230 |
+
output_positions = mask | predict_positions
|
| 231 |
+
logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked)
|
| 232 |
+
|
| 233 |
+
think_rows = mask[output_positions]
|
| 234 |
+
# Graph-connected zero: forbidden-output columns sit at finfo.min, so a raw sum
|
| 235 |
+
# overflows to -inf in low precision and would poison the scalar via -inf * 0.
|
| 236 |
+
zero = logits.sum().clamp(-1.0, 1.0) * 0.0
|
| 237 |
+
|
| 238 |
+
think_loss = zero
|
| 239 |
+
think_accuracy = 0.0
|
| 240 |
+
if bool(mask.any()):
|
| 241 |
+
think_logits = logits[think_rows]
|
| 242 |
+
think_targets = tokens[mask]
|
| 243 |
+
per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none')
|
| 244 |
+
weights = token_noise[mask]
|
| 245 |
+
normalizer = think_region.sum().clamp_min(1)
|
| 246 |
+
think_loss = (per_token / weights).sum() / normalizer
|
| 247 |
+
think_accuracy = float(
|
| 248 |
+
(think_logits.argmax(dim=-1) == think_targets).float().mean()
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
answer_loss = zero
|
| 252 |
+
answer_accuracy = 0.0
|
| 253 |
+
if bool(predict_positions.any()):
|
| 254 |
+
answer_logits = logits[~think_rows]
|
| 255 |
+
target_positions = torch.zeros_like(predict_positions)
|
| 256 |
+
target_positions[:, 1:] = predict_positions[:, :-1]
|
| 257 |
+
answer_targets = tokens[target_positions]
|
| 258 |
+
answer_loss = F.cross_entropy(answer_logits.float(), answer_targets)
|
| 259 |
+
answer_accuracy = float(
|
| 260 |
+
(answer_logits.argmax(dim=-1) == answer_targets).float().mean()
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
return HybridBatchLoss(
|
| 264 |
+
loss=think_loss + answer_loss,
|
| 265 |
+
think_loss=float(think_loss),
|
| 266 |
+
answer_loss=float(answer_loss),
|
| 267 |
+
think_accuracy=think_accuracy,
|
| 268 |
+
answer_accuracy=answer_accuracy,
|
| 269 |
+
think_samples=int(think_sel.sum()),
|
| 270 |
+
answer_samples=int((~think_sel).sum()),
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def adaptive_hybrid_objective(
|
| 275 |
+
model: DiffusionTransformer,
|
| 276 |
+
tokens: Tensor,
|
| 277 |
+
regions: Tensor,
|
| 278 |
+
*,
|
| 279 |
+
size_ids: Tensor,
|
| 280 |
+
end_think_id: int,
|
| 281 |
+
think_probability: float,
|
| 282 |
+
mask_eps: float,
|
| 283 |
+
causal_prefix: bool = False,
|
| 284 |
+
control_context_noise: float = 0.0,
|
| 285 |
+
generator: torch.Generator | None = None,
|
| 286 |
+
) -> HybridBatchLoss:
|
| 287 |
+
"""Mixed objective for the adaptive layout.
|
| 288 |
+
|
| 289 |
+
Think samples denoise every thought block at an independent noise level under
|
| 290 |
+
the variable-boundary block-causal mask. The remaining samples run a causal
|
| 291 |
+
next-token objective over the whole reasoning-and-answer stream, which is where
|
| 292 |
+
the model learns the ``<szN>`` block-size and ``</think>`` termination decisions.
|
| 293 |
+
|
| 294 |
+
Those causal samples read a pristine think region, while at inference the controller
|
| 295 |
+
reads thoughts the model just wrote, complete with sampling damage.
|
| 296 |
+
``control_context_noise`` closes that gap by swapping a fraction of their think tokens
|
| 297 |
+
for other tokens drawn from the batch: inputs degrade, targets stay clean.
|
| 298 |
+
"""
|
| 299 |
+
|
| 300 |
+
device = tokens.device
|
| 301 |
+
batch_size, seq_len = tokens.shape
|
| 302 |
+
problem_len, _, answer_start, answer_end = regions.unbind(dim=1)
|
| 303 |
+
positions = torch.arange(seq_len, device=device)
|
| 304 |
+
|
| 305 |
+
think_sel = (
|
| 306 |
+
torch.rand(batch_size, device=device, generator=generator) < think_probability
|
| 307 |
+
)
|
| 308 |
+
if bool(think_sel.all()):
|
| 309 |
+
think_sel[-1] = False
|
| 310 |
+
if not bool(think_sel.any()):
|
| 311 |
+
think_sel[0] = True
|
| 312 |
+
|
| 313 |
+
prefix_end = problem_len + 1
|
| 314 |
+
in_think = (positions[None, :] >= prefix_end[:, None]) & (
|
| 315 |
+
positions[None, :] < answer_start[:, None]
|
| 316 |
+
)
|
| 317 |
+
is_size = (tokens.unsqueeze(-1) == size_ids).any(dim=-1)
|
| 318 |
+
boundary = (is_size | (tokens == end_think_id)) & in_think
|
| 319 |
+
think_content = in_think & ~boundary & think_sel[:, None]
|
| 320 |
+
|
| 321 |
+
block_id = torch.cumsum((is_size & in_think).long(), dim=1)
|
| 322 |
+
max_blocks = int(block_id.max().clamp(min=1))
|
| 323 |
+
slot_noise = mask_eps + (1.0 - mask_eps) * torch.rand(
|
| 324 |
+
batch_size, max_blocks, device=device, generator=generator
|
| 325 |
+
)
|
| 326 |
+
gather_index = (block_id - 1).clamp(min=0, max=max_blocks - 1)
|
| 327 |
+
token_noise = slot_noise.gather(1, gather_index)
|
| 328 |
+
mask = (
|
| 329 |
+
torch.rand(tokens.shape, device=device, generator=generator) < token_noise
|
| 330 |
+
) & think_content
|
| 331 |
+
noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens)
|
| 332 |
+
|
| 333 |
+
if control_context_noise > 0.0:
|
| 334 |
+
# Replacements come from the batch itself so the corrupted context keeps realistic
|
| 335 |
+
# token statistics; boundaries and targets are read from the clean tensor.
|
| 336 |
+
control_content = in_think & ~boundary & ~think_sel[:, None]
|
| 337 |
+
corrupt = (
|
| 338 |
+
torch.rand(tokens.shape, device=device, generator=generator) < control_context_noise
|
| 339 |
+
) & control_content
|
| 340 |
+
flat = tokens.reshape(-1)
|
| 341 |
+
picks = torch.randint(
|
| 342 |
+
0, flat.numel(), tokens.shape, device=device, generator=generator
|
| 343 |
+
)
|
| 344 |
+
noisy_tokens = torch.where(corrupt, flat[picks], noisy_tokens)
|
| 345 |
+
|
| 346 |
+
think_blocked = adaptive_block_mask(
|
| 347 |
+
tokens, problem_len, answer_start, size_ids, end_think_id,
|
| 348 |
+
causal_prefix=causal_prefix,
|
| 349 |
+
)
|
| 350 |
+
ar_blocked = prefix_causal_blocked(prefix_end, seq_len, causal_prefix=causal_prefix)
|
| 351 |
+
blocked = torch.where(think_sel[:, None, None], think_blocked, ar_blocked)
|
| 352 |
+
|
| 353 |
+
predict_positions = (
|
| 354 |
+
(positions[None, :] >= problem_len[:, None])
|
| 355 |
+
& (positions[None, :] < answer_end[:, None] - 1)
|
| 356 |
+
& ~think_sel[:, None]
|
| 357 |
+
)
|
| 358 |
+
output_positions = mask | predict_positions
|
| 359 |
+
logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked)
|
| 360 |
+
|
| 361 |
+
think_rows = mask[output_positions]
|
| 362 |
+
# Graph-connected zero: forbidden-output columns sit at finfo.min, so a raw sum
|
| 363 |
+
# overflows to -inf in low precision and would poison the scalar via -inf * 0.
|
| 364 |
+
zero = logits.sum().clamp(-1.0, 1.0) * 0.0
|
| 365 |
+
|
| 366 |
+
think_loss = zero
|
| 367 |
+
think_accuracy = 0.0
|
| 368 |
+
if bool(mask.any()):
|
| 369 |
+
think_logits = logits[think_rows]
|
| 370 |
+
think_targets = tokens[mask]
|
| 371 |
+
per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none')
|
| 372 |
+
weights = token_noise[mask]
|
| 373 |
+
normalizer = think_content.sum().clamp_min(1)
|
| 374 |
+
think_loss = (per_token / weights).sum() / normalizer
|
| 375 |
+
think_accuracy = float(
|
| 376 |
+
(think_logits.argmax(dim=-1) == think_targets).float().mean()
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
answer_loss = zero
|
| 380 |
+
answer_accuracy = 0.0
|
| 381 |
+
control_accuracy = 0.0
|
| 382 |
+
stop_accuracy = 0.0
|
| 383 |
+
if bool(predict_positions.any()):
|
| 384 |
+
answer_logits = logits[~think_rows]
|
| 385 |
+
target_positions = torch.zeros_like(predict_positions)
|
| 386 |
+
target_positions[:, 1:] = predict_positions[:, :-1]
|
| 387 |
+
answer_targets = tokens[target_positions]
|
| 388 |
+
answer_loss = F.cross_entropy(answer_logits.float(), answer_targets)
|
| 389 |
+
control_ids = torch.cat(
|
| 390 |
+
[size_ids, torch.tensor([end_think_id], device=device, dtype=size_ids.dtype)]
|
| 391 |
+
)
|
| 392 |
+
is_control = (answer_targets.unsqueeze(-1) == control_ids).any(dim=-1)
|
| 393 |
+
answer_accuracy = float(
|
| 394 |
+
(answer_logits.argmax(dim=-1) == answer_targets).float().mean()
|
| 395 |
+
)
|
| 396 |
+
if bool(is_control.any()):
|
| 397 |
+
# Scored inside the control menu: an open-vocabulary argmax hides whether the
|
| 398 |
+
# boundary decision is right, since content tokens dominate those logits.
|
| 399 |
+
menu = answer_logits[is_control].index_select(-1, control_ids)
|
| 400 |
+
chosen = control_ids[menu.argmax(dim=-1)]
|
| 401 |
+
targets = answer_targets[is_control]
|
| 402 |
+
control_accuracy = float((chosen == targets).float().mean())
|
| 403 |
+
stop_accuracy = float(
|
| 404 |
+
((chosen == end_think_id) == (targets == end_think_id)).float().mean()
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
return HybridBatchLoss(
|
| 408 |
+
loss=think_loss + answer_loss,
|
| 409 |
+
think_loss=float(think_loss),
|
| 410 |
+
answer_loss=float(answer_loss),
|
| 411 |
+
think_accuracy=think_accuracy,
|
| 412 |
+
answer_accuracy=answer_accuracy,
|
| 413 |
+
think_samples=int(think_sel.sum()),
|
| 414 |
+
answer_samples=int((~think_sel).sum()),
|
| 415 |
+
control_accuracy=control_accuracy,
|
| 416 |
+
stop_accuracy=stop_accuracy,
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def block_size_curriculum(
|
| 421 |
+
step: int | None, *, n_sizes: int, curriculum_steps: int
|
| 422 |
+
) -> Tensor:
|
| 423 |
+
"""Segment-size sampling weights: smallest-size-only ramping linearly to uniform.
|
| 424 |
+
|
| 425 |
+
``sizes`` are assumed ascending. ``step=None`` (evaluation) and a zero-length
|
| 426 |
+
curriculum both return the uniform end state so losses stay comparable across
|
| 427 |
+
checkpoints.
|
| 428 |
+
"""
|
| 429 |
+
|
| 430 |
+
uniform = torch.full((n_sizes,), 1.0 / n_sizes)
|
| 431 |
+
if step is None or curriculum_steps <= 0:
|
| 432 |
+
return uniform
|
| 433 |
+
progress = min(1.0, step / curriculum_steps)
|
| 434 |
+
smallest_only = torch.zeros(n_sizes)
|
| 435 |
+
smallest_only[0] = 1.0
|
| 436 |
+
return smallest_only * (1.0 - progress) + uniform * progress
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def block_diffusion_objective(
|
| 440 |
+
model: DiffusionTransformer,
|
| 441 |
+
tokens: Tensor,
|
| 442 |
+
*,
|
| 443 |
+
sizes: tuple[int, ...],
|
| 444 |
+
size_weights: Tensor,
|
| 445 |
+
mask_eps: float,
|
| 446 |
+
ar_probability: float = 0.0,
|
| 447 |
+
generator: torch.Generator | None = None,
|
| 448 |
+
) -> HybridBatchLoss:
|
| 449 |
+
"""Variable-block denoising over plain packed text (the conversion objective).
|
| 450 |
+
|
| 451 |
+
Each sample is tiled with segments whose lengths are drawn from ``sizes`` under
|
| 452 |
+
``size_weights``; every segment is corrupted at an independent noise level and
|
| 453 |
+
denoised in one forward under the block-causal geometry (own segment bidirectional,
|
| 454 |
+
all preceding segments visible). No control tokens exist in the stream — pretraining
|
| 455 |
+
teaches variable-block denoising only; control decisions are learned in SFT. With
|
| 456 |
+
probability ``ar_probability`` a sample instead runs plain causal next-token loss,
|
| 457 |
+
retaining the autoregressive ability that control and answer decoding rely on.
|
| 458 |
+
Reported as ``think_*`` (denoising) and ``answer_*`` (causal retention) metrics.
|
| 459 |
+
"""
|
| 460 |
+
|
| 461 |
+
device = tokens.device
|
| 462 |
+
batch_size, seq_len = tokens.shape
|
| 463 |
+
positions = torch.arange(seq_len, device=device)
|
| 464 |
+
|
| 465 |
+
ar_sel = (
|
| 466 |
+
torch.rand(batch_size, device=device, generator=generator) < ar_probability
|
| 467 |
+
)
|
| 468 |
+
if bool(ar_sel.all()):
|
| 469 |
+
ar_sel[0] = False
|
| 470 |
+
diff_sel = ~ar_sel
|
| 471 |
+
|
| 472 |
+
size_tensor = torch.tensor(sizes, device=device, dtype=torch.long)
|
| 473 |
+
max_segments = -(-seq_len // int(min(sizes)))
|
| 474 |
+
drawn_index = torch.multinomial(
|
| 475 |
+
size_weights.to(device).expand(batch_size, -1),
|
| 476 |
+
max_segments,
|
| 477 |
+
replacement=True,
|
| 478 |
+
generator=generator,
|
| 479 |
+
)
|
| 480 |
+
drawn = size_tensor[drawn_index]
|
| 481 |
+
starts = torch.cumsum(drawn, dim=1) - drawn
|
| 482 |
+
valid = starts < seq_len
|
| 483 |
+
boundary_hits = torch.zeros(batch_size, seq_len, dtype=torch.long, device=device)
|
| 484 |
+
boundary_hits.scatter_add_(1, starts.clamp(max=seq_len - 1), valid.long())
|
| 485 |
+
boundary = boundary_hits > 0
|
| 486 |
+
|
| 487 |
+
segment_id = torch.cumsum(boundary.long(), dim=1) - 1
|
| 488 |
+
segment_noise = mask_eps + (1.0 - mask_eps) * torch.rand(
|
| 489 |
+
batch_size, max_segments, device=device, generator=generator
|
| 490 |
+
)
|
| 491 |
+
token_noise = segment_noise.gather(1, segment_id.clamp(max=max_segments - 1))
|
| 492 |
+
mask = (
|
| 493 |
+
torch.rand(tokens.shape, device=device, generator=generator) < token_noise
|
| 494 |
+
) & diff_sel[:, None]
|
| 495 |
+
noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens)
|
| 496 |
+
|
| 497 |
+
zeros = torch.zeros(batch_size, dtype=torch.long, device=device)
|
| 498 |
+
full = torch.full((batch_size,), seq_len, dtype=torch.long, device=device)
|
| 499 |
+
block_blocked = block_mask_from_boundaries(boundary, zeros, full)
|
| 500 |
+
causal_blocked = prefix_causal_blocked(zeros, seq_len, causal_prefix=True)
|
| 501 |
+
blocked = torch.where(diff_sel[:, None, None], block_blocked, causal_blocked)
|
| 502 |
+
|
| 503 |
+
predict_positions = (positions[None, :] < seq_len - 1) & ar_sel[:, None]
|
| 504 |
+
output_positions = mask | predict_positions
|
| 505 |
+
logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked)
|
| 506 |
+
|
| 507 |
+
think_rows = mask[output_positions]
|
| 508 |
+
# Graph-connected zero: forbidden-output columns sit at finfo.min, so a raw sum
|
| 509 |
+
# overflows to -inf in low precision and would poison the scalar via -inf * 0.
|
| 510 |
+
zero = logits.sum().clamp(-1.0, 1.0) * 0.0
|
| 511 |
+
|
| 512 |
+
think_loss = zero
|
| 513 |
+
think_accuracy = 0.0
|
| 514 |
+
if bool(mask.any()):
|
| 515 |
+
think_logits = logits[think_rows]
|
| 516 |
+
think_targets = tokens[mask]
|
| 517 |
+
per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none')
|
| 518 |
+
weights = token_noise[mask]
|
| 519 |
+
normalizer = (diff_sel.sum() * seq_len).clamp_min(1)
|
| 520 |
+
think_loss = (per_token / weights).sum() / normalizer
|
| 521 |
+
think_accuracy = float(
|
| 522 |
+
(think_logits.argmax(dim=-1) == think_targets).float().mean()
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
answer_loss = zero
|
| 526 |
+
answer_accuracy = 0.0
|
| 527 |
+
if bool(predict_positions.any()):
|
| 528 |
+
answer_logits = logits[~think_rows]
|
| 529 |
+
target_positions = torch.zeros_like(predict_positions)
|
| 530 |
+
target_positions[:, 1:] = predict_positions[:, :-1]
|
| 531 |
+
answer_targets = tokens[target_positions]
|
| 532 |
+
answer_loss = F.cross_entropy(answer_logits.float(), answer_targets)
|
| 533 |
+
answer_accuracy = float(
|
| 534 |
+
(answer_logits.argmax(dim=-1) == answer_targets).float().mean()
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
return HybridBatchLoss(
|
| 538 |
+
loss=think_loss + answer_loss,
|
| 539 |
+
think_loss=float(think_loss),
|
| 540 |
+
answer_loss=float(answer_loss),
|
| 541 |
+
think_accuracy=think_accuracy,
|
| 542 |
+
answer_accuracy=answer_accuracy,
|
| 543 |
+
think_samples=int(diff_sel.sum()),
|
| 544 |
+
answer_samples=int(ar_sel.sum()),
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
@dataclass(frozen=True)
|
| 549 |
+
class ARBatchLoss:
|
| 550 |
+
loss: Tensor
|
| 551 |
+
accuracy: float
|
| 552 |
+
token_count: int
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def ar_objective(
|
| 556 |
+
model: DiffusionTransformer, tokens: Tensor, regions: Tensor
|
| 557 |
+
) -> ARBatchLoss:
|
| 558 |
+
"""Prefix-LM next-token objective over the think and answer regions."""
|
| 559 |
+
|
| 560 |
+
device = tokens.device
|
| 561 |
+
_, seq_len = tokens.shape
|
| 562 |
+
problem_len, _, _, answer_end = regions.unbind(dim=1)
|
| 563 |
+
positions = torch.arange(seq_len, device=device)
|
| 564 |
+
|
| 565 |
+
blocked = prefix_causal_blocked(problem_len + 1, seq_len)
|
| 566 |
+
predict_positions = (positions[None, :] >= problem_len[:, None]) & (
|
| 567 |
+
positions[None, :] < answer_end[:, None] - 1
|
| 568 |
+
)
|
| 569 |
+
logits = model(tokens, output_positions=predict_positions, attn_mask=blocked)
|
| 570 |
+
|
| 571 |
+
target_positions = torch.zeros_like(predict_positions)
|
| 572 |
+
target_positions[:, 1:] = predict_positions[:, :-1]
|
| 573 |
+
targets = tokens[target_positions]
|
| 574 |
+
loss = F.cross_entropy(logits.float(), targets)
|
| 575 |
+
accuracy = float((logits.argmax(dim=-1) == targets).float().mean())
|
| 576 |
+
return ARBatchLoss(loss=loss, accuracy=accuracy, token_count=int(targets.numel()))
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def diffusion_objective(
|
| 580 |
+
model: DiffusionTransformer,
|
| 581 |
+
tokens: Tensor,
|
| 582 |
+
regions: Tensor,
|
| 583 |
+
*,
|
| 584 |
+
mask_eps: float,
|
| 585 |
+
mask_probability: Tensor | None = None,
|
| 586 |
+
generator: torch.Generator | None = None,
|
| 587 |
+
) -> tuple[Tensor, CorruptionBatch, Tensor]:
|
| 588 |
+
"""Whole-response denoising: corrupt everything after the problem, pads included."""
|
| 589 |
+
|
| 590 |
+
device = tokens.device
|
| 591 |
+
_, seq_len = tokens.shape
|
| 592 |
+
problem_len = regions[:, 0]
|
| 593 |
+
positions = torch.arange(seq_len, device=device)
|
| 594 |
+
response_mask = positions[None, :] >= (problem_len[:, None] + 1)
|
| 595 |
+
corruption = corrupt_tokens(
|
| 596 |
+
tokens,
|
| 597 |
+
model.config.mask_token_id,
|
| 598 |
+
valid_mask=response_mask,
|
| 599 |
+
mask_probability=mask_probability,
|
| 600 |
+
eps=mask_eps,
|
| 601 |
+
generator=generator,
|
| 602 |
+
)
|
| 603 |
+
logits = model(corruption.noisy_tokens, output_positions=corruption.mask)
|
| 604 |
+
return logits, corruption, response_mask
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
@dataclass
|
| 608 |
+
class GenerationResult:
|
| 609 |
+
tokens: list[int]
|
| 610 |
+
think_tokens: list[int]
|
| 611 |
+
answer_tokens: list[int]
|
| 612 |
+
think_seconds: float = 0.0
|
| 613 |
+
answer_seconds: float = 0.0
|
| 614 |
+
forward_passes: int = 0
|
| 615 |
+
slots_used: int = 0
|
| 616 |
+
block_sizes: tuple[int, ...] = ()
|
| 617 |
+
terminated: bool = False
|
| 618 |
+
|
| 619 |
+
@property
|
| 620 |
+
def total_seconds(self) -> float:
|
| 621 |
+
return self.think_seconds + self.answer_seconds
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
def _apply_repetition_penalty(
|
| 625 |
+
logits: Tensor, token_ids: list[int], penalty: float
|
| 626 |
+
) -> Tensor:
|
| 627 |
+
"""Divide logits of already-emitted tokens by ``penalty`` (CTRL convention)."""
|
| 628 |
+
|
| 629 |
+
if penalty == 1.0 or not token_ids:
|
| 630 |
+
return logits
|
| 631 |
+
index = torch.tensor(sorted(set(token_ids)), device=logits.device)
|
| 632 |
+
selected = logits.index_select(-1, index)
|
| 633 |
+
adjusted = torch.where(selected > 0, selected / penalty, selected * penalty)
|
| 634 |
+
return logits.index_copy(-1, index, adjusted)
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def _apply_top_p(logits: Tensor, top_p: float) -> Tensor:
|
| 638 |
+
"""Restrict sampling to the smallest set of tokens whose mass reaches ``top_p``."""
|
| 639 |
+
|
| 640 |
+
if top_p >= 1.0:
|
| 641 |
+
return logits
|
| 642 |
+
ordered, indices = torch.sort(logits, descending=True, dim=-1)
|
| 643 |
+
cumulative = ordered.softmax(dim=-1).cumsum(dim=-1)
|
| 644 |
+
remove = cumulative - ordered.softmax(dim=-1) >= top_p
|
| 645 |
+
ordered = ordered.masked_fill(remove, torch.finfo(logits.dtype).min)
|
| 646 |
+
return ordered.gather(-1, indices.argsort(dim=-1))
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def _kv_cache_enabled(part: str) -> bool:
|
| 650 |
+
"""Key/value caching per part, selected by ``MDLM_KV_CACHE``: ar, block, all or off.
|
| 651 |
+
|
| 652 |
+
Defaults to ``ar``, which measured 101s to 12.6s on the same prompt's answer: one query
|
| 653 |
+
token against an all-visible mask has no downside. The denoising prefix does — caching it
|
| 654 |
+
hands the backbone a dense mask, dropping flex attention onto its score_mod path and losing
|
| 655 |
+
the block skipping the uncached call gets, 10.6s per block against 5.3s on an L40S
|
| 656 |
+
(2026-07-27). It stays off until that path builds a rectangular BlockMask instead.
|
| 657 |
+
"""
|
| 658 |
+
|
| 659 |
+
setting = os.environ.get('MDLM_KV_CACHE', 'ar')
|
| 660 |
+
return setting in ('all', '1') or setting == part
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _cached_block_logits(model, blocked: Tensor, prefix_len: int):
|
| 664 |
+
"""Score a denoising block against a cached prefix, or ``None`` without cache support.
|
| 665 |
+
|
| 666 |
+
The prefix is encoded once per block; every denoising step then feeds only the block's
|
| 667 |
+
own positions. Its keys and values are dropped between steps because the block's tokens
|
| 668 |
+
keep changing as they are revealed, while the prefix behind them does not.
|
| 669 |
+
"""
|
| 670 |
+
|
| 671 |
+
if not hasattr(model, 'forward_cached') or not _kv_cache_enabled('block'):
|
| 672 |
+
return None
|
| 673 |
+
|
| 674 |
+
cache = model.new_cache()
|
| 675 |
+
|
| 676 |
+
def logits_fn(tokens: Tensor, masked: Tensor) -> Tensor:
|
| 677 |
+
if cache.get_seq_length() == 0:
|
| 678 |
+
with torch.inference_mode():
|
| 679 |
+
model.forward_cached(
|
| 680 |
+
tokens[:, :prefix_len],
|
| 681 |
+
attn_mask=blocked[:, :prefix_len, :prefix_len],
|
| 682 |
+
past_key_values=cache,
|
| 683 |
+
)
|
| 684 |
+
cache.crop(prefix_len)
|
| 685 |
+
with torch.inference_mode():
|
| 686 |
+
logits, _ = model.forward_cached(
|
| 687 |
+
tokens[:, prefix_len:],
|
| 688 |
+
attn_mask=blocked[:, prefix_len:, :],
|
| 689 |
+
past_key_values=cache,
|
| 690 |
+
output_positions=masked[:, prefix_len:],
|
| 691 |
+
)
|
| 692 |
+
return logits
|
| 693 |
+
|
| 694 |
+
return logits_fn
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
def _ar_decode_cached(
|
| 698 |
+
model: DiffusionTransformer,
|
| 699 |
+
sequence: list[int],
|
| 700 |
+
prefix_len: int,
|
| 701 |
+
*,
|
| 702 |
+
causal_prefix: bool,
|
| 703 |
+
eos_id: int,
|
| 704 |
+
max_new_tokens: int,
|
| 705 |
+
temperature: float,
|
| 706 |
+
repetition_penalty: float,
|
| 707 |
+
top_p: float,
|
| 708 |
+
device: torch.device,
|
| 709 |
+
generator: torch.Generator | None,
|
| 710 |
+
) -> tuple[list[int], int]:
|
| 711 |
+
"""Same decoding as :func:`_ar_decode` with the prefix kept in a key/value cache.
|
| 712 |
+
|
| 713 |
+
Masks come from :func:`prefix_causal_blocked` exactly as in the uncached path, sliced to
|
| 714 |
+
the rows of the queries being fed. Priming with an all-visible mask instead would look
|
| 715 |
+
right — the final row is identical, so a single generated token matches — while silently
|
| 716 |
+
computing every earlier position bidirectionally and poisoning the cached keys.
|
| 717 |
+
"""
|
| 718 |
+
|
| 719 |
+
generated: list[int] = []
|
| 720 |
+
cache = model.new_cache()
|
| 721 |
+
step_in = torch.tensor([sequence], dtype=torch.long, device=device)
|
| 722 |
+
forwards = 0
|
| 723 |
+
prefix = torch.tensor([prefix_len], device=device)
|
| 724 |
+
for _ in range(max_new_tokens):
|
| 725 |
+
cached = cache.get_seq_length()
|
| 726 |
+
length = cached + step_in.shape[1]
|
| 727 |
+
visible = prefix_causal_blocked(prefix, length, causal_prefix=causal_prefix)[:, cached:, :]
|
| 728 |
+
output_positions = torch.zeros_like(step_in, dtype=torch.bool)
|
| 729 |
+
output_positions[0, -1] = True
|
| 730 |
+
with torch.inference_mode():
|
| 731 |
+
logits, cache = model.forward_cached(
|
| 732 |
+
step_in, attn_mask=visible, past_key_values=cache,
|
| 733 |
+
output_positions=output_positions,
|
| 734 |
+
)
|
| 735 |
+
logits = _apply_repetition_penalty(logits, generated, repetition_penalty)
|
| 736 |
+
logits = _apply_top_p(logits, top_p)
|
| 737 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 738 |
+
forwards += 1
|
| 739 |
+
token_id = int(token.item())
|
| 740 |
+
generated.append(token_id)
|
| 741 |
+
if token_id == eos_id:
|
| 742 |
+
break
|
| 743 |
+
step_in = torch.tensor([[token_id]], dtype=torch.long, device=device)
|
| 744 |
+
return generated, forwards
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
def _ar_decode(
|
| 748 |
+
model: DiffusionTransformer,
|
| 749 |
+
sequence: list[int],
|
| 750 |
+
prefix_len: int,
|
| 751 |
+
*,
|
| 752 |
+
eos_id: int,
|
| 753 |
+
max_new_tokens: int,
|
| 754 |
+
temperature: float,
|
| 755 |
+
repetition_penalty: float,
|
| 756 |
+
top_p: float,
|
| 757 |
+
device: torch.device,
|
| 758 |
+
generator: torch.Generator | None,
|
| 759 |
+
causal_prefix: bool = False,
|
| 760 |
+
) -> tuple[list[int], int]:
|
| 761 |
+
"""Greedy/temperature decoding under the bidirectional-prefix causal mask.
|
| 762 |
+
|
| 763 |
+
``repetition_penalty`` (>1 discourages repeats) and ``top_p`` nucleus truncation
|
| 764 |
+
curb the degenerate loops small models fall into under plain temperature sampling.
|
| 765 |
+
"""
|
| 766 |
+
|
| 767 |
+
generated: list[int] = []
|
| 768 |
+
forwards = 0
|
| 769 |
+
max_new_tokens = min(max_new_tokens, model.config.max_seq_len - len(sequence))
|
| 770 |
+
if hasattr(model, 'forward_cached') and _kv_cache_enabled('ar'):
|
| 771 |
+
return _ar_decode_cached(
|
| 772 |
+
model, sequence, prefix_len, causal_prefix=causal_prefix, eos_id=eos_id,
|
| 773 |
+
max_new_tokens=max_new_tokens, temperature=temperature,
|
| 774 |
+
repetition_penalty=repetition_penalty, top_p=top_p, device=device,
|
| 775 |
+
generator=generator,
|
| 776 |
+
)
|
| 777 |
+
for _ in range(max_new_tokens):
|
| 778 |
+
current = torch.tensor([sequence + generated], dtype=torch.long, device=device)
|
| 779 |
+
seq_len = current.shape[1]
|
| 780 |
+
prefix = torch.tensor([prefix_len], device=device)
|
| 781 |
+
blocked = prefix_causal_blocked(prefix, seq_len, causal_prefix=causal_prefix)
|
| 782 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 783 |
+
output_positions[0, -1] = True
|
| 784 |
+
with torch.inference_mode():
|
| 785 |
+
logits = model(current, output_positions=output_positions, attn_mask=blocked)
|
| 786 |
+
logits = _apply_repetition_penalty(logits, generated, repetition_penalty)
|
| 787 |
+
logits = _apply_top_p(logits, top_p)
|
| 788 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 789 |
+
forwards += 1
|
| 790 |
+
token_id = int(token.item())
|
| 791 |
+
generated.append(token_id)
|
| 792 |
+
if token_id == eos_id:
|
| 793 |
+
break
|
| 794 |
+
return generated, forwards
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
@torch.no_grad()
|
| 798 |
+
def generate_hybrid(
|
| 799 |
+
model: DiffusionTransformer,
|
| 800 |
+
prompt_ids: list[int],
|
| 801 |
+
*,
|
| 802 |
+
think_id: int,
|
| 803 |
+
end_think_id: int,
|
| 804 |
+
thought_pad_id: int,
|
| 805 |
+
eos_id: int,
|
| 806 |
+
block: int,
|
| 807 |
+
max_slots: int,
|
| 808 |
+
steps_per_block: int,
|
| 809 |
+
max_answer_tokens: int = 64,
|
| 810 |
+
temperature: float = 0.7,
|
| 811 |
+
repetition_penalty: float = 1.0,
|
| 812 |
+
top_p: float = 1.0,
|
| 813 |
+
strategy: str = 'confidence',
|
| 814 |
+
device: torch.device | str = 'cpu',
|
| 815 |
+
generator: torch.Generator | None = None,
|
| 816 |
+
) -> GenerationResult:
|
| 817 |
+
"""Denoise thought slots sequentially, then decode the answer autoregressively."""
|
| 818 |
+
|
| 819 |
+
device = torch.device(device)
|
| 820 |
+
mask_id = model.config.mask_token_id
|
| 821 |
+
sequence = [*prompt_ids, think_id]
|
| 822 |
+
forwards = 0
|
| 823 |
+
slots_used = 0
|
| 824 |
+
# Each new slot must leave room for itself plus at least a minimal answer.
|
| 825 |
+
slot_budget = model.config.max_seq_len - block - 8
|
| 826 |
+
|
| 827 |
+
think_started = time.perf_counter()
|
| 828 |
+
problem_tensor = torch.tensor([len(prompt_ids)], device=device)
|
| 829 |
+
for slot_index in range(max_slots):
|
| 830 |
+
if len(sequence) > slot_budget:
|
| 831 |
+
break
|
| 832 |
+
window = torch.tensor(
|
| 833 |
+
[sequence + [mask_id] * block], dtype=torch.long, device=device
|
| 834 |
+
)
|
| 835 |
+
blocked = slot_causal_blocked(
|
| 836 |
+
problem_tensor,
|
| 837 |
+
torch.tensor([slot_index + 1], device=device),
|
| 838 |
+
block,
|
| 839 |
+
window.shape[1],
|
| 840 |
+
)
|
| 841 |
+
filled = iterative_unmask(
|
| 842 |
+
model,
|
| 843 |
+
window,
|
| 844 |
+
mask_id,
|
| 845 |
+
steps=steps_per_block,
|
| 846 |
+
temperature=temperature,
|
| 847 |
+
strategy=strategy,
|
| 848 |
+
attn_mask=blocked,
|
| 849 |
+
generator=generator,
|
| 850 |
+
)
|
| 851 |
+
slot = [int(token) for token in filled[0, len(sequence):]]
|
| 852 |
+
forwards += steps_per_block
|
| 853 |
+
slots_used += 1
|
| 854 |
+
sequence.extend(slot)
|
| 855 |
+
if end_think_id in slot:
|
| 856 |
+
break
|
| 857 |
+
if end_think_id not in sequence[len(prompt_ids):]:
|
| 858 |
+
# Match the trained answer geometry when the model never closes its thinking.
|
| 859 |
+
terminal = [end_think_id] + [thought_pad_id] * (block - 1)
|
| 860 |
+
sequence.extend(terminal[: max(1, model.config.max_seq_len - 8 - len(sequence))])
|
| 861 |
+
think_seconds = time.perf_counter() - think_started
|
| 862 |
+
think_tokens = sequence[len(prompt_ids):]
|
| 863 |
+
|
| 864 |
+
answer_started = time.perf_counter()
|
| 865 |
+
answer, answer_forwards = _ar_decode(
|
| 866 |
+
model,
|
| 867 |
+
sequence,
|
| 868 |
+
prefix_len=len(sequence),
|
| 869 |
+
eos_id=eos_id,
|
| 870 |
+
max_new_tokens=max_answer_tokens,
|
| 871 |
+
temperature=temperature,
|
| 872 |
+
repetition_penalty=repetition_penalty,
|
| 873 |
+
top_p=top_p,
|
| 874 |
+
device=device,
|
| 875 |
+
generator=generator,
|
| 876 |
+
)
|
| 877 |
+
answer_seconds = time.perf_counter() - answer_started
|
| 878 |
+
return GenerationResult(
|
| 879 |
+
tokens=sequence + answer,
|
| 880 |
+
think_tokens=think_tokens,
|
| 881 |
+
answer_tokens=answer,
|
| 882 |
+
think_seconds=think_seconds,
|
| 883 |
+
answer_seconds=answer_seconds,
|
| 884 |
+
forward_passes=forwards + answer_forwards,
|
| 885 |
+
slots_used=slots_used,
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
def _ar_predict_control(
|
| 890 |
+
model: DiffusionTransformer,
|
| 891 |
+
sequence: list[int],
|
| 892 |
+
allowed_ids: list[int],
|
| 893 |
+
*,
|
| 894 |
+
prefix_len: int,
|
| 895 |
+
temperature: float,
|
| 896 |
+
device: torch.device,
|
| 897 |
+
generator: torch.Generator | None,
|
| 898 |
+
causal_prefix: bool = False,
|
| 899 |
+
) -> int:
|
| 900 |
+
"""Predict the next control token, restricted to the allowed size/stop ids."""
|
| 901 |
+
|
| 902 |
+
current = torch.tensor([sequence], dtype=torch.long, device=device)
|
| 903 |
+
seq_len = current.shape[1]
|
| 904 |
+
blocked = prefix_causal_blocked(
|
| 905 |
+
torch.tensor([prefix_len], device=device), seq_len, causal_prefix=causal_prefix
|
| 906 |
+
)
|
| 907 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 908 |
+
output_positions[0, -1] = True
|
| 909 |
+
with torch.inference_mode():
|
| 910 |
+
logits = model(current, output_positions=output_positions, attn_mask=blocked)
|
| 911 |
+
restricted = torch.full_like(logits, torch.finfo(logits.dtype).min)
|
| 912 |
+
index = torch.tensor(allowed_ids, device=logits.device)
|
| 913 |
+
restricted.index_copy_(-1, index, logits.index_select(-1, index))
|
| 914 |
+
token, _ = _sample_categorical(restricted, temperature, generator)
|
| 915 |
+
return int(token.item())
|
| 916 |
+
|
| 917 |
+
|
| 918 |
+
@torch.no_grad()
|
| 919 |
+
def generate_hybrid_adaptive(
|
| 920 |
+
model: DiffusionTransformer,
|
| 921 |
+
prompt_ids: list[int],
|
| 922 |
+
*,
|
| 923 |
+
think_id: int,
|
| 924 |
+
end_think_id: int,
|
| 925 |
+
thought_pad_id: int,
|
| 926 |
+
eos_id: int,
|
| 927 |
+
size_ids: dict[int, int],
|
| 928 |
+
steps_per_block: int,
|
| 929 |
+
max_blocks: int,
|
| 930 |
+
max_answer_tokens: int = 96,
|
| 931 |
+
temperature: float = 0.7,
|
| 932 |
+
control_temperature: float = 0.0,
|
| 933 |
+
repetition_penalty: float = 1.0,
|
| 934 |
+
top_p: float = 1.0,
|
| 935 |
+
strategy: str = 'confidence',
|
| 936 |
+
causal_prefix: bool = False,
|
| 937 |
+
device: torch.device | str = 'cpu',
|
| 938 |
+
generator: torch.Generator | None = None,
|
| 939 |
+
) -> GenerationResult:
|
| 940 |
+
"""Interleave AR block-size decisions with in-block diffusion, then decode.
|
| 941 |
+
|
| 942 |
+
At each boundary the model predicts a ``<szN>`` control token or ``</think>``.
|
| 943 |
+
A size token allocates that many masked positions denoised in parallel under the
|
| 944 |
+
variable-boundary block-causal mask; ``</think>`` ends thinking. The answer is
|
| 945 |
+
then decoded autoregressively with the same repetition and nucleus controls.
|
| 946 |
+
"""
|
| 947 |
+
|
| 948 |
+
device = torch.device(device)
|
| 949 |
+
mask_id = model.config.mask_token_id
|
| 950 |
+
size_by_id = {token_id: size for size, token_id in size_ids.items()}
|
| 951 |
+
size_ids_tensor = torch.tensor(sorted(size_ids.values()), device=device)
|
| 952 |
+
control_ids = [*size_by_id.keys(), end_think_id]
|
| 953 |
+
prefix_len = len(prompt_ids) + 1
|
| 954 |
+
problem_tensor = torch.tensor([len(prompt_ids)], device=device)
|
| 955 |
+
|
| 956 |
+
sequence = [*prompt_ids, think_id]
|
| 957 |
+
forwards = 0
|
| 958 |
+
blocks_used = 0
|
| 959 |
+
chosen: list[int] = []
|
| 960 |
+
terminated = False
|
| 961 |
+
|
| 962 |
+
think_started = time.perf_counter()
|
| 963 |
+
for _ in range(max_blocks):
|
| 964 |
+
control = _ar_predict_control(
|
| 965 |
+
model,
|
| 966 |
+
sequence,
|
| 967 |
+
control_ids,
|
| 968 |
+
prefix_len=prefix_len,
|
| 969 |
+
temperature=control_temperature,
|
| 970 |
+
device=device,
|
| 971 |
+
generator=generator,
|
| 972 |
+
causal_prefix=causal_prefix,
|
| 973 |
+
)
|
| 974 |
+
forwards += 1
|
| 975 |
+
if control == end_think_id:
|
| 976 |
+
terminated = True
|
| 977 |
+
break
|
| 978 |
+
size = size_by_id[control]
|
| 979 |
+
if len(sequence) + 1 + size > model.config.max_seq_len - 8:
|
| 980 |
+
break
|
| 981 |
+
sequence.append(control)
|
| 982 |
+
window_prefix = len(sequence)
|
| 983 |
+
window = torch.tensor(
|
| 984 |
+
[sequence + [mask_id] * size], dtype=torch.long, device=device
|
| 985 |
+
)
|
| 986 |
+
blocked = adaptive_block_mask(
|
| 987 |
+
window,
|
| 988 |
+
problem_tensor,
|
| 989 |
+
torch.tensor([window.shape[1]], device=device),
|
| 990 |
+
size_ids_tensor,
|
| 991 |
+
end_think_id,
|
| 992 |
+
causal_prefix=causal_prefix,
|
| 993 |
+
)
|
| 994 |
+
filled = iterative_unmask(
|
| 995 |
+
model,
|
| 996 |
+
window,
|
| 997 |
+
mask_id,
|
| 998 |
+
steps=steps_per_block,
|
| 999 |
+
temperature=temperature,
|
| 1000 |
+
strategy=strategy,
|
| 1001 |
+
attn_mask=blocked,
|
| 1002 |
+
generator=generator,
|
| 1003 |
+
logits_fn=_cached_block_logits(model, blocked, window_prefix),
|
| 1004 |
+
)
|
| 1005 |
+
sequence.extend(int(token) for token in filled[0, window_prefix:])
|
| 1006 |
+
forwards += steps_per_block
|
| 1007 |
+
blocks_used += 1
|
| 1008 |
+
chosen.append(size)
|
| 1009 |
+
|
| 1010 |
+
sequence.append(end_think_id)
|
| 1011 |
+
think_seconds = time.perf_counter() - think_started
|
| 1012 |
+
think_tokens = sequence[len(prompt_ids):]
|
| 1013 |
+
|
| 1014 |
+
answer_started = time.perf_counter()
|
| 1015 |
+
answer, answer_forwards = _ar_decode(
|
| 1016 |
+
model,
|
| 1017 |
+
sequence,
|
| 1018 |
+
prefix_len=prefix_len,
|
| 1019 |
+
eos_id=eos_id,
|
| 1020 |
+
max_new_tokens=max_answer_tokens,
|
| 1021 |
+
temperature=temperature,
|
| 1022 |
+
repetition_penalty=repetition_penalty,
|
| 1023 |
+
top_p=top_p,
|
| 1024 |
+
device=device,
|
| 1025 |
+
generator=generator,
|
| 1026 |
+
causal_prefix=causal_prefix,
|
| 1027 |
+
)
|
| 1028 |
+
answer_seconds = time.perf_counter() - answer_started
|
| 1029 |
+
return GenerationResult(
|
| 1030 |
+
tokens=sequence + answer,
|
| 1031 |
+
think_tokens=think_tokens,
|
| 1032 |
+
answer_tokens=answer,
|
| 1033 |
+
think_seconds=think_seconds,
|
| 1034 |
+
answer_seconds=answer_seconds,
|
| 1035 |
+
forward_passes=forwards + answer_forwards,
|
| 1036 |
+
slots_used=blocks_used,
|
| 1037 |
+
block_sizes=tuple(chosen),
|
| 1038 |
+
terminated=terminated,
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
+
@torch.no_grad()
|
| 1043 |
+
def generate_ar(
|
| 1044 |
+
model: DiffusionTransformer,
|
| 1045 |
+
prompt_ids: list[int],
|
| 1046 |
+
*,
|
| 1047 |
+
think_id: int,
|
| 1048 |
+
end_think_id: int,
|
| 1049 |
+
eos_id: int,
|
| 1050 |
+
max_new_tokens: int = 384,
|
| 1051 |
+
temperature: float = 0.7,
|
| 1052 |
+
device: torch.device | str = 'cpu',
|
| 1053 |
+
generator: torch.Generator | None = None,
|
| 1054 |
+
) -> GenerationResult:
|
| 1055 |
+
"""Classic sequential CoT baseline under the prefix-LM mask."""
|
| 1056 |
+
|
| 1057 |
+
device = torch.device(device)
|
| 1058 |
+
sequence = [*prompt_ids, think_id]
|
| 1059 |
+
started = time.perf_counter()
|
| 1060 |
+
generated, forwards = _ar_decode(
|
| 1061 |
+
model,
|
| 1062 |
+
sequence,
|
| 1063 |
+
prefix_len=len(sequence),
|
| 1064 |
+
eos_id=eos_id,
|
| 1065 |
+
max_new_tokens=max_new_tokens,
|
| 1066 |
+
temperature=temperature,
|
| 1067 |
+
device=device,
|
| 1068 |
+
generator=generator,
|
| 1069 |
+
)
|
| 1070 |
+
elapsed = time.perf_counter() - started
|
| 1071 |
+
if end_think_id in generated:
|
| 1072 |
+
split = generated.index(end_think_id) + 1
|
| 1073 |
+
else:
|
| 1074 |
+
split = len(generated)
|
| 1075 |
+
return GenerationResult(
|
| 1076 |
+
tokens=sequence + generated,
|
| 1077 |
+
think_tokens=generated[:split],
|
| 1078 |
+
answer_tokens=generated[split:],
|
| 1079 |
+
think_seconds=elapsed,
|
| 1080 |
+
answer_seconds=0.0,
|
| 1081 |
+
forward_passes=forwards,
|
| 1082 |
+
)
|
| 1083 |
+
|
| 1084 |
+
|
| 1085 |
+
@torch.no_grad()
|
| 1086 |
+
def generate_diffusion(
|
| 1087 |
+
model: DiffusionTransformer,
|
| 1088 |
+
prompt_ids: list[int],
|
| 1089 |
+
*,
|
| 1090 |
+
think_id: int,
|
| 1091 |
+
end_think_id: int,
|
| 1092 |
+
eos_id: int,
|
| 1093 |
+
response_budget: int,
|
| 1094 |
+
steps: int,
|
| 1095 |
+
temperature: float = 0.7,
|
| 1096 |
+
blocked_token_ids: tuple[int, ...] = (),
|
| 1097 |
+
device: torch.device | str = 'cpu',
|
| 1098 |
+
generator: torch.Generator | None = None,
|
| 1099 |
+
) -> GenerationResult:
|
| 1100 |
+
"""Pure-diffusion baseline: denoise the entire response region at once.
|
| 1101 |
+
|
| 1102 |
+
Blocking the pad token here counters confidence-ordered pad collapse: pads are
|
| 1103 |
+
the easiest predictions, so left unblocked they win every early reveal and
|
| 1104 |
+
squeeze out the actual response text.
|
| 1105 |
+
"""
|
| 1106 |
+
|
| 1107 |
+
device = torch.device(device)
|
| 1108 |
+
mask_id = model.config.mask_token_id
|
| 1109 |
+
budget = min(response_budget, model.config.max_seq_len - len(prompt_ids) - 1)
|
| 1110 |
+
sequence = torch.tensor(
|
| 1111 |
+
[[*prompt_ids, think_id] + [mask_id] * budget], dtype=torch.long, device=device
|
| 1112 |
+
)
|
| 1113 |
+
started = time.perf_counter()
|
| 1114 |
+
filled = iterative_unmask(
|
| 1115 |
+
model,
|
| 1116 |
+
sequence,
|
| 1117 |
+
mask_id,
|
| 1118 |
+
steps=steps,
|
| 1119 |
+
temperature=temperature,
|
| 1120 |
+
strategy='confidence',
|
| 1121 |
+
blocked_token_ids=blocked_token_ids,
|
| 1122 |
+
generator=generator,
|
| 1123 |
+
)
|
| 1124 |
+
elapsed = time.perf_counter() - started
|
| 1125 |
+
response = [int(token) for token in filled[0, len(prompt_ids) + 1:]]
|
| 1126 |
+
if eos_id in response:
|
| 1127 |
+
response = response[: response.index(eos_id) + 1]
|
| 1128 |
+
if end_think_id in response:
|
| 1129 |
+
split = response.index(end_think_id) + 1
|
| 1130 |
+
else:
|
| 1131 |
+
split = len(response)
|
| 1132 |
+
return GenerationResult(
|
| 1133 |
+
tokens=[*prompt_ids, think_id] + response,
|
| 1134 |
+
think_tokens=response[:split],
|
| 1135 |
+
answer_tokens=response[split:],
|
| 1136 |
+
think_seconds=elapsed,
|
| 1137 |
+
answer_seconds=0.0,
|
| 1138 |
+
forward_passes=steps,
|
| 1139 |
+
)
|
src/diffusion_lm/model.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GPT-sized bidirectional Transformer used as a masked-token denoiser."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import math
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch import Tensor, nn
|
| 11 |
+
from torch.utils.checkpoint import checkpoint
|
| 12 |
+
|
| 13 |
+
from diffusion_lm.config import ModelConfig, load_config
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class DiffusionTransformer(nn.Module):
|
| 17 |
+
"""A GPT-like Transformer with the causal mask deliberately removed.
|
| 18 |
+
|
| 19 |
+
The network predicts clean tokens from an input containing absorbing mask
|
| 20 |
+
tokens. Passing ``output_positions`` avoids materializing vocabulary logits
|
| 21 |
+
for already-visible tokens during training.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, config: ModelConfig) -> None:
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.config = config
|
| 27 |
+
self.tokenizer_sha256: str | None = None
|
| 28 |
+
self.token_embedding = nn.Embedding(config.vocab_size, config.d_model)
|
| 29 |
+
self.position_embedding = nn.Embedding(config.max_seq_len, config.d_model)
|
| 30 |
+
self.embedding_dropout = nn.Dropout(config.dropout)
|
| 31 |
+
|
| 32 |
+
if config.use_flex_attention:
|
| 33 |
+
from diffusion_lm.flexattn import FlexEncoder
|
| 34 |
+
|
| 35 |
+
self.transformer = FlexEncoder(
|
| 36 |
+
d_model=config.d_model,
|
| 37 |
+
n_heads=config.n_heads,
|
| 38 |
+
d_ff=config.d_ff,
|
| 39 |
+
dropout=config.dropout,
|
| 40 |
+
n_layers=config.n_layers,
|
| 41 |
+
activation_checkpointing=config.activation_checkpointing,
|
| 42 |
+
)
|
| 43 |
+
else:
|
| 44 |
+
layer = nn.TransformerEncoderLayer(
|
| 45 |
+
d_model=config.d_model,
|
| 46 |
+
nhead=config.n_heads,
|
| 47 |
+
dim_feedforward=config.d_ff,
|
| 48 |
+
dropout=config.dropout,
|
| 49 |
+
activation="gelu",
|
| 50 |
+
batch_first=True,
|
| 51 |
+
norm_first=True,
|
| 52 |
+
)
|
| 53 |
+
self.transformer = nn.TransformerEncoder(
|
| 54 |
+
layer,
|
| 55 |
+
num_layers=config.n_layers,
|
| 56 |
+
norm=nn.LayerNorm(config.d_model),
|
| 57 |
+
enable_nested_tensor=False,
|
| 58 |
+
)
|
| 59 |
+
self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
|
| 60 |
+
|
| 61 |
+
self.apply(self._init_weights)
|
| 62 |
+
self._init_residual_outputs()
|
| 63 |
+
if config.tie_embeddings:
|
| 64 |
+
self.lm_head.weight = self.token_embedding.weight
|
| 65 |
+
self.register_buffer(
|
| 66 |
+
"_forbidden_output_token_ids",
|
| 67 |
+
torch.tensor(config.forbidden_output_token_ids, dtype=torch.long),
|
| 68 |
+
persistent=False,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def _init_weights(module: nn.Module) -> None:
|
| 73 |
+
if isinstance(module, (nn.Linear, nn.Embedding)):
|
| 74 |
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| 75 |
+
if isinstance(module, nn.Linear) and module.bias is not None:
|
| 76 |
+
nn.init.zeros_(module.bias)
|
| 77 |
+
elif isinstance(module, nn.LayerNorm):
|
| 78 |
+
nn.init.ones_(module.weight)
|
| 79 |
+
nn.init.zeros_(module.bias)
|
| 80 |
+
|
| 81 |
+
def _init_residual_outputs(self) -> None:
|
| 82 |
+
"""Scale residual branch outputs as in GPT-2 for stable deep training."""
|
| 83 |
+
|
| 84 |
+
if self.config.use_flex_attention:
|
| 85 |
+
self.transformer.init_residual_outputs(self.config.n_layers)
|
| 86 |
+
return
|
| 87 |
+
residual_std = 0.02 / math.sqrt(2 * self.config.n_layers)
|
| 88 |
+
for layer in self.transformer.layers:
|
| 89 |
+
nn.init.normal_(layer.self_attn.out_proj.weight, mean=0.0, std=residual_std)
|
| 90 |
+
nn.init.normal_(layer.linear2.weight, mean=0.0, std=residual_std)
|
| 91 |
+
|
| 92 |
+
def _checkpointed_transformer(
|
| 93 |
+
self,
|
| 94 |
+
hidden: Tensor,
|
| 95 |
+
padding_mask: Tensor | None,
|
| 96 |
+
attn_mask: Tensor | None = None,
|
| 97 |
+
) -> Tensor:
|
| 98 |
+
for layer in self.transformer.layers:
|
| 99 |
+
|
| 100 |
+
def run_layer(layer_input: Tensor, *, current_layer: nn.Module = layer) -> Tensor:
|
| 101 |
+
return current_layer(
|
| 102 |
+
layer_input, src_mask=attn_mask, src_key_padding_mask=padding_mask
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
hidden = checkpoint(run_layer, hidden, use_reentrant=False)
|
| 106 |
+
|
| 107 |
+
if self.transformer.norm is not None:
|
| 108 |
+
hidden = self.transformer.norm(hidden)
|
| 109 |
+
return hidden
|
| 110 |
+
|
| 111 |
+
def _expand_attn_mask(self, attn_mask: Tensor | None, input_ids: Tensor) -> Tensor | None:
|
| 112 |
+
"""Broadcast a per-sample boolean blocking mask across attention heads.
|
| 113 |
+
|
| 114 |
+
Accepts ``[L, L]`` shared masks or ``[B, L, L]`` per-sample masks with
|
| 115 |
+
``True`` marking blocked key positions, matching the src_mask convention.
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
if attn_mask is None:
|
| 119 |
+
return None
|
| 120 |
+
batch_size, sequence_length = input_ids.shape
|
| 121 |
+
if attn_mask.dtype != torch.bool:
|
| 122 |
+
raise ValueError("attn_mask must be boolean with True marking blocked positions")
|
| 123 |
+
if attn_mask.shape == (sequence_length, sequence_length):
|
| 124 |
+
return attn_mask
|
| 125 |
+
if attn_mask.shape != (batch_size, sequence_length, sequence_length):
|
| 126 |
+
raise ValueError("attn_mask must have shape [L, L] or [batch, L, L]")
|
| 127 |
+
return attn_mask.repeat_interleave(self.config.n_heads, dim=0)
|
| 128 |
+
|
| 129 |
+
def encode(
|
| 130 |
+
self,
|
| 131 |
+
input_ids: Tensor,
|
| 132 |
+
attention_mask: Tensor | None = None,
|
| 133 |
+
attn_mask: Tensor | None = None,
|
| 134 |
+
) -> Tensor:
|
| 135 |
+
"""Return contextual token states; ``attn_mask`` restricts attention topology."""
|
| 136 |
+
|
| 137 |
+
if input_ids.ndim != 2:
|
| 138 |
+
raise ValueError("input_ids must have shape [batch, sequence]")
|
| 139 |
+
batch_size, sequence_length = input_ids.shape
|
| 140 |
+
if sequence_length > self.config.max_seq_len:
|
| 141 |
+
raise ValueError(
|
| 142 |
+
f"sequence length {sequence_length} exceeds max_seq_len "
|
| 143 |
+
f"{self.config.max_seq_len}"
|
| 144 |
+
)
|
| 145 |
+
if attention_mask is not None and attention_mask.shape != input_ids.shape:
|
| 146 |
+
raise ValueError("attention_mask must match input_ids")
|
| 147 |
+
|
| 148 |
+
positions = torch.arange(sequence_length, device=input_ids.device)
|
| 149 |
+
hidden = self.token_embedding(input_ids) + self.position_embedding(positions)[None, :, :]
|
| 150 |
+
hidden = self.embedding_dropout(hidden)
|
| 151 |
+
|
| 152 |
+
# TransformerEncoder expects True for padding, the inverse of the common
|
| 153 |
+
# attention-mask convention. src_mask is only supplied by region-aware callers.
|
| 154 |
+
padding_mask = None if attention_mask is None else ~attention_mask.bool()
|
| 155 |
+
|
| 156 |
+
if self.config.use_flex_attention:
|
| 157 |
+
from diffusion_lm.flexattn import build_block_mask
|
| 158 |
+
|
| 159 |
+
if attn_mask is not None and attn_mask.dtype != torch.bool:
|
| 160 |
+
raise ValueError("attn_mask must be boolean with True marking blocked positions")
|
| 161 |
+
block_mask = build_block_mask(
|
| 162 |
+
attn_mask, padding_mask, batch_size, sequence_length, hidden.device
|
| 163 |
+
)
|
| 164 |
+
return self.transformer(hidden, block_mask)
|
| 165 |
+
|
| 166 |
+
expanded_attn_mask = self._expand_attn_mask(attn_mask, input_ids)
|
| 167 |
+
if (
|
| 168 |
+
self.config.activation_checkpointing
|
| 169 |
+
and self.training
|
| 170 |
+
and torch.is_grad_enabled()
|
| 171 |
+
):
|
| 172 |
+
return self._checkpointed_transformer(hidden, padding_mask, expanded_attn_mask)
|
| 173 |
+
return self.transformer(
|
| 174 |
+
hidden, mask=expanded_attn_mask, src_key_padding_mask=padding_mask
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
def forward(
|
| 178 |
+
self,
|
| 179 |
+
input_ids: Tensor,
|
| 180 |
+
attention_mask: Tensor | None = None,
|
| 181 |
+
output_positions: Tensor | None = None,
|
| 182 |
+
attn_mask: Tensor | None = None,
|
| 183 |
+
) -> Tensor:
|
| 184 |
+
"""Predict vocabulary logits for all tokens or selected positions only."""
|
| 185 |
+
|
| 186 |
+
hidden = self.encode(input_ids, attention_mask=attention_mask, attn_mask=attn_mask)
|
| 187 |
+
if output_positions is not None:
|
| 188 |
+
if output_positions.shape != input_ids.shape:
|
| 189 |
+
raise ValueError("output_positions must match input_ids")
|
| 190 |
+
hidden = hidden[output_positions.bool()]
|
| 191 |
+
|
| 192 |
+
logits = self.lm_head(hidden)
|
| 193 |
+
# Corruption/control tokens are never valid clean-token predictions. EOS
|
| 194 |
+
# deliberately remains available so generation can terminate naturally.
|
| 195 |
+
if self._forbidden_output_token_ids.numel():
|
| 196 |
+
logits.index_fill_(
|
| 197 |
+
-1,
|
| 198 |
+
self._forbidden_output_token_ids,
|
| 199 |
+
torch.finfo(logits.dtype).min,
|
| 200 |
+
)
|
| 201 |
+
return logits
|
| 202 |
+
|
| 203 |
+
@property
|
| 204 |
+
def num_parameters(self) -> int:
|
| 205 |
+
"""Count unique trainable parameters (shared embeddings count once)."""
|
| 206 |
+
|
| 207 |
+
return sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def build_denoiser(
|
| 211 |
+
config: ModelConfig,
|
| 212 |
+
*,
|
| 213 |
+
load_pretrained: bool = True,
|
| 214 |
+
dtype: torch.dtype | None = None,
|
| 215 |
+
) -> nn.Module:
|
| 216 |
+
"""Construct the denoiser a config describes: project transformer or pretrained backbone.
|
| 217 |
+
|
| 218 |
+
``load_pretrained=False`` builds the architecture only, for callers that immediately
|
| 219 |
+
restore weights from a project checkpoint.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
if config.backbone == "hf-qwen3":
|
| 223 |
+
from diffusion_lm.hf_bridge import Qwen3Denoiser
|
| 224 |
+
|
| 225 |
+
return Qwen3Denoiser(config, load_pretrained=load_pretrained, dtype=dtype)
|
| 226 |
+
return DiffusionTransformer(config)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def format_parameter_count(count: int) -> str:
|
| 230 |
+
if count >= 1_000_000:
|
| 231 |
+
return f"{count / 1_000_000:.2f}M"
|
| 232 |
+
if count >= 1_000:
|
| 233 |
+
return f"{count / 1_000:.2f}K"
|
| 234 |
+
return str(count)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def main() -> None:
|
| 238 |
+
parser = argparse.ArgumentParser(description="Report the exact model parameter count")
|
| 239 |
+
parser.add_argument("--config", type=Path, required=True, help="experiment YAML")
|
| 240 |
+
args = parser.parse_args()
|
| 241 |
+
|
| 242 |
+
config = load_config(args.config)
|
| 243 |
+
# Parameter inspection should not allocate four gigabytes for the 1B preset.
|
| 244 |
+
with torch.device("meta"):
|
| 245 |
+
model = DiffusionTransformer(config.model)
|
| 246 |
+
print(f"parameters: {model.num_parameters:,} ({format_parameter_count(model.num_parameters)})")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
main()
|
src/diffusion_lm/playground.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local browser playground that streams the token-unmasking process."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import hashlib
|
| 7 |
+
import html
|
| 8 |
+
import secrets
|
| 9 |
+
import threading
|
| 10 |
+
import time
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Literal
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
from torch import Tensor
|
| 17 |
+
from tokenizers import Tokenizer
|
| 18 |
+
|
| 19 |
+
from diffusion_lm.diffusion import UnmaskStep, iterative_unmask_steps
|
| 20 |
+
from diffusion_lm.model import DiffusionTransformer, format_parameter_count
|
| 21 |
+
from diffusion_lm.sample import load_model
|
| 22 |
+
from diffusion_lm.tokenizer import load_tokenizer, special_token_id, special_token_ids
|
| 23 |
+
from diffusion_lm.train import resolve_device
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True)
|
| 27 |
+
class GenerationSettings:
|
| 28 |
+
prompt: str = ""
|
| 29 |
+
generation_length: int = 64
|
| 30 |
+
steps: int = 64
|
| 31 |
+
temperature: float = 0.8
|
| 32 |
+
strategy: Literal["ancestral", "confidence"] = "confidence"
|
| 33 |
+
seed: int = 1337
|
| 34 |
+
|
| 35 |
+
def __post_init__(self) -> None:
|
| 36 |
+
if self.generation_length <= 0:
|
| 37 |
+
raise ValueError("La longitud debe ser mayor que cero.")
|
| 38 |
+
if not 1 <= self.steps <= 512:
|
| 39 |
+
raise ValueError("Los pasos deben estar entre 1 y 512.")
|
| 40 |
+
if not 0.0 <= self.temperature <= 5.0:
|
| 41 |
+
raise ValueError("La temperatura debe estar entre 0 y 5.")
|
| 42 |
+
if self.strategy not in {"ancestral", "confidence"}:
|
| 43 |
+
raise ValueError("La estrategia debe ser ancestral o confidence.")
|
| 44 |
+
if not 0 <= self.seed < 2**63:
|
| 45 |
+
raise ValueError("La semilla debe estar entre 0 y 2^63-1.")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True)
|
| 49 |
+
class PlaygroundUpdate:
|
| 50 |
+
state: UnmaskStep
|
| 51 |
+
prompt_tokens: int
|
| 52 |
+
partial_text: str
|
| 53 |
+
final_text: str
|
| 54 |
+
token_html: str
|
| 55 |
+
elapsed_seconds: float
|
| 56 |
+
step_seconds: float
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _seed_generation(device: torch.device, seed: int) -> None:
|
| 60 |
+
torch.manual_seed(seed)
|
| 61 |
+
if device.type == "cuda":
|
| 62 |
+
torch.cuda.manual_seed_all(seed)
|
| 63 |
+
elif device.type == "mps" and hasattr(torch.mps, "manual_seed"):
|
| 64 |
+
torch.mps.manual_seed(seed)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _synchronize(device: torch.device) -> None:
|
| 68 |
+
if device.type == "cuda":
|
| 69 |
+
torch.cuda.synchronize(device)
|
| 70 |
+
elif device.type == "mps":
|
| 71 |
+
torch.mps.synchronize()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _token_label(tokenizer: Tokenizer, token_id: int, mask_token_id: int) -> str:
|
| 75 |
+
raw = tokenizer.id_to_token(token_id) or f"#{token_id}"
|
| 76 |
+
if token_id == mask_token_id:
|
| 77 |
+
return "MASK"
|
| 78 |
+
return (
|
| 79 |
+
raw.replace("Ġ", "▁")
|
| 80 |
+
.replace("Ċ", "↵")
|
| 81 |
+
.replace("ĉ", "⇥")
|
| 82 |
+
.replace("\n", "↵")
|
| 83 |
+
) or "∅"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def render_token_grid(
|
| 87 |
+
tokenizer: Tokenizer,
|
| 88 |
+
token_ids: list[int],
|
| 89 |
+
*,
|
| 90 |
+
mask_token_id: int,
|
| 91 |
+
prompt_tokens: int,
|
| 92 |
+
previous_token_ids: list[int] | None,
|
| 93 |
+
) -> str:
|
| 94 |
+
"""Render escaped token chips for a single sample."""
|
| 95 |
+
|
| 96 |
+
chips: list[str] = []
|
| 97 |
+
for position, token_id in enumerate(token_ids):
|
| 98 |
+
if position < prompt_tokens:
|
| 99 |
+
state = "prompt"
|
| 100 |
+
elif token_id == mask_token_id:
|
| 101 |
+
state = "mask"
|
| 102 |
+
elif previous_token_ids is not None and previous_token_ids[position] == mask_token_id:
|
| 103 |
+
state = "new"
|
| 104 |
+
else:
|
| 105 |
+
state = "revealed"
|
| 106 |
+
|
| 107 |
+
raw = tokenizer.id_to_token(token_id) or f"token {token_id}"
|
| 108 |
+
label = html.escape(_token_label(tokenizer, token_id, mask_token_id))
|
| 109 |
+
title = html.escape(f"posición {position} · id {token_id} · {raw}", quote=True)
|
| 110 |
+
chips.append(
|
| 111 |
+
f'<span class="token-chip token-{state}" title="{title}">{label}</span>'
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
return (
|
| 115 |
+
'<section class="token-stage" aria-label="Estado actual de los tokens">'
|
| 116 |
+
'<div class="token-grid">'
|
| 117 |
+
+ "".join(chips)
|
| 118 |
+
+ "</div>"
|
| 119 |
+
'<div class="token-legend" aria-label="Leyenda">'
|
| 120 |
+
'<span><i class="legend-dot legend-prompt"></i>prompt</span>'
|
| 121 |
+
'<span><i class="legend-dot legend-new"></i>recién revelado</span>'
|
| 122 |
+
'<span><i class="legend-dot legend-mask"></i>máscara</span>'
|
| 123 |
+
"</div></section>"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class PlaygroundEngine:
|
| 128 |
+
"""Own one loaded model and serialize interactive generations."""
|
| 129 |
+
|
| 130 |
+
def __init__(self, model: DiffusionTransformer, tokenizer_path: str | Path) -> None:
|
| 131 |
+
self.model = model.eval()
|
| 132 |
+
self.tokenizer_path = Path(tokenizer_path)
|
| 133 |
+
self.tokenizer = load_tokenizer(self.tokenizer_path)
|
| 134 |
+
self._lock = threading.Lock()
|
| 135 |
+
self._validate_tokenizer()
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def device(self) -> torch.device:
|
| 139 |
+
return next(self.model.parameters()).device
|
| 140 |
+
|
| 141 |
+
def _validate_tokenizer(self) -> None:
|
| 142 |
+
tokenizer_hash = hashlib.sha256(self.tokenizer_path.read_bytes()).hexdigest()
|
| 143 |
+
model_hash = getattr(self.model, "tokenizer_sha256", None)
|
| 144 |
+
if model_hash is not None and tokenizer_hash != model_hash:
|
| 145 |
+
raise ValueError("El tokenizer no coincide con el usado para entrenar el checkpoint.")
|
| 146 |
+
if self.tokenizer.get_vocab_size(with_added_tokens=True) != self.model.config.vocab_size:
|
| 147 |
+
raise ValueError("El vocabulario del tokenizer no coincide con el checkpoint.")
|
| 148 |
+
if special_token_id(self.tokenizer, "mask") != self.model.config.mask_token_id:
|
| 149 |
+
raise ValueError("El id de [MASK] no coincide con el checkpoint.")
|
| 150 |
+
|
| 151 |
+
def info(self) -> dict[str, str | int | bool]:
|
| 152 |
+
return {
|
| 153 |
+
"parameters": self.model.num_parameters,
|
| 154 |
+
"parameters_human": format_parameter_count(self.model.num_parameters),
|
| 155 |
+
"device": str(self.device),
|
| 156 |
+
"context": self.model.config.max_seq_len,
|
| 157 |
+
"vocab_size": self.model.config.vocab_size,
|
| 158 |
+
"tokenizer": self.tokenizer_path.name,
|
| 159 |
+
"mps_fp64_fallback": self.device.type == "mps",
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
def _prepare(self, settings: GenerationSettings) -> tuple[Tensor, int, tuple[int, ...]]:
|
| 163 |
+
prompt_ids = (
|
| 164 |
+
self.tokenizer.encode(settings.prompt, add_special_tokens=False).ids
|
| 165 |
+
if settings.prompt
|
| 166 |
+
else []
|
| 167 |
+
)
|
| 168 |
+
role_ids = special_token_ids(self.tokenizer)
|
| 169 |
+
reserved_ids = set(role_ids.values())
|
| 170 |
+
encountered = reserved_ids.intersection(prompt_ids)
|
| 171 |
+
if encountered:
|
| 172 |
+
raise ValueError(
|
| 173 |
+
"El prompt contiene tokens especiales reservados. Escribí texto normal sin "
|
| 174 |
+
"los sentinels internos del modelo."
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
total_length = len(prompt_ids) + settings.generation_length
|
| 178 |
+
if total_length > self.model.config.max_seq_len:
|
| 179 |
+
available = self.model.config.max_seq_len - len(prompt_ids)
|
| 180 |
+
raise ValueError(
|
| 181 |
+
f"El prompt usa {len(prompt_ids)} tokens y deja {max(0, available)} para generar; "
|
| 182 |
+
f"solicitaste {settings.generation_length}."
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
input_ids = torch.full(
|
| 186 |
+
(1, total_length),
|
| 187 |
+
self.model.config.mask_token_id,
|
| 188 |
+
dtype=torch.long,
|
| 189 |
+
device=self.device,
|
| 190 |
+
)
|
| 191 |
+
if prompt_ids:
|
| 192 |
+
input_ids[0, : len(prompt_ids)] = torch.tensor(prompt_ids, device=self.device)
|
| 193 |
+
|
| 194 |
+
blocked = tuple(role_ids[role] for role in ("pad", "unk", "bos", "mask"))
|
| 195 |
+
return input_ids, len(prompt_ids), blocked
|
| 196 |
+
|
| 197 |
+
def _decode_final(self, token_ids: list[int], prompt_tokens: int) -> str:
|
| 198 |
+
eos_id = special_token_id(self.tokenizer, "eos")
|
| 199 |
+
if eos_id in token_ids[prompt_tokens:]:
|
| 200 |
+
token_ids = token_ids[: token_ids.index(eos_id, prompt_tokens)]
|
| 201 |
+
return self.tokenizer.decode(token_ids, skip_special_tokens=True)
|
| 202 |
+
|
| 203 |
+
def stream(self, settings: GenerationSettings):
|
| 204 |
+
"""Yield one UI update per reverse-diffusion pass."""
|
| 205 |
+
|
| 206 |
+
input_ids, prompt_tokens, blocked = self._prepare(settings)
|
| 207 |
+
with self._lock:
|
| 208 |
+
_seed_generation(self.device, settings.seed)
|
| 209 |
+
started = time.perf_counter()
|
| 210 |
+
# Measures engine work per pass; consumer time between yields is excluded.
|
| 211 |
+
pass_started = started
|
| 212 |
+
previous_ids: list[int] | None = None
|
| 213 |
+
for state in iterative_unmask_steps(
|
| 214 |
+
self.model,
|
| 215 |
+
input_ids,
|
| 216 |
+
self.model.config.mask_token_id,
|
| 217 |
+
steps=settings.steps,
|
| 218 |
+
temperature=settings.temperature,
|
| 219 |
+
strategy=settings.strategy,
|
| 220 |
+
blocked_token_ids=blocked,
|
| 221 |
+
):
|
| 222 |
+
token_ids = state.tokens[0].detach().cpu().tolist()
|
| 223 |
+
partial_text = self.tokenizer.decode(token_ids, skip_special_tokens=False)
|
| 224 |
+
final_text = (
|
| 225 |
+
self._decode_final(token_ids, prompt_tokens)
|
| 226 |
+
if state.masked_remaining == 0
|
| 227 |
+
else ""
|
| 228 |
+
)
|
| 229 |
+
token_html = render_token_grid(
|
| 230 |
+
self.tokenizer,
|
| 231 |
+
token_ids,
|
| 232 |
+
mask_token_id=self.model.config.mask_token_id,
|
| 233 |
+
prompt_tokens=prompt_tokens,
|
| 234 |
+
previous_token_ids=previous_ids,
|
| 235 |
+
)
|
| 236 |
+
if state.masked_remaining == 0:
|
| 237 |
+
_synchronize(self.device)
|
| 238 |
+
now = time.perf_counter()
|
| 239 |
+
yield PlaygroundUpdate(
|
| 240 |
+
state=state,
|
| 241 |
+
prompt_tokens=prompt_tokens,
|
| 242 |
+
partial_text=partial_text,
|
| 243 |
+
final_text=final_text,
|
| 244 |
+
token_html=token_html,
|
| 245 |
+
elapsed_seconds=now - started,
|
| 246 |
+
step_seconds=now - pass_started,
|
| 247 |
+
)
|
| 248 |
+
previous_ids = token_ids
|
| 249 |
+
pass_started = time.perf_counter()
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
PLAYGROUND_CSS = """
|
| 253 |
+
:root {
|
| 254 |
+
--playground-accent: #7c3aed;
|
| 255 |
+
--playground-accent-soft: rgba(124, 58, 237, 0.14);
|
| 256 |
+
--playground-teal: #0f766e;
|
| 257 |
+
--playground-border: rgba(100, 116, 139, 0.22);
|
| 258 |
+
}
|
| 259 |
+
.gradio-container { max-width: 1440px !important; }
|
| 260 |
+
.playground-header {
|
| 261 |
+
padding: 22px 24px; border: 1px solid var(--playground-border); border-radius: 18px;
|
| 262 |
+
background: linear-gradient(135deg, rgba(124,58,237,.10), rgba(15,118,110,.06));
|
| 263 |
+
box-shadow: 0 1px 2px rgba(30,41,59,.05), 0 14px 34px rgba(71,85,105,.08);
|
| 264 |
+
}
|
| 265 |
+
.playground-header h1 { margin: 0; font-size: clamp(1.6rem, 3vw, 2.4rem); text-wrap: balance; }
|
| 266 |
+
.playground-header p { margin: 8px 0 0; color: var(--body-text-color-subdued); text-wrap: pretty; }
|
| 267 |
+
.model-strip { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
| 268 |
+
.model-pill {
|
| 269 |
+
padding: 7px 10px; border-radius: 999px; border: 1px solid var(--playground-border);
|
| 270 |
+
background: var(--block-background-fill); font-variant-numeric: tabular-nums; font-size: .82rem;
|
| 271 |
+
}
|
| 272 |
+
.control-panel, .output-panel {
|
| 273 |
+
border: 1px solid var(--playground-border) !important; border-radius: 18px !important;
|
| 274 |
+
padding: 16px !important; box-shadow: 0 1px 2px rgba(30,41,59,.04), 0 10px 28px rgba(71,85,105,.06);
|
| 275 |
+
}
|
| 276 |
+
.token-stage { min-height: 220px; display: flex; flex-direction: column; justify-content: space-between; }
|
| 277 |
+
.token-grid { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 7px; padding: 8px 2px 18px; }
|
| 278 |
+
.token-chip {
|
| 279 |
+
display: inline-flex; min-height: 32px; align-items: center; padding: 5px 8px; border-radius: 9px;
|
| 280 |
+
border: 1px solid transparent; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 281 |
+
font-size: .82rem; font-variant-numeric: tabular-nums; transition: transform 160ms ease-out, opacity 180ms ease-out;
|
| 282 |
+
}
|
| 283 |
+
.token-chip:hover { transform: translateY(-1px); }
|
| 284 |
+
.token-prompt { color: #075985; background: rgba(14,165,233,.12); border-color: rgba(14,165,233,.26); }
|
| 285 |
+
.token-revealed { background: rgba(15,118,110,.10); border-color: rgba(15,118,110,.18); }
|
| 286 |
+
.token-new { color: #5b21b6; background: var(--playground-accent-soft); border-color: rgba(124,58,237,.35); }
|
| 287 |
+
.token-mask { color: var(--body-text-color-subdued); background: rgba(100,116,139,.08); border: 1px dashed rgba(100,116,139,.30); opacity: .7; }
|
| 288 |
+
.token-legend { display: flex; flex-wrap: wrap; gap: 14px; color: var(--body-text-color-subdued); font-size: .78rem; }
|
| 289 |
+
.token-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
| 290 |
+
.legend-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
|
| 291 |
+
.legend-prompt { background: #0ea5e9; } .legend-new { background: #7c3aed; } .legend-mask { background: #94a3b8; }
|
| 292 |
+
#generate-button, #stop-button { min-height: 44px; transition: transform 150ms ease-out; }
|
| 293 |
+
#generate-button:active, #stop-button:active { transform: scale(.98); }
|
| 294 |
+
@media (prefers-reduced-motion: reduce) { .token-chip, #generate-button, #stop-button { transition: none; } }
|
| 295 |
+
"""
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def _model_header(engine: PlaygroundEngine) -> str:
|
| 299 |
+
info = engine.info()
|
| 300 |
+
warning = (
|
| 301 |
+
" · MPS usa CPU para Gumbel fp64 cuando temperatura > 0"
|
| 302 |
+
if info["mps_fp64_fallback"]
|
| 303 |
+
else ""
|
| 304 |
+
)
|
| 305 |
+
return (
|
| 306 |
+
'<header class="playground-header">'
|
| 307 |
+
"<h1>Mini Diffusion LM Playground</h1>"
|
| 308 |
+
"<p>Observá cómo el modelo transforma máscaras en texto usando contexto bidireccional."
|
| 309 |
+
f"{html.escape(warning)}</p>"
|
| 310 |
+
'<div class="model-strip">'
|
| 311 |
+
f'<span class="model-pill">{info["parameters_human"]} parámetros</span>'
|
| 312 |
+
f'<span class="model-pill">{html.escape(str(info["device"]))}</span>'
|
| 313 |
+
f'<span class="model-pill">contexto {info["context"]}</span>'
|
| 314 |
+
f'<span class="model-pill">vocabulario {info["vocab_size"]}</span>'
|
| 315 |
+
f'<span class="model-pill">{html.escape(str(info["tokenizer"]))}</span>'
|
| 316 |
+
"</div></header>"
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def build_playground(engine: PlaygroundEngine):
|
| 321 |
+
"""Build a Gradio Blocks app without importing Gradio for base-package users."""
|
| 322 |
+
|
| 323 |
+
try:
|
| 324 |
+
import gradio as gr
|
| 325 |
+
except ImportError as exc: # pragma: no cover - exercised by CLI environments.
|
| 326 |
+
raise RuntimeError(
|
| 327 |
+
'Falta Gradio. Instalalo con: pip install -e ".[playground]"'
|
| 328 |
+
) from exc
|
| 329 |
+
|
| 330 |
+
max_context = engine.model.config.max_seq_len
|
| 331 |
+
default_length = min(64, max_context)
|
| 332 |
+
theme = gr.themes.Soft(primary_hue="violet", secondary_hue="teal", neutral_hue="slate")
|
| 333 |
+
with gr.Blocks(
|
| 334 |
+
title="Mini Diffusion LM Playground",
|
| 335 |
+
analytics_enabled=False,
|
| 336 |
+
fill_width=True,
|
| 337 |
+
) as demo:
|
| 338 |
+
gr.HTML(_model_header(engine))
|
| 339 |
+
with gr.Row():
|
| 340 |
+
with gr.Column(scale=4, elem_classes="control-panel"):
|
| 341 |
+
gr.Markdown("## Configuración")
|
| 342 |
+
prompt = gr.Textbox(
|
| 343 |
+
label="Prompt (opcional)",
|
| 344 |
+
placeholder="Ej.: Once upon a time…",
|
| 345 |
+
lines=6,
|
| 346 |
+
max_lines=10,
|
| 347 |
+
)
|
| 348 |
+
with gr.Row():
|
| 349 |
+
length = gr.Slider(
|
| 350 |
+
minimum=1,
|
| 351 |
+
maximum=max_context,
|
| 352 |
+
value=default_length,
|
| 353 |
+
step=1,
|
| 354 |
+
label="Tokens a generar",
|
| 355 |
+
)
|
| 356 |
+
steps = gr.Slider(
|
| 357 |
+
minimum=1,
|
| 358 |
+
maximum=256,
|
| 359 |
+
value=min(64, max_context),
|
| 360 |
+
step=1,
|
| 361 |
+
label="Pasos de difusión",
|
| 362 |
+
)
|
| 363 |
+
with gr.Accordion("Opciones avanzadas", open=False):
|
| 364 |
+
strategy = gr.Radio(
|
| 365 |
+
choices=[
|
| 366 |
+
("Confianza · revela los tokens más seguros", "confidence"),
|
| 367 |
+
("Ancestral · transición probabilística", "ancestral"),
|
| 368 |
+
],
|
| 369 |
+
value="confidence",
|
| 370 |
+
label="Estrategia",
|
| 371 |
+
)
|
| 372 |
+
temperature = gr.Slider(
|
| 373 |
+
minimum=0.0,
|
| 374 |
+
maximum=2.0,
|
| 375 |
+
value=0.8,
|
| 376 |
+
step=0.05,
|
| 377 |
+
label="Temperatura",
|
| 378 |
+
)
|
| 379 |
+
seed = gr.Number(
|
| 380 |
+
value=0,
|
| 381 |
+
precision=0,
|
| 382 |
+
minimum=0,
|
| 383 |
+
maximum=2**31 - 1,
|
| 384 |
+
label='Semilla (0 = aleatoria en cada generación)',
|
| 385 |
+
)
|
| 386 |
+
with gr.Row():
|
| 387 |
+
generate_button = gr.Button(
|
| 388 |
+
"Generar",
|
| 389 |
+
variant="primary",
|
| 390 |
+
elem_id="generate-button",
|
| 391 |
+
)
|
| 392 |
+
stop_button = gr.Button(
|
| 393 |
+
"Detener",
|
| 394 |
+
variant="stop",
|
| 395 |
+
elem_id="stop-button",
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
with gr.Column(scale=7, elem_classes="output-panel"):
|
| 399 |
+
status = gr.Markdown(
|
| 400 |
+
"### Listo\nConfigurá una muestra y presioná **Generar**."
|
| 401 |
+
)
|
| 402 |
+
token_view = gr.HTML(
|
| 403 |
+
'<div class="token-stage"><p>Los tokens aparecerán acá.</p></div>'
|
| 404 |
+
)
|
| 405 |
+
output = gr.Textbox(
|
| 406 |
+
label="Texto actual",
|
| 407 |
+
lines=7,
|
| 408 |
+
interactive=False,
|
| 409 |
+
)
|
| 410 |
+
metrics = gr.Markdown("`Esperando una generación`", elem_classes="metrics")
|
| 411 |
+
|
| 412 |
+
def stream_generation(
|
| 413 |
+
prompt_value: str,
|
| 414 |
+
length_value: float,
|
| 415 |
+
steps_value: float,
|
| 416 |
+
strategy_value: str,
|
| 417 |
+
temperature_value: float,
|
| 418 |
+
seed_value: float,
|
| 419 |
+
):
|
| 420 |
+
clicked = time.perf_counter()
|
| 421 |
+
try:
|
| 422 |
+
resolved_seed = int(seed_value) or secrets.randbelow(2**31 - 1) + 1
|
| 423 |
+
settings = GenerationSettings(
|
| 424 |
+
prompt=prompt_value or "",
|
| 425 |
+
generation_length=int(length_value),
|
| 426 |
+
steps=int(steps_value),
|
| 427 |
+
strategy=strategy_value, # type: ignore[arg-type]
|
| 428 |
+
temperature=float(temperature_value),
|
| 429 |
+
seed=resolved_seed,
|
| 430 |
+
)
|
| 431 |
+
for update in engine.stream(settings):
|
| 432 |
+
generated = settings.generation_length - update.state.masked_remaining
|
| 433 |
+
percent = 100.0 * generated / settings.generation_length
|
| 434 |
+
total_seconds = time.perf_counter() - clicked
|
| 435 |
+
average_step = update.elapsed_seconds / max(1, update.state.step)
|
| 436 |
+
tokens_per_second = (
|
| 437 |
+
generated / update.elapsed_seconds if update.elapsed_seconds > 0 else 0.0
|
| 438 |
+
)
|
| 439 |
+
if update.state.masked_remaining == 0:
|
| 440 |
+
status_text = (
|
| 441 |
+
f'### Completado en {total_seconds:.2f} s\n'
|
| 442 |
+
f'{generated} tokens en {update.state.step} pasos · '
|
| 443 |
+
f'{tokens_per_second:.1f} tok/s · '
|
| 444 |
+
f'{average_step * 1000:.0f} ms/paso promedio'
|
| 445 |
+
)
|
| 446 |
+
else:
|
| 447 |
+
status_text = (
|
| 448 |
+
f"### Paso {update.state.step}/{update.state.total_steps}\n"
|
| 449 |
+
f"{update.state.masked_remaining} máscaras restantes · "
|
| 450 |
+
f"{percent:.0f}% revelado"
|
| 451 |
+
)
|
| 452 |
+
visible_text = (
|
| 453 |
+
update.final_text
|
| 454 |
+
if update.state.masked_remaining == 0
|
| 455 |
+
else update.partial_text
|
| 456 |
+
)
|
| 457 |
+
metrics_text = (
|
| 458 |
+
f'`{total_seconds:.2f} s desde el clic` · '
|
| 459 |
+
f'`modelo {update.elapsed_seconds:.2f} s` · '
|
| 460 |
+
f'`paso {update.step_seconds * 1000:.0f} ms` · '
|
| 461 |
+
f'`prom. {average_step * 1000:.0f} ms/paso` · '
|
| 462 |
+
f'`{tokens_per_second:.1f} tok/s` · '
|
| 463 |
+
f'`{update.prompt_tokens} tokens de prompt` · '
|
| 464 |
+
f'`seed {settings.seed}`'
|
| 465 |
+
)
|
| 466 |
+
yield update.token_html, status_text, visible_text, metrics_text
|
| 467 |
+
except ValueError as exc:
|
| 468 |
+
raise gr.Error(str(exc)) from exc
|
| 469 |
+
|
| 470 |
+
generation_event = generate_button.click(
|
| 471 |
+
fn=stream_generation,
|
| 472 |
+
inputs=[prompt, length, steps, strategy, temperature, seed],
|
| 473 |
+
outputs=[token_view, status, output, metrics],
|
| 474 |
+
show_progress="minimal",
|
| 475 |
+
scroll_to_output=False,
|
| 476 |
+
concurrency_limit=1,
|
| 477 |
+
concurrency_id="diffusion-model",
|
| 478 |
+
trigger_mode="once",
|
| 479 |
+
stream_every=0.1,
|
| 480 |
+
api_visibility="private",
|
| 481 |
+
)
|
| 482 |
+
stop_button.click(
|
| 483 |
+
fn=lambda: "### Generación detenida",
|
| 484 |
+
outputs=status,
|
| 485 |
+
cancels=[generation_event],
|
| 486 |
+
queue=False,
|
| 487 |
+
api_visibility="private",
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
demo = demo.queue(max_size=8, default_concurrency_limit=1)
|
| 491 |
+
# Gradio 6 moved presentation arguments from Blocks() to launch().
|
| 492 |
+
demo._mini_diffusion_theme = theme
|
| 493 |
+
return demo
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 497 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 498 |
+
parser.add_argument("--checkpoint", type=Path, required=True, help="checkpoint local confiable")
|
| 499 |
+
parser.add_argument("--tokenizer", type=Path, required=True, help="tokenizer usado al entrenar")
|
| 500 |
+
parser.add_argument("--device", default="auto", help="auto, cpu, mps, cuda…")
|
| 501 |
+
parser.add_argument("--host", default="127.0.0.1")
|
| 502 |
+
parser.add_argument("--port", type=int, default=7860)
|
| 503 |
+
parser.add_argument("--no-browser", action="store_true", help="no abrir el navegador")
|
| 504 |
+
return parser
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def main() -> None:
|
| 508 |
+
args = _build_parser().parse_args()
|
| 509 |
+
if not args.checkpoint.is_file():
|
| 510 |
+
raise SystemExit(f"Checkpoint inexistente: {args.checkpoint}")
|
| 511 |
+
if not args.tokenizer.is_file():
|
| 512 |
+
raise SystemExit(f"Tokenizer inexistente: {args.tokenizer}")
|
| 513 |
+
if not 1 <= args.port <= 65535:
|
| 514 |
+
raise SystemExit("El puerto debe estar entre 1 y 65535")
|
| 515 |
+
|
| 516 |
+
device = resolve_device(args.device)
|
| 517 |
+
print(f"Cargando {args.checkpoint} en {device}…")
|
| 518 |
+
model = load_model(args.checkpoint, device)
|
| 519 |
+
engine = PlaygroundEngine(model, args.tokenizer)
|
| 520 |
+
demo = build_playground(engine)
|
| 521 |
+
print(f"Playground: http://{args.host}:{args.port}")
|
| 522 |
+
print("Usá únicamente checkpoints locales confiables.")
|
| 523 |
+
demo.launch(
|
| 524 |
+
server_name=args.host,
|
| 525 |
+
server_port=args.port,
|
| 526 |
+
inbrowser=not args.no_browser,
|
| 527 |
+
share=False,
|
| 528 |
+
show_error=True,
|
| 529 |
+
strict_cors=True,
|
| 530 |
+
max_threads=4,
|
| 531 |
+
footer_links=[],
|
| 532 |
+
enable_monitoring=False,
|
| 533 |
+
ssr_mode=False,
|
| 534 |
+
pwa=False,
|
| 535 |
+
theme=demo._mini_diffusion_theme,
|
| 536 |
+
css=PLAYGROUND_CSS,
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
if __name__ == "__main__":
|
| 541 |
+
main()
|
src/diffusion_lm/quality.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Programmatic generation quality: prose damage, and a reward over claim recaps.
|
| 2 |
+
|
| 3 |
+
The weights below encode a priority order rather than a tuned optimum: being right first,
|
| 4 |
+
not lying second, readable prose third, and brevity last and only relative to how much the
|
| 5 |
+
problem actually required. They are meant to be calibrated against judged samples before
|
| 6 |
+
driving any policy optimisation, since a reward this cheap is also cheap to game.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
WORD = re.compile(r"[A-Za-z']+")
|
| 14 |
+
|
| 15 |
+
# Ordering, not tuning. Lying is charged PER ITEM rather than as a fraction of the record:
|
| 16 |
+
# a reader can work around clumsy prose or a missing line, but a fabricated plate or a
|
| 17 |
+
# superseded amount silently corrupts the report, so one of them has to outweigh several
|
| 18 |
+
# recalled fields. Verbosity is charged per fact carried, so a harder case may run longer.
|
| 19 |
+
WEIGHTS = {
|
| 20 |
+
'recall': 1.0,
|
| 21 |
+
'stale': -0.35,
|
| 22 |
+
'invented': -0.35,
|
| 23 |
+
'duplication': -0.2,
|
| 24 |
+
'verbosity': -0.15,
|
| 25 |
+
}
|
| 26 |
+
# Thinking tokens per recalled fact treated as free before the verbosity term bites.
|
| 27 |
+
VERBOSITY_BUDGET = 40.0
|
| 28 |
+
# Lie counts saturate here so a wholly hallucinated report does not dominate the gradient.
|
| 29 |
+
MAX_LIES = 3.0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def prose_damage(text: str) -> dict[str, float]:
|
| 33 |
+
"""Adjacent duplicate rate and lexical diversity, the parallel-sampling signatures."""
|
| 34 |
+
|
| 35 |
+
words = [word.lower() for word in WORD.findall(text)]
|
| 36 |
+
pairs = max(1, len(words) - 1)
|
| 37 |
+
duplicates = sum(a == b for a, b in zip(words, words[1:]))
|
| 38 |
+
return {
|
| 39 |
+
'duplicate_rate': duplicates / pairs,
|
| 40 |
+
'lexical_diversity': len(set(words)) / max(1, len(words)),
|
| 41 |
+
'words': len(words),
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def reward(report_score: dict[str, object], think_tokens: int, answer_text: str) -> dict:
|
| 46 |
+
"""Combine fact fidelity, prose damage and verbosity into one scalar plus its parts.
|
| 47 |
+
|
| 48 |
+
``report_score`` comes from :func:`diffusion_lm.claims.score_report`. Penalties are
|
| 49 |
+
fractions of the fact count so a long claim and a short one stay comparable.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
recalled = max(1, int(report_score['recalled']))
|
| 53 |
+
damage = prose_damage(answer_text)
|
| 54 |
+
per_fact = think_tokens / recalled
|
| 55 |
+
parts = {
|
| 56 |
+
'recall': float(report_score['recall']),
|
| 57 |
+
'stale': min(MAX_LIES, len(report_score['stale_kept'])),
|
| 58 |
+
'invented': min(MAX_LIES, len(report_score['invented'])),
|
| 59 |
+
'duplication': min(1.0, damage['duplicate_rate'] * 20.0),
|
| 60 |
+
'verbosity': max(0.0, per_fact - VERBOSITY_BUDGET) / VERBOSITY_BUDGET,
|
| 61 |
+
}
|
| 62 |
+
total = sum(WEIGHTS[name] * value for name, value in parts.items())
|
| 63 |
+
return {
|
| 64 |
+
'total': total,
|
| 65 |
+
'parts': parts,
|
| 66 |
+
'think_tokens_per_fact': per_fact,
|
| 67 |
+
'duplicate_rate': damage['duplicate_rate'],
|
| 68 |
+
'lexical_diversity': damage['lexical_diversity'],
|
| 69 |
+
}
|
src/diffusion_lm/reasoning.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenMathInstruct-2 preparation and packed datasets for reasoning experiments.
|
| 2 |
+
|
| 3 |
+
Each example is rendered into two layouts over one shared tokenizer:
|
| 4 |
+
|
| 5 |
+
- ``flat``: problem <think> steps... </think> answer <eos>
|
| 6 |
+
- ``slotted``: problem <think> [fixed-size thought slots, one step each, <tpad>-filled]
|
| 7 |
+
[terminal slot: </think> <tpad>...] answer <eos>
|
| 8 |
+
|
| 9 |
+
The slotted layout gives every thought a fixed-geometry block so a block-diffusion
|
| 10 |
+
model can denoise one thought at a time; the flat layout serves the autoregressive
|
| 11 |
+
and pure-diffusion baselines on identical example sets.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import hashlib
|
| 18 |
+
import json
|
| 19 |
+
import re
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any, Iterator
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
from torch import Tensor
|
| 27 |
+
from torch.utils.data import Dataset
|
| 28 |
+
|
| 29 |
+
from diffusion_lm.tokenizer import (
|
| 30 |
+
RoleAwareTokenizer,
|
| 31 |
+
load_tokenizer,
|
| 32 |
+
special_token_ids,
|
| 33 |
+
train_tokenizer_from_iterator,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
REASONING_PACKED_FORMAT = 'mini-diffusion-lm-reasoning-packed-v1'
|
| 37 |
+
|
| 38 |
+
_REASONING_NAMESPACE = 'mdlm-r-8f4e2a6c'
|
| 39 |
+
THINK_TOKEN = f'<|{_REASONING_NAMESPACE}:think|>'
|
| 40 |
+
END_THINK_TOKEN = f'<|{_REASONING_NAMESPACE}:endthink|>'
|
| 41 |
+
THOUGHT_PAD_TOKEN = f'<|{_REASONING_NAMESPACE}:tpad|>'
|
| 42 |
+
REASONING_SPECIAL_TOKENS = (THINK_TOKEN, END_THINK_TOKEN, THOUGHT_PAD_TOKEN)
|
| 43 |
+
|
| 44 |
+
# Block sizes a thought may occupy in the adaptive layout. The model emits one
|
| 45 |
+
# ``<szN>`` control token per thought to choose the block length; the full menu is
|
| 46 |
+
# baked into the tokenizer so the vocabulary stays stable across experiments even
|
| 47 |
+
# when a run activates only a subset of the sizes.
|
| 48 |
+
BLOCK_SIZES = (32, 64, 128, 256, 512)
|
| 49 |
+
SIZE_TOKENS = tuple(f'<|{_REASONING_NAMESPACE}:sz{size}|>' for size in BLOCK_SIZES)
|
| 50 |
+
ADAPTIVE_SPECIAL_TOKENS = REASONING_SPECIAL_TOKENS + SIZE_TOKENS
|
| 51 |
+
|
| 52 |
+
# Native spellings honored when the tokenizer comes from a pretrained backbone whose vocabulary
|
| 53 |
+
# already carries trained think delimiters (e.g. Qwen3). The namespaced sentinel wins when both
|
| 54 |
+
# spellings exist so project tokenizers keep their exact historical ids.
|
| 55 |
+
_NATIVE_REASONING_TOKENS = {'think': '<think>', 'end_think': '</think>'}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def size_token_ids(
|
| 59 |
+
tokenizer: RoleAwareTokenizer, sizes: tuple[int, ...] | None = None
|
| 60 |
+
) -> dict[int, int]:
|
| 61 |
+
"""Map block sizes to ``<szN>`` control-token ids.
|
| 62 |
+
|
| 63 |
+
Without ``sizes`` the full menu is scanned and absent tokens are skipped, so tokenizers
|
| 64 |
+
trained before a menu extension keep resolving. Explicitly requested sizes must exist.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
ids: dict[int, int] = {}
|
| 68 |
+
for size, token in zip(BLOCK_SIZES, SIZE_TOKENS):
|
| 69 |
+
if sizes is not None and size not in sizes:
|
| 70 |
+
continue
|
| 71 |
+
token_id = tokenizer.token_to_id(token)
|
| 72 |
+
if token_id is None:
|
| 73 |
+
if sizes is None:
|
| 74 |
+
continue
|
| 75 |
+
raise ValueError(f'tokenizer is missing the size token {token!r}')
|
| 76 |
+
ids[size] = token_id
|
| 77 |
+
if sizes is not None and (missing := set(sizes) - set(ids)):
|
| 78 |
+
raise ValueError(f'sizes {sorted(missing)} are not in the block-size menu {BLOCK_SIZES}')
|
| 79 |
+
if not ids:
|
| 80 |
+
raise ValueError('tokenizer carries no <szN> size tokens')
|
| 81 |
+
return ids
|
| 82 |
+
|
| 83 |
+
_BOILERPLATE_STEP = re.compile(
|
| 84 |
+
r"^let'?s\s+(solve|answer|tackle)\s+(the\s+)?(new\s+)?(question|problem)\b.*$",
|
| 85 |
+
re.IGNORECASE,
|
| 86 |
+
)
|
| 87 |
+
_MATH_SPAN = re.compile(r'\$[^$]*\$')
|
| 88 |
+
_SENTENCE_BREAK = re.compile(r'(?<=[.!?])\s+(?=[A-Z\d(])')
|
| 89 |
+
_BOXED = re.compile(r'\\boxed\{([^{}]*)\}')
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def reasoning_token_ids(tokenizer: RoleAwareTokenizer) -> dict[str, int]:
|
| 93 |
+
"""Resolve the reasoning control-token ids, failing on non-reasoning tokenizers.
|
| 94 |
+
|
| 95 |
+
Pretrained-backbone tokenizers resolve ``think``/``end_think`` through their native
|
| 96 |
+
trained spellings when the namespaced sentinels are absent.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
ids: dict[str, int] = {}
|
| 100 |
+
for name, token in (
|
| 101 |
+
('think', THINK_TOKEN),
|
| 102 |
+
('end_think', END_THINK_TOKEN),
|
| 103 |
+
('thought_pad', THOUGHT_PAD_TOKEN),
|
| 104 |
+
):
|
| 105 |
+
token_id = tokenizer.token_to_id(token)
|
| 106 |
+
if token_id is None and name in _NATIVE_REASONING_TOKENS:
|
| 107 |
+
token_id = tokenizer.token_to_id(_NATIVE_REASONING_TOKENS[name])
|
| 108 |
+
if token_id is None:
|
| 109 |
+
raise ValueError(f'tokenizer is missing the reasoning token {token!r}')
|
| 110 |
+
ids[name] = token_id
|
| 111 |
+
return ids
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def extract_boxed_answer(text: str) -> str | None:
|
| 115 |
+
matches = _BOXED.findall(text)
|
| 116 |
+
if not matches:
|
| 117 |
+
return None
|
| 118 |
+
return matches[-1].strip()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _normalize_answer(answer: str) -> str:
|
| 122 |
+
cleaned = answer.strip().replace(',', '').replace('$', '').rstrip('.')
|
| 123 |
+
try:
|
| 124 |
+
value = float(cleaned)
|
| 125 |
+
except ValueError:
|
| 126 |
+
return cleaned
|
| 127 |
+
return str(int(value)) if value == int(value) else str(value)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def split_solution_steps(solution: str) -> list[str]:
|
| 131 |
+
"""Split a natural-language CoT solution into sentence-level steps."""
|
| 132 |
+
|
| 133 |
+
protected: list[str] = []
|
| 134 |
+
|
| 135 |
+
def _protect(match: re.Match[str]) -> str:
|
| 136 |
+
protected.append(match.group(0))
|
| 137 |
+
return f'\x00{len(protected) - 1}\x00'
|
| 138 |
+
|
| 139 |
+
masked = _MATH_SPAN.sub(_protect, solution)
|
| 140 |
+
parts: list[str] = []
|
| 141 |
+
for line in masked.splitlines():
|
| 142 |
+
line = line.strip()
|
| 143 |
+
if not line:
|
| 144 |
+
continue
|
| 145 |
+
parts.extend(piece.strip() for piece in _SENTENCE_BREAK.split(line) if piece.strip())
|
| 146 |
+
|
| 147 |
+
def _restore(text: str) -> str:
|
| 148 |
+
return re.sub(r'\x00(\d+)\x00', lambda m: protected[int(m.group(1))], text)
|
| 149 |
+
|
| 150 |
+
steps = [_restore(part) for part in parts]
|
| 151 |
+
return [step for step in steps if not _BOILERPLATE_STEP.match(step)]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@dataclass(frozen=True)
|
| 155 |
+
class ReasoningExample:
|
| 156 |
+
problem: str
|
| 157 |
+
steps: tuple[str, ...]
|
| 158 |
+
answer: str
|
| 159 |
+
expected_answer: str
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def parse_example(row: dict[str, Any]) -> ReasoningExample | None:
|
| 163 |
+
"""Clean one dataset row into problem, thought steps, and an answer sentence."""
|
| 164 |
+
|
| 165 |
+
problem = ' '.join(str(row['problem']).split())
|
| 166 |
+
expected = _normalize_answer(str(row['expected_answer']))
|
| 167 |
+
steps = split_solution_steps(str(row['generated_solution']))
|
| 168 |
+
answer_index = None
|
| 169 |
+
for index in range(len(steps) - 1, -1, -1):
|
| 170 |
+
if _BOXED.search(steps[index]):
|
| 171 |
+
answer_index = index
|
| 172 |
+
break
|
| 173 |
+
if answer_index is None or not problem or not expected:
|
| 174 |
+
return None
|
| 175 |
+
answer = steps[answer_index]
|
| 176 |
+
thoughts = tuple(steps[:answer_index])
|
| 177 |
+
if not thoughts:
|
| 178 |
+
return None
|
| 179 |
+
boxed = extract_boxed_answer(answer)
|
| 180 |
+
if boxed is None or _normalize_answer(boxed) != expected:
|
| 181 |
+
return None
|
| 182 |
+
return ReasoningExample(problem, thoughts, answer, expected)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@dataclass(frozen=True)
|
| 186 |
+
class LayoutSpec:
|
| 187 |
+
seq_len: int
|
| 188 |
+
block: int
|
| 189 |
+
max_slots: int
|
| 190 |
+
sizes: tuple[int, ...] = ()
|
| 191 |
+
|
| 192 |
+
def __post_init__(self) -> None:
|
| 193 |
+
if self.seq_len <= 0 or self.block <= 1 or self.max_slots <= 1:
|
| 194 |
+
raise ValueError('seq_len, block, and max_slots must be meaningfully positive')
|
| 195 |
+
sizes = tuple(self.sizes)
|
| 196 |
+
if sizes:
|
| 197 |
+
if any(size not in BLOCK_SIZES for size in sizes):
|
| 198 |
+
raise ValueError(f'adaptive sizes must be drawn from {BLOCK_SIZES}')
|
| 199 |
+
if list(sizes) != sorted(set(sizes)):
|
| 200 |
+
raise ValueError('adaptive sizes must be unique and ascending')
|
| 201 |
+
object.__setattr__(self, 'sizes', sizes)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@dataclass(frozen=True)
|
| 205 |
+
class EncodedExample:
|
| 206 |
+
flat: np.ndarray
|
| 207 |
+
flat_regions: np.ndarray
|
| 208 |
+
slotted: np.ndarray
|
| 209 |
+
slotted_regions: np.ndarray
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@dataclass(frozen=True)
|
| 213 |
+
class EncodedAdaptive:
|
| 214 |
+
tokens: np.ndarray
|
| 215 |
+
regions: np.ndarray
|
| 216 |
+
block_sizes: tuple[int, ...]
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class ExampleEncoder:
|
| 220 |
+
"""Render cleaned examples into the flat and slotted token layouts."""
|
| 221 |
+
|
| 222 |
+
def __init__(self, tokenizer: RoleAwareTokenizer, spec: LayoutSpec) -> None:
|
| 223 |
+
self.tokenizer = tokenizer
|
| 224 |
+
self.spec = spec
|
| 225 |
+
roles = special_token_ids(tokenizer)
|
| 226 |
+
reasoning = reasoning_token_ids(tokenizer)
|
| 227 |
+
self.pad_id = roles['pad']
|
| 228 |
+
self.eos_id = roles['eos']
|
| 229 |
+
self.think_id = reasoning['think']
|
| 230 |
+
self.end_think_id = reasoning['end_think']
|
| 231 |
+
self.tpad_id = reasoning['thought_pad']
|
| 232 |
+
self.size_ids = size_token_ids(tokenizer, spec.sizes) if spec.sizes else {}
|
| 233 |
+
vocab_size = tokenizer.get_vocab_size(with_added_tokens=True)
|
| 234 |
+
self.token_dtype = np.dtype(
|
| 235 |
+
'uint16' if vocab_size <= np.iinfo(np.uint16).max else 'uint32'
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
def _encode(self, text: str) -> list[int]:
|
| 239 |
+
return self.tokenizer.encode(text, add_special_tokens=False).ids
|
| 240 |
+
|
| 241 |
+
def encode_adaptive(self, example: ReasoningExample) -> EncodedAdaptive | None:
|
| 242 |
+
"""Render an example into the adaptive layout with per-thought size tokens.
|
| 243 |
+
|
| 244 |
+
Each thought is placed in the smallest active block that fits it; thoughts
|
| 245 |
+
longer than the largest block continue across consecutive maximum blocks.
|
| 246 |
+
The ``<szN>`` control token precedes every block, giving the model an
|
| 247 |
+
autoregressive target for the block-length decision, and the remainder of
|
| 248 |
+
each block is filled with ``<tpad>``.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
spec = self.spec
|
| 252 |
+
sizes = spec.sizes
|
| 253 |
+
if not sizes:
|
| 254 |
+
raise ValueError('encode_adaptive requires a LayoutSpec with sizes set')
|
| 255 |
+
max_size = sizes[-1]
|
| 256 |
+
problem_ids = self._encode(example.problem)
|
| 257 |
+
answer_ids = self._encode(example.answer)
|
| 258 |
+
if not answer_ids or not problem_ids:
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
blocks: list[tuple[int, list[int]]] = []
|
| 262 |
+
for step in example.steps:
|
| 263 |
+
ids = self._encode(step)
|
| 264 |
+
for start in range(0, len(ids), max_size):
|
| 265 |
+
chunk = ids[start : start + max_size]
|
| 266 |
+
size = next(candidate for candidate in sizes if candidate >= len(chunk))
|
| 267 |
+
blocks.append((size, chunk))
|
| 268 |
+
# An empty chain is a legitimate example: it teaches the controller to answer without
|
| 269 |
+
# thinking, which a conversation needs on every trivial turn.
|
| 270 |
+
if len(blocks) > spec.max_slots:
|
| 271 |
+
return None
|
| 272 |
+
|
| 273 |
+
tokens = [*problem_ids, self.think_id]
|
| 274 |
+
for size, chunk in blocks:
|
| 275 |
+
tokens.append(self.size_ids[size])
|
| 276 |
+
tokens.extend(chunk)
|
| 277 |
+
tokens.extend([self.tpad_id] * (size - len(chunk)))
|
| 278 |
+
tokens.append(self.end_think_id)
|
| 279 |
+
answer_start = len(tokens)
|
| 280 |
+
tokens.extend(answer_ids)
|
| 281 |
+
tokens.append(self.eos_id)
|
| 282 |
+
answer_end = len(tokens)
|
| 283 |
+
if answer_end > spec.seq_len:
|
| 284 |
+
return None
|
| 285 |
+
tokens.extend([self.pad_id] * (spec.seq_len - len(tokens)))
|
| 286 |
+
|
| 287 |
+
regions = np.asarray(
|
| 288 |
+
[len(problem_ids), len(blocks), answer_start, answer_end], dtype=np.int32
|
| 289 |
+
)
|
| 290 |
+
return EncodedAdaptive(
|
| 291 |
+
tokens=np.asarray(tokens, dtype=self.token_dtype),
|
| 292 |
+
regions=regions,
|
| 293 |
+
block_sizes=tuple(size for size, _ in blocks),
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
def encode_example(self, example: ReasoningExample) -> EncodedExample | None:
|
| 297 |
+
spec = self.spec
|
| 298 |
+
problem_ids = self._encode(example.problem)
|
| 299 |
+
step_ids = [self._encode(step) for step in example.steps]
|
| 300 |
+
answer_ids = self._encode(example.answer)
|
| 301 |
+
|
| 302 |
+
if not answer_ids or not problem_ids:
|
| 303 |
+
return None
|
| 304 |
+
|
| 305 |
+
# A thought that exceeds one slot continues into the next, so long steps
|
| 306 |
+
# cost extra slots instead of dropping the example.
|
| 307 |
+
chunked: list[list[int]] = []
|
| 308 |
+
for ids in step_ids:
|
| 309 |
+
for start in range(0, len(ids), spec.block):
|
| 310 |
+
chunked.append(ids[start : start + spec.block])
|
| 311 |
+
step_ids = chunked
|
| 312 |
+
if len(step_ids) > spec.max_slots - 1:
|
| 313 |
+
return None
|
| 314 |
+
|
| 315 |
+
slots = len(step_ids) + 1
|
| 316 |
+
slotted_len = len(problem_ids) + 1 + slots * spec.block + len(answer_ids) + 1
|
| 317 |
+
if slotted_len > spec.seq_len:
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
slotted = [*problem_ids, self.think_id]
|
| 321 |
+
for ids in step_ids:
|
| 322 |
+
slotted.extend(ids)
|
| 323 |
+
slotted.extend([self.tpad_id] * (spec.block - len(ids)))
|
| 324 |
+
slotted.append(self.end_think_id)
|
| 325 |
+
slotted.extend([self.tpad_id] * (spec.block - 1))
|
| 326 |
+
answer_start_slotted = len(slotted)
|
| 327 |
+
slotted.extend(answer_ids)
|
| 328 |
+
slotted.append(self.eos_id)
|
| 329 |
+
answer_end_slotted = len(slotted)
|
| 330 |
+
slotted.extend([self.pad_id] * (spec.seq_len - len(slotted)))
|
| 331 |
+
|
| 332 |
+
flat_think_ids = self._encode(' '.join(example.steps))
|
| 333 |
+
flat = [*problem_ids, self.think_id, *flat_think_ids, self.end_think_id]
|
| 334 |
+
answer_start_flat = len(flat)
|
| 335 |
+
flat.extend(answer_ids)
|
| 336 |
+
flat.append(self.eos_id)
|
| 337 |
+
answer_end_flat = len(flat)
|
| 338 |
+
if answer_end_flat > spec.seq_len:
|
| 339 |
+
return None
|
| 340 |
+
flat.extend([self.pad_id] * (spec.seq_len - len(flat)))
|
| 341 |
+
|
| 342 |
+
def _regions(problem_len: int, n_slots: int, start: int, end: int) -> np.ndarray:
|
| 343 |
+
return np.asarray([problem_len, n_slots, start, end], dtype=np.int32)
|
| 344 |
+
|
| 345 |
+
return EncodedExample(
|
| 346 |
+
flat=np.asarray(flat, dtype=self.token_dtype),
|
| 347 |
+
flat_regions=_regions(len(problem_ids), 0, answer_start_flat, answer_end_flat),
|
| 348 |
+
slotted=np.asarray(slotted, dtype=self.token_dtype),
|
| 349 |
+
slotted_regions=_regions(
|
| 350 |
+
len(problem_ids), slots, answer_start_slotted, answer_end_slotted
|
| 351 |
+
),
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def regions_path(token_path: str | Path) -> Path:
|
| 356 |
+
path = Path(token_path)
|
| 357 |
+
return path.with_suffix('.regions.npy')
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class ReasoningTokenDataset(Dataset[tuple[Tensor, Tensor]]):
|
| 361 |
+
"""Fixed-length reasoning sequences with per-example region annotations.
|
| 362 |
+
|
| 363 |
+
Regions hold ``[problem_len, n_slots, answer_start, answer_end]``. ``n_slots``
|
| 364 |
+
is zero for the flat layout and counts thought slots including the terminal
|
| 365 |
+
``</think>`` slot for the slotted layout.
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
def __init__(self, path: str | Path) -> None:
|
| 369 |
+
self.path = Path(path)
|
| 370 |
+
metadata_path = self.path.with_suffix(self.path.suffix + '.json')
|
| 371 |
+
if not metadata_path.is_file():
|
| 372 |
+
raise FileNotFoundError(f'reasoning metadata not found: {metadata_path}')
|
| 373 |
+
with metadata_path.open('r', encoding='utf-8') as handle:
|
| 374 |
+
self.metadata = json.load(handle)
|
| 375 |
+
if self.metadata.get('format') != REASONING_PACKED_FORMAT:
|
| 376 |
+
raise ValueError(f'unsupported reasoning data format in {metadata_path}')
|
| 377 |
+
self.seq_len = int(self.metadata['seq_len'])
|
| 378 |
+
count = int(self.metadata['example_count'])
|
| 379 |
+
# Datasets packed before the dtype key existed are always uint16.
|
| 380 |
+
self.token_dtype = np.dtype(self.metadata.get('dtype', 'uint16'))
|
| 381 |
+
self._tokens = np.memmap(
|
| 382 |
+
self.path, mode='r', dtype=self.token_dtype, shape=(count, self.seq_len)
|
| 383 |
+
)
|
| 384 |
+
self._regions = np.load(regions_path(self.path))
|
| 385 |
+
if self._regions.shape != (count, 4):
|
| 386 |
+
raise ValueError(f'regions shape {self._regions.shape} does not match example count')
|
| 387 |
+
|
| 388 |
+
def __len__(self) -> int:
|
| 389 |
+
return int(self._tokens.shape[0])
|
| 390 |
+
|
| 391 |
+
def __getitem__(self, index: int) -> tuple[Tensor, Tensor]:
|
| 392 |
+
tokens = torch.from_numpy(np.asarray(self._tokens[index], dtype=np.int64))
|
| 393 |
+
regions = torch.from_numpy(np.asarray(self._regions[index], dtype=np.int64))
|
| 394 |
+
return tokens, regions
|
| 395 |
+
|
| 396 |
+
def __getstate__(self) -> dict[str, Any]:
|
| 397 |
+
state = self.__dict__.copy()
|
| 398 |
+
state['_tokens'] = None
|
| 399 |
+
return state
|
| 400 |
+
|
| 401 |
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
| 402 |
+
self.__dict__.update(state)
|
| 403 |
+
count = int(self.metadata['example_count'])
|
| 404 |
+
self._tokens = np.memmap(
|
| 405 |
+
self.path, mode='r', dtype=self.token_dtype, shape=(count, self.seq_len)
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _write_packed(
|
| 410 |
+
path: Path,
|
| 411 |
+
tokens: np.ndarray,
|
| 412 |
+
regions: np.ndarray,
|
| 413 |
+
*,
|
| 414 |
+
layout: str,
|
| 415 |
+
spec: LayoutSpec,
|
| 416 |
+
tokenizer_path: Path,
|
| 417 |
+
tokenizer: RoleAwareTokenizer,
|
| 418 |
+
extra_metadata: dict[str, Any] | None = None,
|
| 419 |
+
) -> None:
|
| 420 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 421 |
+
tokens.tofile(path)
|
| 422 |
+
np.save(regions_path(path), regions)
|
| 423 |
+
if tokens.dtype not in (np.dtype('uint16'), np.dtype('uint32')):
|
| 424 |
+
raise ValueError(f'packed reasoning tokens must be uint16 or uint32, got {tokens.dtype}')
|
| 425 |
+
metadata = {
|
| 426 |
+
'format': REASONING_PACKED_FORMAT,
|
| 427 |
+
'layout': layout,
|
| 428 |
+
'seq_len': spec.seq_len,
|
| 429 |
+
'block': spec.block,
|
| 430 |
+
'max_slots': spec.max_slots,
|
| 431 |
+
'dtype': tokens.dtype.name,
|
| 432 |
+
'example_count': int(tokens.shape[0]),
|
| 433 |
+
'vocab_size': tokenizer.get_vocab_size(with_added_tokens=True),
|
| 434 |
+
'special_token_ids': special_token_ids(tokenizer),
|
| 435 |
+
'reasoning_token_ids': reasoning_token_ids(tokenizer),
|
| 436 |
+
'tokenizer_sha256': hashlib.sha256(tokenizer_path.read_bytes()).hexdigest(),
|
| 437 |
+
}
|
| 438 |
+
if extra_metadata:
|
| 439 |
+
metadata.update(extra_metadata)
|
| 440 |
+
metadata_path = path.with_suffix(path.suffix + '.json')
|
| 441 |
+
with metadata_path.open('w', encoding='utf-8') as handle:
|
| 442 |
+
json.dump(metadata, handle, indent=2)
|
| 443 |
+
handle.write('\n')
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def _iter_rows(parquet_paths: list[Path], sources: set[str]) -> Iterator[dict[str, Any]]:
|
| 447 |
+
import pyarrow.parquet as pq
|
| 448 |
+
|
| 449 |
+
for path in parquet_paths:
|
| 450 |
+
table = pq.read_table(
|
| 451 |
+
path, columns=['problem', 'generated_solution', 'expected_answer', 'problem_source']
|
| 452 |
+
)
|
| 453 |
+
for row in table.to_pylist():
|
| 454 |
+
if row['problem_source'] in sources:
|
| 455 |
+
yield row
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def _is_validation(problem: str, val_fraction: float) -> bool:
|
| 459 |
+
digest = hashlib.sha256(problem.encode('utf-8')).digest()
|
| 460 |
+
return int.from_bytes(digest[:4], 'big') / 2**32 < val_fraction
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def prepare(args: argparse.Namespace) -> None:
|
| 464 |
+
sources = set(args.sources.split(','))
|
| 465 |
+
parquet_paths = [Path(path) for path in args.parquet]
|
| 466 |
+
spec = LayoutSpec(seq_len=args.seq_len, block=args.block, max_slots=args.max_slots)
|
| 467 |
+
output_dir = Path(args.output_dir)
|
| 468 |
+
tokenizer_path = Path(args.tokenizer)
|
| 469 |
+
|
| 470 |
+
examples: list[ReasoningExample] = []
|
| 471 |
+
dropped_parse = 0
|
| 472 |
+
seen: set[str] = set()
|
| 473 |
+
for row in _iter_rows(parquet_paths, sources):
|
| 474 |
+
example = parse_example(row)
|
| 475 |
+
if example is None:
|
| 476 |
+
dropped_parse += 1
|
| 477 |
+
continue
|
| 478 |
+
key = hashlib.sha256(
|
| 479 |
+
(example.problem + '\x1f' + ' '.join(example.steps)).encode('utf-8')
|
| 480 |
+
).hexdigest()
|
| 481 |
+
if key in seen:
|
| 482 |
+
continue
|
| 483 |
+
seen.add(key)
|
| 484 |
+
examples.append(example)
|
| 485 |
+
print(f'parsed {len(examples):,} unique examples ({dropped_parse:,} dropped at parse)')
|
| 486 |
+
|
| 487 |
+
if tokenizer_path.is_file():
|
| 488 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 489 |
+
reasoning_token_ids(tokenizer)
|
| 490 |
+
print(f'reusing tokenizer {tokenizer_path}')
|
| 491 |
+
else:
|
| 492 |
+
def _texts() -> Iterator[str]:
|
| 493 |
+
for example in examples:
|
| 494 |
+
yield f'{example.problem}\n{" ".join(example.steps)}\n{example.answer}'
|
| 495 |
+
|
| 496 |
+
tokenizer = train_tokenizer_from_iterator(
|
| 497 |
+
_texts(),
|
| 498 |
+
tokenizer_path,
|
| 499 |
+
vocab_size=args.vocab_size,
|
| 500 |
+
min_frequency=4,
|
| 501 |
+
length=len(examples),
|
| 502 |
+
extra_special_tokens=REASONING_SPECIAL_TOKENS,
|
| 503 |
+
)
|
| 504 |
+
print(f'trained tokenizer {tokenizer_path}')
|
| 505 |
+
|
| 506 |
+
encoder = ExampleEncoder(tokenizer, spec)
|
| 507 |
+
split: dict[str, dict[str, list[np.ndarray]]] = {
|
| 508 |
+
'train': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
|
| 509 |
+
'validation': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
|
| 510 |
+
}
|
| 511 |
+
val_problems: list[dict[str, str]] = []
|
| 512 |
+
dropped_encode = 0
|
| 513 |
+
for example in examples:
|
| 514 |
+
encoded = encoder.encode_example(example)
|
| 515 |
+
if encoded is None:
|
| 516 |
+
dropped_encode += 1
|
| 517 |
+
continue
|
| 518 |
+
is_val = _is_validation(example.problem, args.val_fraction)
|
| 519 |
+
bucket = split['validation' if is_val else 'train']
|
| 520 |
+
bucket['flat'].append(encoded.flat)
|
| 521 |
+
bucket['flat_regions'].append(encoded.flat_regions)
|
| 522 |
+
bucket['slotted'].append(encoded.slotted)
|
| 523 |
+
bucket['slotted_regions'].append(encoded.slotted_regions)
|
| 524 |
+
if is_val:
|
| 525 |
+
val_problems.append(
|
| 526 |
+
{'problem': example.problem, 'expected_answer': example.expected_answer}
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
total = sum(len(bucket['flat']) for bucket in split.values())
|
| 530 |
+
print(f'encoded {total:,} examples ({dropped_encode:,} dropped at encode)')
|
| 531 |
+
for split_name, bucket in split.items():
|
| 532 |
+
if not bucket['flat']:
|
| 533 |
+
raise ValueError(f'no examples in the {split_name} split; adjust filters')
|
| 534 |
+
for layout in ('flat', 'slotted'):
|
| 535 |
+
_write_packed(
|
| 536 |
+
output_dir / f'{split_name}-{layout}.bin',
|
| 537 |
+
np.stack(bucket[layout]),
|
| 538 |
+
np.stack(bucket[f'{layout}_regions']),
|
| 539 |
+
layout=layout,
|
| 540 |
+
spec=spec,
|
| 541 |
+
tokenizer_path=tokenizer_path,
|
| 542 |
+
tokenizer=tokenizer,
|
| 543 |
+
)
|
| 544 |
+
print(f'{split_name}: {len(bucket["flat"]):,} examples -> {output_dir}')
|
| 545 |
+
|
| 546 |
+
problems_path = output_dir / 'validation-problems.jsonl'
|
| 547 |
+
with problems_path.open('w', encoding='utf-8') as handle:
|
| 548 |
+
for record in val_problems:
|
| 549 |
+
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 550 |
+
print(f'wrote {len(val_problems):,} validation problems to {problems_path}')
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 554 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 555 |
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
| 556 |
+
prepare_parser = subparsers.add_parser(
|
| 557 |
+
'prepare', help='filter, tokenize, and pack OpenMathInstruct-2 parquet shards'
|
| 558 |
+
)
|
| 559 |
+
prepare_parser.add_argument('--parquet', type=Path, nargs='+', required=True)
|
| 560 |
+
prepare_parser.add_argument('--output-dir', type=Path, required=True)
|
| 561 |
+
prepare_parser.add_argument('--tokenizer', type=Path, required=True)
|
| 562 |
+
prepare_parser.add_argument('--vocab-size', type=int, default=8192)
|
| 563 |
+
prepare_parser.add_argument('--seq-len', type=int, default=512)
|
| 564 |
+
prepare_parser.add_argument('--block', type=int, default=32)
|
| 565 |
+
prepare_parser.add_argument('--max-slots', type=int, default=11)
|
| 566 |
+
prepare_parser.add_argument('--val-fraction', type=float, default=0.02)
|
| 567 |
+
prepare_parser.add_argument('--sources', default='gsm8k,augmented_gsm8k')
|
| 568 |
+
return parser
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def main() -> None:
|
| 572 |
+
args = _build_parser().parse_args()
|
| 573 |
+
if args.command == 'prepare':
|
| 574 |
+
prepare(args)
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
if __name__ == '__main__':
|
| 578 |
+
main()
|
src/diffusion_lm/reasoning_playground.py
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Interactive playground for the reasoning variants.
|
| 2 |
+
|
| 3 |
+
Loads any ``mini-diffusion-lm-reasoning-inference-v1`` checkpoint and solves
|
| 4 |
+
user-provided problems with the sampler matching its training objective,
|
| 5 |
+
streaming thought slots as they are denoised and answer tokens as they are
|
| 6 |
+
decoded, together with per-phase wall-clock metrics.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import html
|
| 13 |
+
import json
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from diffusion_lm.claims import (
|
| 20 |
+
IM_END,
|
| 21 |
+
SYSTEM,
|
| 22 |
+
chat_prefix,
|
| 23 |
+
ledger_line,
|
| 24 |
+
ledger_notes,
|
| 25 |
+
merge_notes,
|
| 26 |
+
window_start,
|
| 27 |
+
)
|
| 28 |
+
from diffusion_lm.config import ModelConfig
|
| 29 |
+
from diffusion_lm.diffusion import iterative_unmask_steps, _sample_categorical
|
| 30 |
+
from diffusion_lm.flexattn import _FLEX_EAGER
|
| 31 |
+
from diffusion_lm.hybrid import (
|
| 32 |
+
_apply_repetition_penalty,
|
| 33 |
+
_apply_top_p,
|
| 34 |
+
_ar_predict_control,
|
| 35 |
+
_kv_cache_enabled,
|
| 36 |
+
adaptive_block_mask,
|
| 37 |
+
block_mask_from_boundaries,
|
| 38 |
+
block_size_curriculum,
|
| 39 |
+
prefix_causal_blocked,
|
| 40 |
+
slot_causal_blocked,
|
| 41 |
+
)
|
| 42 |
+
from diffusion_lm.model import build_denoiser
|
| 43 |
+
from diffusion_lm.reasoning import extract_boxed_answer, reasoning_token_ids, size_token_ids
|
| 44 |
+
from diffusion_lm.tokenizer import load_tokenizer, special_token_ids
|
| 45 |
+
from diffusion_lm.train import resolve_device
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _discover_checkpoints(root: Path) -> dict[str, Path]:
|
| 49 |
+
found: dict[str, Path] = {}
|
| 50 |
+
for path in sorted(root.glob('*/inference-latest.pt')):
|
| 51 |
+
found[path.parent.name] = path
|
| 52 |
+
return found
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class ReasoningEngine:
|
| 56 |
+
"""Load one reasoning checkpoint and stream problem solutions."""
|
| 57 |
+
|
| 58 |
+
def __init__(self, checkpoint_path: Path, tokenizer_path: Path, device: torch.device) -> None:
|
| 59 |
+
payload = torch.load(checkpoint_path, map_location='cpu', weights_only=False)
|
| 60 |
+
if payload.get('format') != 'mini-diffusion-lm-reasoning-inference-v1':
|
| 61 |
+
raise ValueError(f'unsupported checkpoint format in {checkpoint_path}')
|
| 62 |
+
self.config = payload['config']
|
| 63 |
+
self.objective = self.config['reasoning']['objective']
|
| 64 |
+
model_config = ModelConfig(**self.config['model'])
|
| 65 |
+
self.model = build_denoiser(model_config, load_pretrained=False)
|
| 66 |
+
self.model.load_state_dict(payload['model'])
|
| 67 |
+
dtype = (
|
| 68 |
+
torch.bfloat16
|
| 69 |
+
if model_config.backbone != 'project' and device.type == 'cuda'
|
| 70 |
+
else torch.float32
|
| 71 |
+
)
|
| 72 |
+
self.model.to(device=device, dtype=dtype).eval()
|
| 73 |
+
self.device = device
|
| 74 |
+
self.step = payload.get('step')
|
| 75 |
+
self.tokenizer = load_tokenizer(tokenizer_path)
|
| 76 |
+
self.roles = special_token_ids(self.tokenizer)
|
| 77 |
+
self.reasoning_ids = reasoning_token_ids(self.tokenizer)
|
| 78 |
+
self.adaptive = bool(self.config['reasoning'].get('adaptive'))
|
| 79 |
+
self.causal_prefix = bool(self.config['reasoning'].get('causal_prefix', False))
|
| 80 |
+
trained_sizes = tuple(self.config['reasoning'].get('sizes') or ()) or None
|
| 81 |
+
self.size_ids = size_token_ids(self.tokenizer, trained_sizes) if self.adaptive else {}
|
| 82 |
+
self.im_end_id = self.tokenizer.token_to_id(IM_END)
|
| 83 |
+
self.chat_ready = (
|
| 84 |
+
self.objective == 'hybrid' and self.adaptive and self.im_end_id is not None
|
| 85 |
+
)
|
| 86 |
+
self.block = 32
|
| 87 |
+
self.max_slots = 40
|
| 88 |
+
self.max_blocks = 48
|
| 89 |
+
self.log_lines: list[str] = []
|
| 90 |
+
|
| 91 |
+
def _decode(self, ids: list[int]) -> str:
|
| 92 |
+
return self.tokenizer.decode(ids, skip_special_tokens=True)
|
| 93 |
+
|
| 94 |
+
def _slot_texts(self, think_ids: list[int]) -> list[str]:
|
| 95 |
+
end_id = self.reasoning_ids['end_think']
|
| 96 |
+
pad_id = self.reasoning_ids['thought_pad']
|
| 97 |
+
slots = [
|
| 98 |
+
think_ids[start : start + self.block]
|
| 99 |
+
for start in range(0, len(think_ids), self.block)
|
| 100 |
+
]
|
| 101 |
+
texts = []
|
| 102 |
+
for slot in slots:
|
| 103 |
+
content = [t for t in slot if t not in (pad_id, end_id)]
|
| 104 |
+
texts.append(self._decode(content).strip())
|
| 105 |
+
return [text for text in texts if text]
|
| 106 |
+
|
| 107 |
+
def _block_contents(self, think_ids: list[int]) -> list[tuple[int, str]]:
|
| 108 |
+
"""Decode variable-size blocks by walking the ``<szN>`` tokens in the stream."""
|
| 109 |
+
|
| 110 |
+
size_by_id = {token_id: size for size, token_id in self.size_ids.items()}
|
| 111 |
+
end_id = self.reasoning_ids['end_think']
|
| 112 |
+
pad_id = self.reasoning_ids['thought_pad']
|
| 113 |
+
blocks = []
|
| 114 |
+
cut = 0
|
| 115 |
+
while cut < len(think_ids):
|
| 116 |
+
size = size_by_id.get(think_ids[cut])
|
| 117 |
+
if size is None:
|
| 118 |
+
cut += 1
|
| 119 |
+
continue
|
| 120 |
+
block = think_ids[cut + 1 : cut + 1 + size]
|
| 121 |
+
content = [t for t in block if t not in (pad_id, end_id)]
|
| 122 |
+
blocks.append((size, self._decode(content).strip()))
|
| 123 |
+
cut += 1 + size
|
| 124 |
+
return blocks
|
| 125 |
+
|
| 126 |
+
def _adaptive_block_texts(self, think_ids: list[int]) -> list[str]:
|
| 127 |
+
return [
|
| 128 |
+
f'({size}) {text}'.strip() for size, text in self._block_contents(think_ids)
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
def _token_glyph(self, token_id: int) -> str:
|
| 132 |
+
"""Render one in-flight token: masks and structural tokens get visible glyphs."""
|
| 133 |
+
|
| 134 |
+
if token_id == self.model.config.mask_token_id:
|
| 135 |
+
return '▒'
|
| 136 |
+
if token_id == self.reasoning_ids['thought_pad']:
|
| 137 |
+
return '·'
|
| 138 |
+
if token_id == self.reasoning_ids['end_think']:
|
| 139 |
+
return ' ⏹'
|
| 140 |
+
return self.tokenizer.decode([token_id], skip_special_tokens=False)
|
| 141 |
+
|
| 142 |
+
def _active_slot_text(self, slot_tokens: list[int]) -> str:
|
| 143 |
+
return ''.join(self._token_glyph(token) for token in slot_tokens)
|
| 144 |
+
|
| 145 |
+
@torch.no_grad()
|
| 146 |
+
def stream_solve(
|
| 147 |
+
self,
|
| 148 |
+
problem: str,
|
| 149 |
+
*,
|
| 150 |
+
temperature: float,
|
| 151 |
+
steps_per_block: int,
|
| 152 |
+
diffusion_steps: int,
|
| 153 |
+
max_answer_tokens: int = 200,
|
| 154 |
+
repetition_penalty: float = 1.0,
|
| 155 |
+
top_p: float = 1.0,
|
| 156 |
+
seed: int = 0,
|
| 157 |
+
strategy: str = 'ancestral',
|
| 158 |
+
step_delay: float = 0.0,
|
| 159 |
+
):
|
| 160 |
+
"""Yield ``(thoughts, answer, status)`` snapshots while solving.
|
| 161 |
+
|
| 162 |
+
``step_delay`` throttles each denoising step (and each AR token) so the
|
| 163 |
+
reveal order is watchable in real time.
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
generator = torch.Generator(device=self.device.type)
|
| 167 |
+
generator.manual_seed(seed if seed else int(time.time_ns() % 2**31))
|
| 168 |
+
prompt_ids = self.tokenizer.encode(problem.strip(), add_special_tokens=False).ids
|
| 169 |
+
mask_id = self.model.config.mask_token_id
|
| 170 |
+
think_id = self.reasoning_ids['think']
|
| 171 |
+
end_think_id = self.reasoning_ids['end_think']
|
| 172 |
+
eos_id = self.roles['eos']
|
| 173 |
+
|
| 174 |
+
if self.objective == 'hybrid' and self.adaptive:
|
| 175 |
+
yield from self._stream_adaptive(
|
| 176 |
+
prompt_ids,
|
| 177 |
+
stop_ids=(eos_id,),
|
| 178 |
+
temperature=temperature,
|
| 179 |
+
steps_per_block=steps_per_block,
|
| 180 |
+
max_answer_tokens=max_answer_tokens,
|
| 181 |
+
repetition_penalty=repetition_penalty,
|
| 182 |
+
top_p=top_p,
|
| 183 |
+
strategy=strategy,
|
| 184 |
+
step_delay=step_delay,
|
| 185 |
+
generator=generator,
|
| 186 |
+
)
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
if self.objective == 'block_diffusion':
|
| 190 |
+
# Plain-text continuation under the conversion geometry: each new block is
|
| 191 |
+
# fully masked and denoised bidirectionally over its causal past, with block
|
| 192 |
+
# sizes drawn from the same curriculum weights the checkpoint trained under.
|
| 193 |
+
reasoning_cfg = self.config['reasoning']
|
| 194 |
+
sizes = tuple(reasoning_cfg.get('sizes') or (self.block,))
|
| 195 |
+
weights = block_size_curriculum(
|
| 196 |
+
self.step,
|
| 197 |
+
n_sizes=len(sizes),
|
| 198 |
+
curriculum_steps=int(reasoning_cfg.get('curriculum_steps') or 0),
|
| 199 |
+
).to(self.device)
|
| 200 |
+
size_tensor = torch.tensor(sizes, device=self.device)
|
| 201 |
+
budget = min(384, self.model.config.max_seq_len - len(prompt_ids) - 1)
|
| 202 |
+
self.log_lines = [
|
| 203 |
+
f'prompt_tokens={len(prompt_ids)} temp={temperature} '
|
| 204 |
+
f'steps/block={steps_per_block} sizes={list(sizes)} '
|
| 205 |
+
f'weights={[round(w, 3) for w in weights.tolist()]} '
|
| 206 |
+
f'strategy={strategy} flex_eager={_FLEX_EAGER}'
|
| 207 |
+
]
|
| 208 |
+
sequence = list(prompt_ids)
|
| 209 |
+
prompt_len = len(prompt_ids)
|
| 210 |
+
boundary_starts: list[int] = []
|
| 211 |
+
block_texts: list[str] = []
|
| 212 |
+
generated: list[int] = []
|
| 213 |
+
compute = 0.0
|
| 214 |
+
finished = False
|
| 215 |
+
while not finished and len(generated) < budget:
|
| 216 |
+
drawn = int(
|
| 217 |
+
size_tensor[torch.multinomial(weights, 1, generator=generator)].item()
|
| 218 |
+
)
|
| 219 |
+
block = min(drawn, budget - len(generated))
|
| 220 |
+
prefix_len = len(sequence)
|
| 221 |
+
seq_len = prefix_len + block
|
| 222 |
+
boundary_starts.append(prefix_len)
|
| 223 |
+
boundary = torch.zeros(1, seq_len, dtype=torch.bool, device=self.device)
|
| 224 |
+
boundary[0, 0] = True
|
| 225 |
+
for start in boundary_starts:
|
| 226 |
+
boundary[0, start] = True
|
| 227 |
+
blocked = block_mask_from_boundaries(
|
| 228 |
+
boundary,
|
| 229 |
+
torch.tensor([prompt_len], device=self.device),
|
| 230 |
+
torch.tensor([seq_len], device=self.device),
|
| 231 |
+
causal_prefix=self.causal_prefix,
|
| 232 |
+
)
|
| 233 |
+
current = torch.tensor(
|
| 234 |
+
[sequence + [mask_id] * block], dtype=torch.long, device=self.device
|
| 235 |
+
)
|
| 236 |
+
block_ids: list[int] = []
|
| 237 |
+
tick = time.perf_counter()
|
| 238 |
+
for state in iterative_unmask_steps(
|
| 239 |
+
self.model,
|
| 240 |
+
current,
|
| 241 |
+
mask_id,
|
| 242 |
+
steps=steps_per_block,
|
| 243 |
+
temperature=temperature,
|
| 244 |
+
strategy=strategy,
|
| 245 |
+
blocked_token_ids=(self.roles['pad'],),
|
| 246 |
+
attn_mask=blocked,
|
| 247 |
+
generator=generator,
|
| 248 |
+
):
|
| 249 |
+
compute += time.perf_counter() - tick
|
| 250 |
+
block_ids = [int(t) for t in state.tokens[0, prefix_len:]]
|
| 251 |
+
if step_delay or state.masked_remaining == 0:
|
| 252 |
+
yield (
|
| 253 |
+
[*block_texts, self._active_slot_text(block_ids)],
|
| 254 |
+
self._decode(generated),
|
| 255 |
+
f'bloque {len(block_texts) + 1} ({block} tok) · '
|
| 256 |
+
f'denoising {state.step}/{state.total_steps}',
|
| 257 |
+
)
|
| 258 |
+
if step_delay and state.masked_remaining:
|
| 259 |
+
time.sleep(step_delay)
|
| 260 |
+
tick = time.perf_counter()
|
| 261 |
+
if eos_id in block_ids:
|
| 262 |
+
block_ids = block_ids[: block_ids.index(eos_id)]
|
| 263 |
+
finished = True
|
| 264 |
+
sequence.extend(block_ids)
|
| 265 |
+
generated.extend(block_ids)
|
| 266 |
+
block_texts.append(f'({block}) {self._decode(block_ids).strip()}'.strip())
|
| 267 |
+
self.log_lines.append(
|
| 268 |
+
f'block {len(block_texts)}: size={block} · {compute:.2f}s cum'
|
| 269 |
+
)
|
| 270 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 271 |
+
self.log_lines.append(
|
| 272 |
+
f'TOTAL: {len(generated)} tok · blocks={len(block_texts)} · '
|
| 273 |
+
f'{compute:.2f}s ({rate:.0f} tok/s)'
|
| 274 |
+
)
|
| 275 |
+
yield block_texts, self._decode(generated), (
|
| 276 |
+
f'listo · {len(block_texts)} bloques · {len(generated)} tok en '
|
| 277 |
+
f'{compute:.2f}s ({rate:.0f} tok/s) '
|
| 278 |
+
f'(checkpoint CPT: continúa texto, no piensa)'
|
| 279 |
+
)
|
| 280 |
+
return
|
| 281 |
+
|
| 282 |
+
if self.objective == 'hybrid':
|
| 283 |
+
self.log_lines = [
|
| 284 |
+
f'prompt_tokens={len(prompt_ids)} temp={temperature} '
|
| 285 |
+
f'steps/slot={steps_per_block} rep_penalty={repetition_penalty} '
|
| 286 |
+
f'top_p={top_p} max_slots={self.max_slots} strategy={strategy} '
|
| 287 |
+
f'flex_eager={_FLEX_EAGER}'
|
| 288 |
+
]
|
| 289 |
+
sequence = [*prompt_ids, think_id]
|
| 290 |
+
think_ids: list[int] = []
|
| 291 |
+
think_compute = 0.0
|
| 292 |
+
slot_budget = self.model.config.max_seq_len - self.block - 8
|
| 293 |
+
problem_tensor = torch.tensor([len(prompt_ids)], device=self.device)
|
| 294 |
+
for slot_index in range(self.max_slots):
|
| 295 |
+
if len(sequence) > slot_budget:
|
| 296 |
+
break
|
| 297 |
+
slot_wall_start = time.perf_counter()
|
| 298 |
+
window = torch.tensor(
|
| 299 |
+
[sequence + [mask_id] * self.block], dtype=torch.long, device=self.device
|
| 300 |
+
)
|
| 301 |
+
blocked = slot_causal_blocked(
|
| 302 |
+
problem_tensor,
|
| 303 |
+
torch.tensor([slot_index + 1], device=self.device),
|
| 304 |
+
self.block,
|
| 305 |
+
window.shape[1],
|
| 306 |
+
)
|
| 307 |
+
slot: list[int] = []
|
| 308 |
+
tick = time.perf_counter()
|
| 309 |
+
for state in iterative_unmask_steps(
|
| 310 |
+
self.model,
|
| 311 |
+
window,
|
| 312 |
+
mask_id,
|
| 313 |
+
steps=steps_per_block,
|
| 314 |
+
temperature=temperature,
|
| 315 |
+
strategy=strategy,
|
| 316 |
+
attn_mask=blocked,
|
| 317 |
+
generator=generator,
|
| 318 |
+
):
|
| 319 |
+
think_compute += time.perf_counter() - tick
|
| 320 |
+
slot = [int(t) for t in state.tokens[0, len(sequence):]]
|
| 321 |
+
revealed = len(think_ids) + self.block - state.masked_remaining
|
| 322 |
+
rate = revealed / think_compute if think_compute > 0 else 0.0
|
| 323 |
+
yield (
|
| 324 |
+
self._slot_texts(think_ids)
|
| 325 |
+
+ [self._active_slot_text(slot)],
|
| 326 |
+
'',
|
| 327 |
+
f'denoising slot {slot_index + 1} · paso '
|
| 328 |
+
f'{state.step}/{state.total_steps} · {rate:.0f} tok/s',
|
| 329 |
+
)
|
| 330 |
+
if step_delay and state.step < state.total_steps:
|
| 331 |
+
time.sleep(step_delay)
|
| 332 |
+
tick = time.perf_counter()
|
| 333 |
+
sequence.extend(slot)
|
| 334 |
+
think_ids.extend(slot)
|
| 335 |
+
slot_content = [t for t in slot if t not in (
|
| 336 |
+
self.reasoning_ids['thought_pad'], end_think_id
|
| 337 |
+
)]
|
| 338 |
+
self.log_lines.append(
|
| 339 |
+
f' T{slot_index + 1}: {len(slot_content)} tok · '
|
| 340 |
+
f'{time.perf_counter() - slot_wall_start:.2f}s wall'
|
| 341 |
+
+ (' · </think>' if end_think_id in slot else '')
|
| 342 |
+
)
|
| 343 |
+
if end_think_id in slot:
|
| 344 |
+
break
|
| 345 |
+
else:
|
| 346 |
+
sequence.extend(
|
| 347 |
+
[end_think_id] + [self.reasoning_ids['thought_pad']] * (self.block - 1)
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
answer_ids: list[int] = []
|
| 351 |
+
prefix_len = len(sequence)
|
| 352 |
+
answer_compute = 0.0
|
| 353 |
+
tick = time.perf_counter()
|
| 354 |
+
for _ in range(max_answer_tokens):
|
| 355 |
+
current = torch.tensor(
|
| 356 |
+
[sequence + answer_ids], dtype=torch.long, device=self.device
|
| 357 |
+
)
|
| 358 |
+
blocked = prefix_causal_blocked(
|
| 359 |
+
torch.tensor([prefix_len], device=self.device), current.shape[1]
|
| 360 |
+
)
|
| 361 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 362 |
+
output_positions[0, -1] = True
|
| 363 |
+
logits = self.model(
|
| 364 |
+
current, output_positions=output_positions, attn_mask=blocked
|
| 365 |
+
)
|
| 366 |
+
logits = _apply_repetition_penalty(logits, answer_ids, repetition_penalty)
|
| 367 |
+
logits = _apply_top_p(logits, top_p)
|
| 368 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 369 |
+
token_id = int(token.item())
|
| 370 |
+
answer_ids.append(token_id)
|
| 371 |
+
answer_compute += time.perf_counter() - tick
|
| 372 |
+
if step_delay or len(answer_ids) % 4 == 0 or token_id == eos_id:
|
| 373 |
+
rate = len(answer_ids) / answer_compute if answer_compute > 0 else 0.0
|
| 374 |
+
yield (
|
| 375 |
+
self._slot_texts(think_ids),
|
| 376 |
+
self._decode(answer_ids),
|
| 377 |
+
f'respondiendo (AR, token {len(answer_ids)}) · {rate:.0f} tok/s',
|
| 378 |
+
)
|
| 379 |
+
if step_delay and token_id != eos_id:
|
| 380 |
+
time.sleep(step_delay / 3)
|
| 381 |
+
if token_id == eos_id:
|
| 382 |
+
break
|
| 383 |
+
tick = time.perf_counter()
|
| 384 |
+
think_tokens = len(think_ids)
|
| 385 |
+
answer_tokens = len(answer_ids)
|
| 386 |
+
total_compute = think_compute + answer_compute
|
| 387 |
+
think_rate = think_tokens / think_compute if think_compute > 0 else 0.0
|
| 388 |
+
answer_rate = answer_tokens / answer_compute if answer_compute > 0 else 0.0
|
| 389 |
+
total_rate = (
|
| 390 |
+
(think_tokens + answer_tokens) / total_compute if total_compute > 0 else 0.0
|
| 391 |
+
)
|
| 392 |
+
status = (
|
| 393 |
+
f'listo · pensar: {think_tokens} tok en {think_compute:.2f}s '
|
| 394 |
+
f'({think_rate:.0f} tok/s) · responder: {answer_tokens} tok en '
|
| 395 |
+
f'{answer_compute:.2f}s ({answer_rate:.0f} tok/s) · total: '
|
| 396 |
+
f'{think_tokens + answer_tokens} tok en {total_compute:.2f}s '
|
| 397 |
+
f'({total_rate:.0f} tok/s)'
|
| 398 |
+
)
|
| 399 |
+
self.log_lines.append(
|
| 400 |
+
f' answer: {answer_tokens} tok · {answer_compute:.2f}s wall '
|
| 401 |
+
f'({answer_rate:.0f} tok/s) · terminated={end_think_id in think_ids}'
|
| 402 |
+
)
|
| 403 |
+
self.log_lines.append(
|
| 404 |
+
f'TOTAL: {think_tokens + answer_tokens} tok · {total_compute:.2f}s '
|
| 405 |
+
f'({total_rate:.0f} tok/s)'
|
| 406 |
+
)
|
| 407 |
+
yield self._slot_texts(think_ids), self._decode(answer_ids), status
|
| 408 |
+
return
|
| 409 |
+
|
| 410 |
+
if self.objective == 'lm':
|
| 411 |
+
# Plain causal continuation for pretraining checkpoints: no think token,
|
| 412 |
+
# no masking; the prompt is simply extended left to right.
|
| 413 |
+
sequence = list(prompt_ids)
|
| 414 |
+
generated: list[int] = []
|
| 415 |
+
prefix_len = len(sequence)
|
| 416 |
+
budget = self.model.config.max_seq_len - prefix_len - 1
|
| 417 |
+
compute = 0.0
|
| 418 |
+
tick = time.perf_counter()
|
| 419 |
+
for _ in range(min(budget, 384)):
|
| 420 |
+
current = torch.tensor(
|
| 421 |
+
[sequence + generated], dtype=torch.long, device=self.device
|
| 422 |
+
)
|
| 423 |
+
blocked = prefix_causal_blocked(
|
| 424 |
+
torch.tensor([prefix_len], device=self.device), current.shape[1]
|
| 425 |
+
)
|
| 426 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 427 |
+
output_positions[0, -1] = True
|
| 428 |
+
logits = self.model(
|
| 429 |
+
current, output_positions=output_positions, attn_mask=blocked
|
| 430 |
+
)
|
| 431 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 432 |
+
token_id = int(token.item())
|
| 433 |
+
generated.append(token_id)
|
| 434 |
+
compute += time.perf_counter() - tick
|
| 435 |
+
if step_delay or token_id == eos_id or len(generated) % 8 == 0:
|
| 436 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 437 |
+
yield (
|
| 438 |
+
[],
|
| 439 |
+
self._decode(generated),
|
| 440 |
+
f'continuando (token {len(generated)}) · {rate:.0f} tok/s',
|
| 441 |
+
)
|
| 442 |
+
if step_delay and token_id != eos_id:
|
| 443 |
+
time.sleep(step_delay / 3)
|
| 444 |
+
if token_id == eos_id:
|
| 445 |
+
break
|
| 446 |
+
tick = time.perf_counter()
|
| 447 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 448 |
+
yield [], self._decode(generated), (
|
| 449 |
+
f'listo · total: {len(generated)} tok en {compute:.2f}s ({rate:.0f} tok/s) '
|
| 450 |
+
f'(checkpoint base: continua texto, no piensa)'
|
| 451 |
+
)
|
| 452 |
+
return
|
| 453 |
+
|
| 454 |
+
if self.objective == 'ar':
|
| 455 |
+
sequence = [*prompt_ids, think_id]
|
| 456 |
+
generated: list[int] = []
|
| 457 |
+
prefix_len = len(sequence)
|
| 458 |
+
budget = self.model.config.max_seq_len - prefix_len - 1
|
| 459 |
+
compute = 0.0
|
| 460 |
+
tick = time.perf_counter()
|
| 461 |
+
for _ in range(min(budget, 384)):
|
| 462 |
+
current = torch.tensor(
|
| 463 |
+
[sequence + generated], dtype=torch.long, device=self.device
|
| 464 |
+
)
|
| 465 |
+
blocked = prefix_causal_blocked(
|
| 466 |
+
torch.tensor([prefix_len], device=self.device), current.shape[1]
|
| 467 |
+
)
|
| 468 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 469 |
+
output_positions[0, -1] = True
|
| 470 |
+
logits = self.model(
|
| 471 |
+
current, output_positions=output_positions, attn_mask=blocked
|
| 472 |
+
)
|
| 473 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 474 |
+
token_id = int(token.item())
|
| 475 |
+
generated.append(token_id)
|
| 476 |
+
compute += time.perf_counter() - tick
|
| 477 |
+
if step_delay or token_id == eos_id or len(generated) % 8 == 0:
|
| 478 |
+
thoughts, answer = self._split_flat(generated)
|
| 479 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 480 |
+
yield thoughts, answer, (
|
| 481 |
+
f'generando (token {len(generated)}) · {rate:.0f} tok/s'
|
| 482 |
+
)
|
| 483 |
+
if step_delay and token_id != eos_id:
|
| 484 |
+
time.sleep(step_delay / 3)
|
| 485 |
+
if token_id == eos_id:
|
| 486 |
+
break
|
| 487 |
+
tick = time.perf_counter()
|
| 488 |
+
thoughts, answer = self._split_flat(generated)
|
| 489 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 490 |
+
yield thoughts, answer, (
|
| 491 |
+
f'listo · total: {len(generated)} tok en {compute:.2f}s ({rate:.0f} tok/s)'
|
| 492 |
+
)
|
| 493 |
+
return
|
| 494 |
+
|
| 495 |
+
budget = min(352, self.model.config.max_seq_len - len(prompt_ids) - 1)
|
| 496 |
+
sequence_tensor = torch.tensor(
|
| 497 |
+
[[*prompt_ids, think_id] + [mask_id] * budget], dtype=torch.long, device=self.device
|
| 498 |
+
)
|
| 499 |
+
final_ids: list[int] = []
|
| 500 |
+
compute = 0.0
|
| 501 |
+
tick = time.perf_counter()
|
| 502 |
+
for state in iterative_unmask_steps(
|
| 503 |
+
self.model,
|
| 504 |
+
sequence_tensor,
|
| 505 |
+
mask_id,
|
| 506 |
+
steps=diffusion_steps,
|
| 507 |
+
temperature=temperature,
|
| 508 |
+
strategy=strategy,
|
| 509 |
+
blocked_token_ids=(self.roles['pad'],),
|
| 510 |
+
generator=generator,
|
| 511 |
+
):
|
| 512 |
+
compute += time.perf_counter() - tick
|
| 513 |
+
final_ids = [int(t) for t in state.tokens[0, len(prompt_ids) + 1:]]
|
| 514 |
+
if step_delay or state.step % 8 == 0 or state.masked_remaining == 0:
|
| 515 |
+
if step_delay:
|
| 516 |
+
thoughts = [self._active_slot_text(final_ids)]
|
| 517 |
+
answer = ''
|
| 518 |
+
else:
|
| 519 |
+
visible = [t for t in final_ids if t != mask_id]
|
| 520 |
+
thoughts, answer = self._split_flat(visible)
|
| 521 |
+
revealed = len(final_ids) - state.masked_remaining
|
| 522 |
+
rate = revealed / compute if compute > 0 else 0.0
|
| 523 |
+
yield (
|
| 524 |
+
thoughts,
|
| 525 |
+
answer,
|
| 526 |
+
f'denoising {state.step}/{state.total_steps} · {rate:.0f} tok/s',
|
| 527 |
+
)
|
| 528 |
+
if step_delay and state.masked_remaining:
|
| 529 |
+
time.sleep(step_delay)
|
| 530 |
+
tick = time.perf_counter()
|
| 531 |
+
if eos_id in final_ids:
|
| 532 |
+
final_ids = final_ids[: final_ids.index(eos_id) + 1]
|
| 533 |
+
thoughts, answer = self._split_flat(final_ids)
|
| 534 |
+
revealed = len(final_ids)
|
| 535 |
+
rate = revealed / compute if compute > 0 else 0.0
|
| 536 |
+
yield thoughts, answer, (
|
| 537 |
+
f'listo · total: {revealed} tok en {compute:.2f}s ({rate:.0f} tok/s)'
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
@torch.no_grad()
|
| 541 |
+
def stream_chat(
|
| 542 |
+
self,
|
| 543 |
+
prefix_text: str,
|
| 544 |
+
*,
|
| 545 |
+
temperature: float,
|
| 546 |
+
steps_per_block: int,
|
| 547 |
+
max_answer_tokens: int = 224,
|
| 548 |
+
repetition_penalty: float = 1.0,
|
| 549 |
+
top_p: float = 1.0,
|
| 550 |
+
seed: int = 0,
|
| 551 |
+
strategy: str = 'ancestral',
|
| 552 |
+
step_delay: float = 0.0,
|
| 553 |
+
blocks_out: list[tuple[int, str]] | None = None,
|
| 554 |
+
):
|
| 555 |
+
"""Yield ``(thoughts, answer, status)`` snapshots for one ChatML assistant turn.
|
| 556 |
+
|
| 557 |
+
``prefix_text`` is the rendered conversation ending right after the assistant
|
| 558 |
+
header, encoded verbatim: training saw the trailing newline, so no stripping.
|
| 559 |
+
The answer stops at ``<|im_end|>`` as well as the eos token. ``blocks_out``
|
| 560 |
+
receives the turn's ``(size, content)`` blocks; a per-call sink rather than
|
| 561 |
+
engine state, so concurrent runs on the same engine cannot leak into each other.
|
| 562 |
+
"""
|
| 563 |
+
|
| 564 |
+
if not self.chat_ready:
|
| 565 |
+
raise ValueError('checkpoint is not an adaptive hybrid over a ChatML tokenizer')
|
| 566 |
+
generator = torch.Generator(device=self.device.type)
|
| 567 |
+
generator.manual_seed(seed if seed else int(time.time_ns() % 2**31))
|
| 568 |
+
prompt_ids = self.tokenizer.encode(prefix_text, add_special_tokens=False).ids
|
| 569 |
+
if len(prompt_ids) >= self.model.config.max_seq_len - 48:
|
| 570 |
+
message = (
|
| 571 |
+
f'contexto lleno: prefijo de {len(prompt_ids)} tokens con ventana de '
|
| 572 |
+
f'{self.model.config.max_seq_len} — bajá los mensajes visibles o reiniciá'
|
| 573 |
+
)
|
| 574 |
+
self.log_lines = [message]
|
| 575 |
+
yield [], '', message
|
| 576 |
+
return
|
| 577 |
+
yield from self._stream_adaptive(
|
| 578 |
+
prompt_ids,
|
| 579 |
+
stop_ids=(self.im_end_id, self.roles['eos']),
|
| 580 |
+
temperature=temperature,
|
| 581 |
+
steps_per_block=steps_per_block,
|
| 582 |
+
max_answer_tokens=max_answer_tokens,
|
| 583 |
+
repetition_penalty=repetition_penalty,
|
| 584 |
+
top_p=top_p,
|
| 585 |
+
strategy=strategy,
|
| 586 |
+
step_delay=step_delay,
|
| 587 |
+
generator=generator,
|
| 588 |
+
blocks_out=blocks_out,
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
@torch.no_grad()
|
| 592 |
+
def _stream_adaptive(
|
| 593 |
+
self,
|
| 594 |
+
prompt_ids: list[int],
|
| 595 |
+
*,
|
| 596 |
+
stop_ids: tuple[int, ...],
|
| 597 |
+
temperature: float,
|
| 598 |
+
steps_per_block: int,
|
| 599 |
+
max_answer_tokens: int,
|
| 600 |
+
repetition_penalty: float,
|
| 601 |
+
top_p: float,
|
| 602 |
+
strategy: str,
|
| 603 |
+
step_delay: float,
|
| 604 |
+
generator: torch.Generator,
|
| 605 |
+
blocks_out: list[tuple[int, str]] | None = None,
|
| 606 |
+
):
|
| 607 |
+
"""Stream the adaptive control/denoise/answer loop shared by solve and chat.
|
| 608 |
+
|
| 609 |
+
The answer phase reuses the prefix through the model's key/value cache when the
|
| 610 |
+
backbone provides one and ``MDLM_KV_CACHE`` selects the ar part, matching the
|
| 611 |
+
batch decoder in :mod:`diffusion_lm.hybrid`.
|
| 612 |
+
"""
|
| 613 |
+
|
| 614 |
+
mask_id = self.model.config.mask_token_id
|
| 615 |
+
think_id = self.reasoning_ids['think']
|
| 616 |
+
end_think_id = self.reasoning_ids['end_think']
|
| 617 |
+
size_by_id = {token_id: size for size, token_id in self.size_ids.items()}
|
| 618 |
+
size_ids_tensor = torch.tensor(sorted(self.size_ids.values()), device=self.device)
|
| 619 |
+
control_ids = [*size_by_id.keys(), end_think_id]
|
| 620 |
+
cached = hasattr(self.model, 'forward_cached') and _kv_cache_enabled('ar')
|
| 621 |
+
self.log_lines = [
|
| 622 |
+
f'prompt_tokens={len(prompt_ids)} temp={temperature} '
|
| 623 |
+
f'steps/block={steps_per_block} rep_penalty={repetition_penalty} '
|
| 624 |
+
f'top_p={top_p} max_blocks={self.max_blocks} '
|
| 625 |
+
f'sizes={sorted(size_by_id.values())} strategy={strategy} '
|
| 626 |
+
f'flex_eager={_FLEX_EAGER} kv_cache={"ar" if cached else "off"}'
|
| 627 |
+
]
|
| 628 |
+
sequence = [*prompt_ids, think_id]
|
| 629 |
+
prefix_len = len(prompt_ids) + 1
|
| 630 |
+
problem_tensor = torch.tensor([len(prompt_ids)], device=self.device)
|
| 631 |
+
think_compute = 0.0
|
| 632 |
+
terminated = False
|
| 633 |
+
for block_index in range(self.max_blocks):
|
| 634 |
+
tick = time.perf_counter()
|
| 635 |
+
control = _ar_predict_control(
|
| 636 |
+
self.model,
|
| 637 |
+
sequence,
|
| 638 |
+
control_ids,
|
| 639 |
+
prefix_len=prefix_len,
|
| 640 |
+
temperature=0.0,
|
| 641 |
+
device=self.device,
|
| 642 |
+
generator=generator,
|
| 643 |
+
causal_prefix=self.causal_prefix,
|
| 644 |
+
)
|
| 645 |
+
think_compute += time.perf_counter() - tick
|
| 646 |
+
if control == end_think_id:
|
| 647 |
+
terminated = True
|
| 648 |
+
self.log_lines.append(f' control {block_index + 1}: </think>')
|
| 649 |
+
break
|
| 650 |
+
size = size_by_id[control]
|
| 651 |
+
if len(sequence) + 1 + size > self.model.config.max_seq_len - 8:
|
| 652 |
+
self.log_lines.append(
|
| 653 |
+
f' control {block_index + 1}: <sz{size}> · sin contexto, corto acá'
|
| 654 |
+
)
|
| 655 |
+
break
|
| 656 |
+
sequence.append(control)
|
| 657 |
+
window_prefix = len(sequence)
|
| 658 |
+
block_wall_start = time.perf_counter()
|
| 659 |
+
window = torch.tensor(
|
| 660 |
+
[sequence + [mask_id] * size], dtype=torch.long, device=self.device
|
| 661 |
+
)
|
| 662 |
+
blocked = adaptive_block_mask(
|
| 663 |
+
window,
|
| 664 |
+
problem_tensor,
|
| 665 |
+
torch.tensor([window.shape[1]], device=self.device),
|
| 666 |
+
size_ids_tensor,
|
| 667 |
+
end_think_id,
|
| 668 |
+
causal_prefix=self.causal_prefix,
|
| 669 |
+
)
|
| 670 |
+
block: list[int] = []
|
| 671 |
+
tick = time.perf_counter()
|
| 672 |
+
for state in iterative_unmask_steps(
|
| 673 |
+
self.model,
|
| 674 |
+
window,
|
| 675 |
+
mask_id,
|
| 676 |
+
steps=steps_per_block,
|
| 677 |
+
temperature=temperature,
|
| 678 |
+
strategy=strategy,
|
| 679 |
+
attn_mask=blocked,
|
| 680 |
+
generator=generator,
|
| 681 |
+
):
|
| 682 |
+
think_compute += time.perf_counter() - tick
|
| 683 |
+
block = [int(t) for t in state.tokens[0, window_prefix:]]
|
| 684 |
+
yield (
|
| 685 |
+
self._adaptive_block_texts(sequence[prefix_len:])
|
| 686 |
+
+ [f'({size}) ' + self._active_slot_text(block)],
|
| 687 |
+
'',
|
| 688 |
+
f'denoising bloque {block_index + 1} (tamaño {size}) · paso '
|
| 689 |
+
f'{state.step}/{state.total_steps}',
|
| 690 |
+
)
|
| 691 |
+
if step_delay and state.step < state.total_steps:
|
| 692 |
+
time.sleep(step_delay)
|
| 693 |
+
tick = time.perf_counter()
|
| 694 |
+
sequence.extend(block)
|
| 695 |
+
self.log_lines.append(
|
| 696 |
+
f' B{block_index + 1}: <sz{size}> · '
|
| 697 |
+
f'{time.perf_counter() - block_wall_start:.2f}s wall'
|
| 698 |
+
)
|
| 699 |
+
sequence.append(end_think_id)
|
| 700 |
+
think_ids = sequence[prefix_len:]
|
| 701 |
+
if blocks_out is not None:
|
| 702 |
+
blocks_out.extend(self._block_contents(think_ids))
|
| 703 |
+
|
| 704 |
+
answer_ids: list[int] = []
|
| 705 |
+
answer_compute = 0.0
|
| 706 |
+
prefix_tensor = torch.tensor([prefix_len], device=self.device)
|
| 707 |
+
answer_budget = min(
|
| 708 |
+
max_answer_tokens, self.model.config.max_seq_len - len(sequence)
|
| 709 |
+
)
|
| 710 |
+
cache = self.model.new_cache() if cached else None
|
| 711 |
+
step_in = torch.tensor([sequence], dtype=torch.long, device=self.device)
|
| 712 |
+
tick = time.perf_counter()
|
| 713 |
+
for _ in range(max(0, answer_budget)):
|
| 714 |
+
if cached:
|
| 715 |
+
seen = cache.get_seq_length()
|
| 716 |
+
blocked = prefix_causal_blocked(
|
| 717 |
+
prefix_tensor, seen + step_in.shape[1], causal_prefix=self.causal_prefix
|
| 718 |
+
)[:, seen:, :]
|
| 719 |
+
output_positions = torch.zeros_like(step_in, dtype=torch.bool)
|
| 720 |
+
output_positions[0, -1] = True
|
| 721 |
+
logits, cache = self.model.forward_cached(
|
| 722 |
+
step_in, attn_mask=blocked, past_key_values=cache,
|
| 723 |
+
output_positions=output_positions,
|
| 724 |
+
)
|
| 725 |
+
else:
|
| 726 |
+
current = torch.tensor(
|
| 727 |
+
[sequence + answer_ids], dtype=torch.long, device=self.device
|
| 728 |
+
)
|
| 729 |
+
blocked = prefix_causal_blocked(
|
| 730 |
+
prefix_tensor, current.shape[1], causal_prefix=self.causal_prefix
|
| 731 |
+
)
|
| 732 |
+
output_positions = torch.zeros_like(current, dtype=torch.bool)
|
| 733 |
+
output_positions[0, -1] = True
|
| 734 |
+
logits = self.model(
|
| 735 |
+
current, output_positions=output_positions, attn_mask=blocked
|
| 736 |
+
)
|
| 737 |
+
logits = _apply_repetition_penalty(logits, answer_ids, repetition_penalty)
|
| 738 |
+
logits = _apply_top_p(logits, top_p)
|
| 739 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 740 |
+
token_id = int(token.item())
|
| 741 |
+
answer_ids.append(token_id)
|
| 742 |
+
step_in = torch.tensor([[token_id]], dtype=torch.long, device=self.device)
|
| 743 |
+
answer_compute += time.perf_counter() - tick
|
| 744 |
+
done = token_id in stop_ids
|
| 745 |
+
if step_delay or len(answer_ids) % 4 == 0 or done:
|
| 746 |
+
rate = len(answer_ids) / answer_compute if answer_compute > 0 else 0.0
|
| 747 |
+
yield (
|
| 748 |
+
self._adaptive_block_texts(think_ids),
|
| 749 |
+
self._decode(answer_ids),
|
| 750 |
+
f'respondiendo (AR, token {len(answer_ids)}) · {rate:.0f} tok/s',
|
| 751 |
+
)
|
| 752 |
+
if step_delay and not done:
|
| 753 |
+
time.sleep(step_delay / 3)
|
| 754 |
+
if done:
|
| 755 |
+
break
|
| 756 |
+
tick = time.perf_counter()
|
| 757 |
+
|
| 758 |
+
sizes_chosen = [
|
| 759 |
+
size_by_id[t] for t in think_ids if t in size_by_id
|
| 760 |
+
]
|
| 761 |
+
total_compute = think_compute + answer_compute
|
| 762 |
+
status = (
|
| 763 |
+
f'listo · bloques: {sizes_chosen} · terminated={terminated} · '
|
| 764 |
+
f'pensar {think_compute:.2f}s · responder {len(answer_ids)} tok en '
|
| 765 |
+
f'{answer_compute:.2f}s · total {total_compute:.2f}s'
|
| 766 |
+
)
|
| 767 |
+
self.log_lines.append(
|
| 768 |
+
f' answer: {len(answer_ids)} tok · {answer_compute:.2f}s wall'
|
| 769 |
+
)
|
| 770 |
+
self.log_lines.append(
|
| 771 |
+
f'TOTAL: sizes={sizes_chosen} terminated={terminated} · '
|
| 772 |
+
f'{total_compute:.2f}s'
|
| 773 |
+
)
|
| 774 |
+
yield self._adaptive_block_texts(think_ids), self._decode(answer_ids), status
|
| 775 |
+
|
| 776 |
+
def _split_flat(self, generated: list[int]) -> tuple[list[str], str]:
|
| 777 |
+
end_think_id = self.reasoning_ids['end_think']
|
| 778 |
+
if end_think_id in generated:
|
| 779 |
+
split = generated.index(end_think_id)
|
| 780 |
+
think, answer = generated[:split], generated[split + 1:]
|
| 781 |
+
else:
|
| 782 |
+
think, answer = generated, []
|
| 783 |
+
return [self._decode(think).strip()], self._decode(answer)
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
class HFCausalEngine:
|
| 787 |
+
"""Serve a Hugging Face causal LM behind the same streaming interface.
|
| 788 |
+
|
| 789 |
+
Thinking-mode outputs (Qwen3-style ``<think>...</think>`` prefixes) are routed to the
|
| 790 |
+
thoughts panel; tokens after the closing tag stream as the answer. Models without a
|
| 791 |
+
``</think>`` vocabulary entry stream everything as the answer.
|
| 792 |
+
"""
|
| 793 |
+
|
| 794 |
+
def __init__(self, model_path: str, device: torch.device) -> None:
|
| 795 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 796 |
+
|
| 797 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 798 |
+
dtype = torch.bfloat16 if device.type == 'cuda' else torch.float32
|
| 799 |
+
self.model = AutoModelForCausalLM.from_pretrained(model_path, dtype=dtype)
|
| 800 |
+
self.model.to(device).eval()
|
| 801 |
+
self.device = device
|
| 802 |
+
self.objective = 'hf-ar'
|
| 803 |
+
self.step = '-'
|
| 804 |
+
eos = self.model.generation_config.eos_token_id
|
| 805 |
+
self.eos_ids = set(eos if isinstance(eos, (list, tuple)) else [eos])
|
| 806 |
+
end_think = self.tokenizer.convert_tokens_to_ids('</think>')
|
| 807 |
+
unk = self.tokenizer.unk_token_id
|
| 808 |
+
self.end_think_id = end_think if isinstance(end_think, int) and end_think != unk else None
|
| 809 |
+
self.max_new_tokens = 1024
|
| 810 |
+
self.log_lines: list[str] = []
|
| 811 |
+
|
| 812 |
+
def _decode(self, ids: list[int]) -> str:
|
| 813 |
+
# Qwen3 registers <think>/</think> as regular added tokens, so
|
| 814 |
+
# skip_special_tokens leaves them in the decoded text.
|
| 815 |
+
text = self.tokenizer.decode(ids, skip_special_tokens=True)
|
| 816 |
+
return text.replace('<think>', '').replace('</think>', '').strip()
|
| 817 |
+
|
| 818 |
+
def _split(self, generated: list[int], split_at: int | None) -> tuple[list[str], str]:
|
| 819 |
+
if self.end_think_id is None:
|
| 820 |
+
return [], self._decode(generated)
|
| 821 |
+
if split_at is None:
|
| 822 |
+
thoughts = self._decode(generated)
|
| 823 |
+
return ([thoughts] if thoughts else []), ''
|
| 824 |
+
thoughts = self._decode(generated[: split_at - 1])
|
| 825 |
+
return ([thoughts] if thoughts else []), self._decode(generated[split_at:])
|
| 826 |
+
|
| 827 |
+
@torch.no_grad()
|
| 828 |
+
def stream_solve(
|
| 829 |
+
self,
|
| 830 |
+
problem: str,
|
| 831 |
+
*,
|
| 832 |
+
temperature: float,
|
| 833 |
+
steps_per_block: int,
|
| 834 |
+
diffusion_steps: int,
|
| 835 |
+
max_answer_tokens: int = 200,
|
| 836 |
+
repetition_penalty: float = 1.0,
|
| 837 |
+
top_p: float = 1.0,
|
| 838 |
+
seed: int = 0,
|
| 839 |
+
strategy: str = 'ancestral',
|
| 840 |
+
step_delay: float = 0.0,
|
| 841 |
+
):
|
| 842 |
+
"""Yield ``(thoughts, answer, status)`` snapshots while decoding token by token.
|
| 843 |
+
|
| 844 |
+
Diffusion-only knobs (``steps_per_block``, ``diffusion_steps``, ``strategy``) are
|
| 845 |
+
accepted for interface parity and ignored.
|
| 846 |
+
"""
|
| 847 |
+
|
| 848 |
+
del steps_per_block, diffusion_steps, strategy, max_answer_tokens
|
| 849 |
+
generator = torch.Generator(device=self.device.type)
|
| 850 |
+
generator.manual_seed(seed if seed else int(time.time_ns() % 2**31))
|
| 851 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 852 |
+
[{'role': 'user', 'content': problem.strip()}],
|
| 853 |
+
tokenize=False,
|
| 854 |
+
add_generation_prompt=True,
|
| 855 |
+
)
|
| 856 |
+
input_ids = self.tokenizer(prompt, return_tensors='pt').input_ids.to(self.device)
|
| 857 |
+
self.log_lines = [
|
| 858 |
+
f'hf-ar · prompt_tokens={input_ids.shape[1]} temp={temperature} top_p={top_p} '
|
| 859 |
+
f'rep_penalty={repetition_penalty} max_new_tokens={self.max_new_tokens} '
|
| 860 |
+
f'(Qwen3 sugerido: temp 0.6 · top_p 0.95 · rep 1.0)'
|
| 861 |
+
]
|
| 862 |
+
generated: list[int] = []
|
| 863 |
+
past_key_values = None
|
| 864 |
+
current = input_ids
|
| 865 |
+
split_at: int | None = None
|
| 866 |
+
compute = 0.0
|
| 867 |
+
tick = time.perf_counter()
|
| 868 |
+
for _ in range(self.max_new_tokens):
|
| 869 |
+
output = self.model(input_ids=current, past_key_values=past_key_values, use_cache=True)
|
| 870 |
+
past_key_values = output.past_key_values
|
| 871 |
+
logits = output.logits[:, -1, :].float()
|
| 872 |
+
logits = _apply_repetition_penalty(logits, generated, repetition_penalty)
|
| 873 |
+
logits = _apply_top_p(logits, top_p)
|
| 874 |
+
token, _ = _sample_categorical(logits, temperature, generator)
|
| 875 |
+
token_id = int(token.item())
|
| 876 |
+
generated.append(token_id)
|
| 877 |
+
compute += time.perf_counter() - tick
|
| 878 |
+
if split_at is None and token_id == self.end_think_id:
|
| 879 |
+
split_at = len(generated)
|
| 880 |
+
self.log_lines.append(f' </think> en token {split_at}')
|
| 881 |
+
done = token_id in self.eos_ids
|
| 882 |
+
if step_delay or done or len(generated) % 4 == 0:
|
| 883 |
+
thoughts, answer = self._split(generated, split_at)
|
| 884 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 885 |
+
thinking = self.end_think_id is not None and split_at is None
|
| 886 |
+
phase = 'pensando' if thinking else 'respondiendo'
|
| 887 |
+
yield thoughts, answer, f'{phase} (AR, token {len(generated)}) · {rate:.0f} tok/s'
|
| 888 |
+
if step_delay and not done:
|
| 889 |
+
time.sleep(step_delay / 3)
|
| 890 |
+
if done:
|
| 891 |
+
break
|
| 892 |
+
current = token.view(1, 1)
|
| 893 |
+
tick = time.perf_counter()
|
| 894 |
+
thoughts, answer = self._split(generated, split_at)
|
| 895 |
+
rate = len(generated) / compute if compute > 0 else 0.0
|
| 896 |
+
terminated = bool(generated) and generated[-1] in self.eos_ids
|
| 897 |
+
self.log_lines.append(
|
| 898 |
+
f'TOTAL: {len(generated)} tok · {compute:.2f}s ({rate:.0f} tok/s) · '
|
| 899 |
+
f'terminated={terminated}'
|
| 900 |
+
)
|
| 901 |
+
yield thoughts, answer, (
|
| 902 |
+
f'listo · total: {len(generated)} tok en {compute:.2f}s ({rate:.0f} tok/s) · '
|
| 903 |
+
f'terminated={terminated}'
|
| 904 |
+
)
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
def _render_thoughts(slots: list[str]) -> str:
|
| 908 |
+
if not slots:
|
| 909 |
+
return '<em>sin pensamientos todavía</em>'
|
| 910 |
+
chips = []
|
| 911 |
+
for index, text in enumerate(slots, start=1):
|
| 912 |
+
chips.append(
|
| 913 |
+
f'<div style="margin:6px 0;padding:8px 12px;border-radius:10px;'
|
| 914 |
+
f'background:rgba(124,58,237,.10);border:1px solid rgba(124,58,237,.25);">'
|
| 915 |
+
f'<b>T{index}</b> · {html.escape(text)}</div>'
|
| 916 |
+
)
|
| 917 |
+
return ''.join(chips)
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
def _chat_display(messages: list[dict[str, str]], partial: str = '') -> list[dict[str, str]]:
|
| 921 |
+
display = [{'role': m['role'], 'content': m['content']} for m in messages]
|
| 922 |
+
if partial:
|
| 923 |
+
display.append({'role': 'assistant', 'content': partial})
|
| 924 |
+
return display
|
| 925 |
+
|
| 926 |
+
|
| 927 |
+
def _chat_ledger(messages: list[dict[str, str]], keep: int) -> str:
|
| 928 |
+
return ledger_line(merge_notes(ledger_notes(messages, keep)))
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
def build_app(engines: dict[str, 'ReasoningEngine | HFCausalEngine']):
|
| 932 |
+
import gradio as gr
|
| 933 |
+
|
| 934 |
+
first = next(iter(engines))
|
| 935 |
+
|
| 936 |
+
def solve(
|
| 937 |
+
checkpoint_name, problem, temperature, steps_per_block, diffusion_steps,
|
| 938 |
+
repetition_penalty, top_p, seed, strategy, slowmo_ms,
|
| 939 |
+
):
|
| 940 |
+
engine = engines[checkpoint_name]
|
| 941 |
+
if not problem.strip():
|
| 942 |
+
yield '<em>escribí un problema primero</em>', '', 'esperando problema', ''
|
| 943 |
+
return
|
| 944 |
+
for slots, answer, status in engine.stream_solve(
|
| 945 |
+
problem,
|
| 946 |
+
temperature=float(temperature),
|
| 947 |
+
steps_per_block=int(steps_per_block),
|
| 948 |
+
diffusion_steps=int(diffusion_steps),
|
| 949 |
+
repetition_penalty=float(repetition_penalty),
|
| 950 |
+
top_p=float(top_p),
|
| 951 |
+
seed=int(seed),
|
| 952 |
+
strategy=str(strategy),
|
| 953 |
+
step_delay=float(slowmo_ms) / 1000.0,
|
| 954 |
+
):
|
| 955 |
+
boxed = extract_boxed_answer(answer or '')
|
| 956 |
+
answer_display = answer + (f'\n\n**→ respuesta extraída: {boxed}**' if boxed else '')
|
| 957 |
+
yield _render_thoughts(slots), answer_display, status, '\n'.join(engine.log_lines)
|
| 958 |
+
|
| 959 |
+
def chat_send(
|
| 960 |
+
checkpoint_name, user_text, messages, system_text, keep_last, temperature,
|
| 961 |
+
steps_per_block, repetition_penalty, top_p, max_answer_tokens, seed, slowmo_ms,
|
| 962 |
+
datalog,
|
| 963 |
+
):
|
| 964 |
+
engine = engines[checkpoint_name] if checkpoint_name in engines else None
|
| 965 |
+
messages = list(messages or [])
|
| 966 |
+
datalog = list(datalog or [])
|
| 967 |
+
keep = int(keep_last)
|
| 968 |
+
idle = _chat_display(messages), messages, '<em>sin pensamientos todavía</em>'
|
| 969 |
+
if engine is None or not getattr(engine, 'chat_ready', False):
|
| 970 |
+
yield (*idle, _chat_ledger(messages, keep),
|
| 971 |
+
'elegí un checkpoint adaptativo con tokenizer ChatML', '', gr.skip(),
|
| 972 |
+
datalog, gr.skip())
|
| 973 |
+
return
|
| 974 |
+
user_text = (user_text or '').strip()
|
| 975 |
+
if not user_text:
|
| 976 |
+
yield (*idle, _chat_ledger(messages, keep), 'escribí un mensaje primero',
|
| 977 |
+
'', gr.skip(), datalog, gr.skip())
|
| 978 |
+
return
|
| 979 |
+
messages.append({'role': 'user', 'content': user_text})
|
| 980 |
+
notes = ledger_notes(messages, keep)
|
| 981 |
+
merged = merge_notes(notes)
|
| 982 |
+
ledger = ledger_line(merged)
|
| 983 |
+
start = window_start(messages, keep)
|
| 984 |
+
system = (system_text or '').strip() or SYSTEM
|
| 985 |
+
prefix = chat_prefix(
|
| 986 |
+
[{'role': m['role'], 'content': m['content']} for m in messages[start:]],
|
| 987 |
+
system=system,
|
| 988 |
+
extra=ledger,
|
| 989 |
+
)
|
| 990 |
+
history_snapshot = [
|
| 991 |
+
{'index': index, 'visible': index >= start, **message}
|
| 992 |
+
for index, message in enumerate(messages)
|
| 993 |
+
]
|
| 994 |
+
answer = ''
|
| 995 |
+
status = ''
|
| 996 |
+
blocks: list[tuple[int, str]] = []
|
| 997 |
+
thoughts_html = '<em>sin pensamientos todavía</em>'
|
| 998 |
+
for slots, answer, status in engine.stream_chat(
|
| 999 |
+
prefix,
|
| 1000 |
+
temperature=float(temperature),
|
| 1001 |
+
steps_per_block=int(steps_per_block),
|
| 1002 |
+
max_answer_tokens=int(max_answer_tokens),
|
| 1003 |
+
repetition_penalty=float(repetition_penalty),
|
| 1004 |
+
top_p=float(top_p),
|
| 1005 |
+
seed=int(seed),
|
| 1006 |
+
step_delay=float(slowmo_ms) / 1000.0,
|
| 1007 |
+
blocks_out=blocks,
|
| 1008 |
+
):
|
| 1009 |
+
thoughts_html = _render_thoughts(slots)
|
| 1010 |
+
yield (_chat_display(messages, answer), messages, thoughts_html, ledger,
|
| 1011 |
+
status, '\n'.join(engine.log_lines), '', datalog, gr.skip())
|
| 1012 |
+
note = '; '.join(text for _, text in blocks if text)
|
| 1013 |
+
if answer.strip() or note:
|
| 1014 |
+
messages.append({'role': 'assistant', 'content': answer.strip(), 'note': note})
|
| 1015 |
+
restored_input = ''
|
| 1016 |
+
else:
|
| 1017 |
+
# The turn produced nothing (context-full guard): undo the user message and
|
| 1018 |
+
# put its text back in the box so it can be resent after lowering the window.
|
| 1019 |
+
messages.pop()
|
| 1020 |
+
restored_input = user_text
|
| 1021 |
+
datalog.append({
|
| 1022 |
+
'turn': sum(1 for m in messages if m['role'] == 'user') + bool(restored_input),
|
| 1023 |
+
'checkpoint': checkpoint_name,
|
| 1024 |
+
'settings': {
|
| 1025 |
+
'temperature': float(temperature), 'steps_per_block': int(steps_per_block),
|
| 1026 |
+
'repetition_penalty': float(repetition_penalty), 'top_p': float(top_p),
|
| 1027 |
+
'max_answer_tokens': int(max_answer_tokens), 'seed': int(seed),
|
| 1028 |
+
'keep_last': keep, 'system': system,
|
| 1029 |
+
},
|
| 1030 |
+
'history_at_send': history_snapshot,
|
| 1031 |
+
'ledger': {'notes': notes, 'merged': merged, 'line': ledger},
|
| 1032 |
+
'prefix_sent': prefix,
|
| 1033 |
+
'prefix_tokens': len(
|
| 1034 |
+
engine.tokenizer.encode(prefix, add_special_tokens=False).ids
|
| 1035 |
+
),
|
| 1036 |
+
'thinking_blocks': [
|
| 1037 |
+
{'size': size, 'content': text} for size, text in blocks
|
| 1038 |
+
],
|
| 1039 |
+
'answer': answer.strip(),
|
| 1040 |
+
'status': status,
|
| 1041 |
+
'engine_log': list(engine.log_lines),
|
| 1042 |
+
})
|
| 1043 |
+
yield (_chat_display(messages), messages, thoughts_html,
|
| 1044 |
+
_chat_ledger(messages, keep), status, '\n'.join(engine.log_lines),
|
| 1045 |
+
restored_input, datalog,
|
| 1046 |
+
json.dumps(datalog, indent=2, ensure_ascii=False))
|
| 1047 |
+
|
| 1048 |
+
def chat_reset():
|
| 1049 |
+
return ([], [], '<em>sin pensamientos todavía</em>', '', 'conversación reiniciada',
|
| 1050 |
+
'', '', [], '')
|
| 1051 |
+
|
| 1052 |
+
chat_names = [name for name, engine in engines.items()
|
| 1053 |
+
if getattr(engine, 'chat_ready', False)]
|
| 1054 |
+
chat_default = next((n for n in chat_names if 'chat' in n), chat_names[0] if chat_names else None)
|
| 1055 |
+
|
| 1056 |
+
with gr.Blocks(title='Reasoning playground') as app:
|
| 1057 |
+
names = {
|
| 1058 |
+
name: f'{name} · {engines[name].objective} · step {engines[name].step}'
|
| 1059 |
+
for name in engines
|
| 1060 |
+
}
|
| 1061 |
+
gr.Markdown('# Reasoning playground')
|
| 1062 |
+
with gr.Tab('chat'):
|
| 1063 |
+
chat_state = gr.State([])
|
| 1064 |
+
chat_datalog_state = gr.State([])
|
| 1065 |
+
with gr.Row():
|
| 1066 |
+
with gr.Column(scale=2):
|
| 1067 |
+
chat_checkpoint = gr.Dropdown(
|
| 1068 |
+
choices=chat_names, value=chat_default, label='checkpoint',
|
| 1069 |
+
info='adaptativos sobre tokenizer ChatML; solo los entrenados '
|
| 1070 |
+
'en chat (chat-sft) conocen este formato',
|
| 1071 |
+
)
|
| 1072 |
+
chat_system = gr.Textbox(label='system', value=SYSTEM, lines=3)
|
| 1073 |
+
chat_keep = gr.Slider(
|
| 1074 |
+
0, 12, value=4, step=2,
|
| 1075 |
+
label='mensajes visibles (0 = historial completo; lo anterior '
|
| 1076 |
+
'sobrevive solo en el ledger)',
|
| 1077 |
+
)
|
| 1078 |
+
chat_temperature = gr.Slider(0.0, 1.2, value=0.8, step=0.05,
|
| 1079 |
+
label='temperatura')
|
| 1080 |
+
chat_steps = gr.Slider(
|
| 1081 |
+
1, 64, value=16, step=1,
|
| 1082 |
+
label='pasos de denoising por bloque (32 es el óptimo medido)',
|
| 1083 |
+
)
|
| 1084 |
+
chat_rep = gr.Slider(1.0, 2.0, value=1.4, step=0.05,
|
| 1085 |
+
label='penalización de repetición (respuesta)')
|
| 1086 |
+
chat_top_p = gr.Slider(0.1, 1.0, value=0.92, step=0.02,
|
| 1087 |
+
label='top-p (respuesta)')
|
| 1088 |
+
chat_max_answer = gr.Slider(32, 512, value=224, step=16,
|
| 1089 |
+
label='tokens máximos de respuesta')
|
| 1090 |
+
chat_slowmo = gr.Slider(0, 600, value=0, step=20,
|
| 1091 |
+
label='cámara lenta (ms por paso de denoising)')
|
| 1092 |
+
chat_seed = gr.Number(value=0, label='seed (0 = aleatoria)', precision=0)
|
| 1093 |
+
chat_clear = gr.Button('reiniciar conversación')
|
| 1094 |
+
with gr.Column(scale=3):
|
| 1095 |
+
chatbot = gr.Chatbot(label='conversación', height=420)
|
| 1096 |
+
chat_input = gr.Textbox(
|
| 1097 |
+
label='mensaje', lines=2,
|
| 1098 |
+
placeholder='p.ej. My policy is PL-48291. — o cualquier pregunta',
|
| 1099 |
+
)
|
| 1100 |
+
chat_go = gr.Button('enviar', variant='primary')
|
| 1101 |
+
chat_status = gr.Markdown('esperando mensaje')
|
| 1102 |
+
chat_thoughts = gr.HTML(label='pensamientos del turno')
|
| 1103 |
+
chat_ledger_box = gr.Textbox(
|
| 1104 |
+
label='ledger (notas del modelo fuera de la ventana visible)',
|
| 1105 |
+
interactive=False,
|
| 1106 |
+
)
|
| 1107 |
+
chat_logs = gr.Textbox(label='logs', lines=8, max_lines=20,
|
| 1108 |
+
interactive=False)
|
| 1109 |
+
with gr.Accordion('datalog — todo lo que viajó, por turno', open=False):
|
| 1110 |
+
chat_datalog_box = gr.Textbox(
|
| 1111 |
+
label='sesión completa en JSON: settings, historial con ventana, '
|
| 1112 |
+
'ledger, prefijo exacto, bloques de thinking, respuesta y logs',
|
| 1113 |
+
lines=18, max_lines=40, interactive=False, buttons=['copy'],
|
| 1114 |
+
)
|
| 1115 |
+
chat_inputs = [
|
| 1116 |
+
chat_checkpoint, chat_input, chat_state, chat_system, chat_keep,
|
| 1117 |
+
chat_temperature, chat_steps, chat_rep, chat_top_p, chat_max_answer,
|
| 1118 |
+
chat_seed, chat_slowmo, chat_datalog_state,
|
| 1119 |
+
]
|
| 1120 |
+
chat_outputs = [
|
| 1121 |
+
chatbot, chat_state, chat_thoughts, chat_ledger_box, chat_status,
|
| 1122 |
+
chat_logs, chat_input, chat_datalog_state, chat_datalog_box,
|
| 1123 |
+
]
|
| 1124 |
+
chat_go.click(chat_send, inputs=chat_inputs, outputs=chat_outputs)
|
| 1125 |
+
chat_input.submit(chat_send, inputs=chat_inputs, outputs=chat_outputs)
|
| 1126 |
+
chat_clear.click(chat_reset, inputs=[], outputs=chat_outputs)
|
| 1127 |
+
with gr.Tab('resolver'):
|
| 1128 |
+
with gr.Row():
|
| 1129 |
+
with gr.Column(scale=2):
|
| 1130 |
+
checkpoint = gr.Dropdown(
|
| 1131 |
+
choices=list(engines), value=first, label='checkpoint',
|
| 1132 |
+
info=' | '.join(names.values()),
|
| 1133 |
+
)
|
| 1134 |
+
problem = gr.Textbox(
|
| 1135 |
+
label='problema / instrucción',
|
| 1136 |
+
lines=4,
|
| 1137 |
+
placeholder=(
|
| 1138 |
+
'hybrid: Write a short story. It should feature: Dialogue. '
|
| 1139 |
+
'Use the words: dragon, cake, brave.\n'
|
| 1140 |
+
'lm (base): cualquier texto a continuar, p.ej. '
|
| 1141 |
+
'"Tom was a happy boy who"'
|
| 1142 |
+
),
|
| 1143 |
+
)
|
| 1144 |
+
temperature = gr.Slider(0.0, 1.2, value=0.8, step=0.05,
|
| 1145 |
+
label='temperatura')
|
| 1146 |
+
steps_per_block = gr.Slider(
|
| 1147 |
+
1, 64, value=16, step=1,
|
| 1148 |
+
label='pasos de denoising por slot/bloque (hybrid)'
|
| 1149 |
+
)
|
| 1150 |
+
diffusion_steps = gr.Slider(
|
| 1151 |
+
8, 256, value=64, step=8, label='pasos totales (diffusion)'
|
| 1152 |
+
)
|
| 1153 |
+
repetition_penalty = gr.Slider(
|
| 1154 |
+
1.0, 2.0, value=1.4, step=0.05,
|
| 1155 |
+
label='penalización de repetición (respuesta)'
|
| 1156 |
+
)
|
| 1157 |
+
top_p = gr.Slider(
|
| 1158 |
+
0.1, 1.0, value=0.92, step=0.02, label='top-p (respuesta)'
|
| 1159 |
+
)
|
| 1160 |
+
strategy = gr.Dropdown(
|
| 1161 |
+
choices=['ancestral', 'confidence', 'left_to_right'],
|
| 1162 |
+
value='ancestral',
|
| 1163 |
+
label='orden de revelado (denoising; ancestral es el único que rinde)',
|
| 1164 |
+
)
|
| 1165 |
+
slowmo_ms = gr.Slider(
|
| 1166 |
+
0, 600, value=0, step=20,
|
| 1167 |
+
label='cámara lenta (ms por paso de denoising, 0 = tiempo real)',
|
| 1168 |
+
)
|
| 1169 |
+
seed = gr.Number(value=0, label='seed (0 = aleatoria)', precision=0)
|
| 1170 |
+
go = gr.Button('resolver', variant='primary')
|
| 1171 |
+
with gr.Column(scale=3):
|
| 1172 |
+
status = gr.Markdown('esperando problema')
|
| 1173 |
+
thoughts = gr.HTML(label='pensamientos')
|
| 1174 |
+
answer = gr.Markdown(label='respuesta')
|
| 1175 |
+
logs = gr.Textbox(
|
| 1176 |
+
label='logs (copiá y pegá)', lines=12, max_lines=30,
|
| 1177 |
+
interactive=False,
|
| 1178 |
+
)
|
| 1179 |
+
go.click(
|
| 1180 |
+
solve,
|
| 1181 |
+
inputs=[
|
| 1182 |
+
checkpoint, problem, temperature, steps_per_block, diffusion_steps,
|
| 1183 |
+
repetition_penalty, top_p, seed, strategy, slowmo_ms,
|
| 1184 |
+
],
|
| 1185 |
+
outputs=[thoughts, answer, status, logs],
|
| 1186 |
+
)
|
| 1187 |
+
return app
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
+
def main() -> None:
|
| 1191 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 1192 |
+
parser.add_argument('--outputs-dir', type=Path, default=Path('outputs'))
|
| 1193 |
+
parser.add_argument('--prefix', default='reasoning-')
|
| 1194 |
+
parser.add_argument('--tokenizer', type=Path, required=True)
|
| 1195 |
+
parser.add_argument('--device', default='auto')
|
| 1196 |
+
parser.add_argument('--host', default='127.0.0.1')
|
| 1197 |
+
parser.add_argument('--port', type=int, default=7999)
|
| 1198 |
+
parser.add_argument(
|
| 1199 |
+
'--hf-model', action='append', default=[], metavar='NAME=PATH',
|
| 1200 |
+
help='serve a Hugging Face causal LM (local path or repo id) alongside the checkpoints',
|
| 1201 |
+
)
|
| 1202 |
+
args = parser.parse_args()
|
| 1203 |
+
|
| 1204 |
+
device = resolve_device(args.device)
|
| 1205 |
+
checkpoints = _discover_checkpoints(args.outputs_dir)
|
| 1206 |
+
checkpoints = {
|
| 1207 |
+
name: path for name, path in checkpoints.items() if name.startswith(args.prefix)
|
| 1208 |
+
}
|
| 1209 |
+
if not checkpoints and not args.hf_model:
|
| 1210 |
+
raise SystemExit(f'no reasoning checkpoints under {args.outputs_dir}')
|
| 1211 |
+
engines: dict[str, ReasoningEngine | HFCausalEngine] = {
|
| 1212 |
+
name: ReasoningEngine(path, args.tokenizer, device)
|
| 1213 |
+
for name, path in checkpoints.items()
|
| 1214 |
+
}
|
| 1215 |
+
for spec in args.hf_model:
|
| 1216 |
+
name, _, path = spec.partition('=')
|
| 1217 |
+
if not name or not path:
|
| 1218 |
+
raise SystemExit(f'--hf-model expects NAME=PATH, got {spec!r}')
|
| 1219 |
+
engines[name] = HFCausalEngine(path, device)
|
| 1220 |
+
print(f'loaded: {", ".join(engines)} on {device}')
|
| 1221 |
+
app = build_app(engines)
|
| 1222 |
+
app.queue().launch(server_name=args.host, server_port=args.port)
|
| 1223 |
+
|
| 1224 |
+
|
| 1225 |
+
if __name__ == '__main__':
|
| 1226 |
+
main()
|
src/diffusion_lm/reasoning_train.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training loop for the reasoning variants: ar, diffusion, and hybrid objectives."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
from dataclasses import dataclass, asdict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import yaml
|
| 15 |
+
from torch.utils.data import DataLoader
|
| 16 |
+
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
from diffusion_lm.config import ModelConfig, TrainingConfig
|
| 20 |
+
from diffusion_lm.data import DeterministicBatchSampler, load_packed_dataset
|
| 21 |
+
from diffusion_lm.diffusion import diffusion_cross_entropy
|
| 22 |
+
from diffusion_lm.hybrid import (
|
| 23 |
+
adaptive_hybrid_objective,
|
| 24 |
+
ar_objective,
|
| 25 |
+
block_diffusion_objective,
|
| 26 |
+
block_size_curriculum,
|
| 27 |
+
diffusion_objective,
|
| 28 |
+
hybrid_objective,
|
| 29 |
+
)
|
| 30 |
+
from diffusion_lm.model import DiffusionTransformer, build_denoiser, format_parameter_count
|
| 31 |
+
from diffusion_lm.reasoning import ReasoningTokenDataset
|
| 32 |
+
from diffusion_lm.tokenizer import load_tokenizer
|
| 33 |
+
from diffusion_lm.train import (
|
| 34 |
+
_inference_state_dict,
|
| 35 |
+
autocast_context,
|
| 36 |
+
build_optimizer,
|
| 37 |
+
capture_rng_state,
|
| 38 |
+
configure_cuda_backends,
|
| 39 |
+
create_grad_scaler,
|
| 40 |
+
learning_rate,
|
| 41 |
+
resolve_device,
|
| 42 |
+
resolve_precision,
|
| 43 |
+
restore_rng_state,
|
| 44 |
+
seed_everything,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
REASONING_CHECKPOINT_FORMAT = 'mini-diffusion-lm-reasoning-checkpoint-v1'
|
| 48 |
+
OBJECTIVES = ('ar', 'diffusion', 'hybrid', 'lm', 'block_diffusion')
|
| 49 |
+
# Objectives trained on continuous packed text (manifest datasets, no region annotations).
|
| 50 |
+
PACKED_OBJECTIVES = ('lm', 'block_diffusion')
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass(frozen=True)
|
| 54 |
+
class ReasoningConfig:
|
| 55 |
+
"""Objective selection and hybrid-mode hyperparameters."""
|
| 56 |
+
|
| 57 |
+
objective: str
|
| 58 |
+
think_probability: float = 0.65
|
| 59 |
+
adaptive: bool = False
|
| 60 |
+
sizes: tuple[int, ...] = ()
|
| 61 |
+
causal_prefix: bool = False
|
| 62 |
+
curriculum_steps: int = 0
|
| 63 |
+
ar_probability: float = 0.0
|
| 64 |
+
# Fraction of think tokens corrupted in the causal samples, so the controller learns its
|
| 65 |
+
# decisions from damaged context instead of only from pristine traces.
|
| 66 |
+
control_context_noise: float = 0.0
|
| 67 |
+
|
| 68 |
+
def __post_init__(self) -> None:
|
| 69 |
+
if self.objective not in OBJECTIVES:
|
| 70 |
+
raise ValueError(f'objective must be one of {OBJECTIVES}')
|
| 71 |
+
if not 0.0 < self.think_probability < 1.0:
|
| 72 |
+
raise ValueError('think_probability must be in (0, 1)')
|
| 73 |
+
if self.adaptive and self.objective != 'hybrid':
|
| 74 |
+
raise ValueError('adaptive block sizing is only defined for the hybrid objective')
|
| 75 |
+
if not isinstance(self.causal_prefix, bool):
|
| 76 |
+
raise ValueError('causal_prefix must be a boolean')
|
| 77 |
+
if self.curriculum_steps < 0:
|
| 78 |
+
raise ValueError('curriculum_steps must be non-negative')
|
| 79 |
+
if not 0.0 <= self.ar_probability < 1.0:
|
| 80 |
+
raise ValueError('ar_probability must be in [0, 1)')
|
| 81 |
+
if not 0.0 <= self.control_context_noise < 1.0:
|
| 82 |
+
raise ValueError('control_context_noise must be in [0, 1)')
|
| 83 |
+
sizes = tuple(self.sizes)
|
| 84 |
+
if self.objective == 'block_diffusion':
|
| 85 |
+
if not sizes:
|
| 86 |
+
raise ValueError('block_diffusion requires a non-empty sizes menu')
|
| 87 |
+
if list(sizes) != sorted(set(sizes)) or sizes[0] <= 0:
|
| 88 |
+
raise ValueError('sizes must be unique, ascending, and positive')
|
| 89 |
+
object.__setattr__(self, 'sizes', sizes)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@dataclass(frozen=True)
|
| 93 |
+
class ReasoningExperiment:
|
| 94 |
+
model: ModelConfig
|
| 95 |
+
training: TrainingConfig
|
| 96 |
+
reasoning: ReasoningConfig
|
| 97 |
+
|
| 98 |
+
def to_dict(self) -> dict[str, Any]:
|
| 99 |
+
return asdict(self)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def load_reasoning_config(path: str | Path) -> ReasoningExperiment:
|
| 103 |
+
with Path(path).open('r', encoding='utf-8') as handle:
|
| 104 |
+
raw = yaml.safe_load(handle)
|
| 105 |
+
for section in ('model', 'training', 'reasoning'):
|
| 106 |
+
if section not in raw:
|
| 107 |
+
raise ValueError(f'config must contain a top-level {section} mapping')
|
| 108 |
+
return ReasoningExperiment(
|
| 109 |
+
model=ModelConfig(**raw['model']),
|
| 110 |
+
training=TrainingConfig(**raw['training']),
|
| 111 |
+
reasoning=ReasoningConfig(**raw['reasoning']),
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _validate_inputs(experiment: ReasoningExperiment, datasets: list) -> None:
|
| 116 |
+
tokenizer = load_tokenizer(experiment.training.tokenizer)
|
| 117 |
+
actual_vocab = tokenizer.get_vocab_size(with_added_tokens=True)
|
| 118 |
+
if experiment.model.backbone == 'project':
|
| 119 |
+
if actual_vocab != experiment.model.vocab_size:
|
| 120 |
+
raise ValueError(
|
| 121 |
+
f'config vocab_size is {experiment.model.vocab_size}, '
|
| 122 |
+
f'tokenizer has {actual_vocab}'
|
| 123 |
+
)
|
| 124 |
+
elif actual_vocab > experiment.model.vocab_size:
|
| 125 |
+
# Pretrained embedding matrices may carry unused tail rows beyond the tokenizer.
|
| 126 |
+
raise ValueError(
|
| 127 |
+
f'tokenizer has {actual_vocab} tokens, beyond the {experiment.model.vocab_size} '
|
| 128 |
+
f'embedding rows of the pretrained backbone'
|
| 129 |
+
)
|
| 130 |
+
tokenizer_hash = hashlib.sha256(
|
| 131 |
+
Path(experiment.training.tokenizer).read_bytes()
|
| 132 |
+
).hexdigest()
|
| 133 |
+
for dataset in datasets:
|
| 134 |
+
if dataset.metadata['tokenizer_sha256'] != tokenizer_hash:
|
| 135 |
+
raise ValueError(f'{dataset.path} was encoded with a different tokenizer file')
|
| 136 |
+
if int(dataset.metadata['vocab_size']) != actual_vocab:
|
| 137 |
+
raise ValueError(f'{dataset.path} was encoded with a different vocabulary size')
|
| 138 |
+
if 'layout' not in dataset.metadata:
|
| 139 |
+
continue
|
| 140 |
+
if experiment.reasoning.objective == 'hybrid':
|
| 141 |
+
expected_layout = 'adaptive' if experiment.reasoning.adaptive else 'slotted'
|
| 142 |
+
else:
|
| 143 |
+
expected_layout = 'flat'
|
| 144 |
+
if dataset.metadata['layout'] != expected_layout:
|
| 145 |
+
raise ValueError(
|
| 146 |
+
f'{dataset.path} has layout {dataset.metadata["layout"]!r}; the '
|
| 147 |
+
f'{experiment.reasoning.objective} objective requires {expected_layout!r}'
|
| 148 |
+
)
|
| 149 |
+
if dataset.seq_len != experiment.model.max_seq_len:
|
| 150 |
+
raise ValueError(f'{dataset.path} sequence length does not match max_seq_len')
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _lm_objective(
|
| 154 |
+
model: DiffusionTransformer, tokens: torch.Tensor
|
| 155 |
+
) -> tuple[torch.Tensor, dict[str, float]]:
|
| 156 |
+
"""Plain causal-LM pretraining over continuous packed text."""
|
| 157 |
+
|
| 158 |
+
seq_len = tokens.shape[1]
|
| 159 |
+
causal_blocked = torch.triu(
|
| 160 |
+
torch.ones(seq_len, seq_len, dtype=torch.bool, device=tokens.device), diagonal=1
|
| 161 |
+
)
|
| 162 |
+
output_positions = torch.ones_like(tokens, dtype=torch.bool)
|
| 163 |
+
output_positions[:, -1] = False
|
| 164 |
+
logits = model(tokens, output_positions=output_positions, attn_mask=causal_blocked)
|
| 165 |
+
targets = tokens[:, 1:].reshape(-1)
|
| 166 |
+
loss = F.cross_entropy(logits.float(), targets)
|
| 167 |
+
accuracy = float((logits.argmax(dim=-1) == targets).float().mean())
|
| 168 |
+
return loss, {'accuracy': accuracy}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _objective_step(
|
| 172 |
+
model: DiffusionTransformer,
|
| 173 |
+
tokens: torch.Tensor,
|
| 174 |
+
regions: torch.Tensor | None,
|
| 175 |
+
experiment: ReasoningExperiment,
|
| 176 |
+
*,
|
| 177 |
+
eval_mask_level: float | None = None,
|
| 178 |
+
step: int | None = None,
|
| 179 |
+
) -> tuple[torch.Tensor, dict[str, float]]:
|
| 180 |
+
objective = experiment.reasoning.objective
|
| 181 |
+
if objective == 'lm':
|
| 182 |
+
return _lm_objective(model, tokens)
|
| 183 |
+
if objective == 'block_diffusion':
|
| 184 |
+
size_weights = block_size_curriculum(
|
| 185 |
+
step,
|
| 186 |
+
n_sizes=len(experiment.reasoning.sizes),
|
| 187 |
+
curriculum_steps=experiment.reasoning.curriculum_steps,
|
| 188 |
+
)
|
| 189 |
+
output = block_diffusion_objective(
|
| 190 |
+
model,
|
| 191 |
+
tokens,
|
| 192 |
+
sizes=experiment.reasoning.sizes,
|
| 193 |
+
size_weights=size_weights,
|
| 194 |
+
mask_eps=experiment.training.mask_eps,
|
| 195 |
+
ar_probability=experiment.reasoning.ar_probability,
|
| 196 |
+
)
|
| 197 |
+
return output.loss, {
|
| 198 |
+
'think_loss': output.think_loss,
|
| 199 |
+
'answer_loss': output.answer_loss,
|
| 200 |
+
'think_accuracy': output.think_accuracy,
|
| 201 |
+
'answer_accuracy': output.answer_accuracy,
|
| 202 |
+
}
|
| 203 |
+
if objective == 'hybrid':
|
| 204 |
+
if experiment.reasoning.adaptive:
|
| 205 |
+
size_ids = torch.tensor(model.adaptive_size_ids, device=tokens.device)
|
| 206 |
+
output = adaptive_hybrid_objective(
|
| 207 |
+
model,
|
| 208 |
+
tokens,
|
| 209 |
+
regions,
|
| 210 |
+
size_ids=size_ids,
|
| 211 |
+
end_think_id=model.adaptive_end_think_id,
|
| 212 |
+
think_probability=experiment.reasoning.think_probability,
|
| 213 |
+
mask_eps=experiment.training.mask_eps,
|
| 214 |
+
causal_prefix=experiment.reasoning.causal_prefix,
|
| 215 |
+
control_context_noise=experiment.reasoning.control_context_noise,
|
| 216 |
+
)
|
| 217 |
+
else:
|
| 218 |
+
output = hybrid_objective(
|
| 219 |
+
model,
|
| 220 |
+
tokens,
|
| 221 |
+
regions,
|
| 222 |
+
block=int(model.reasoning_block),
|
| 223 |
+
think_probability=experiment.reasoning.think_probability,
|
| 224 |
+
mask_eps=experiment.training.mask_eps,
|
| 225 |
+
)
|
| 226 |
+
metrics = {
|
| 227 |
+
'think_loss': output.think_loss,
|
| 228 |
+
'answer_loss': output.answer_loss,
|
| 229 |
+
'think_accuracy': output.think_accuracy,
|
| 230 |
+
'answer_accuracy': output.answer_accuracy,
|
| 231 |
+
}
|
| 232 |
+
if experiment.reasoning.adaptive:
|
| 233 |
+
metrics['control_accuracy'] = output.control_accuracy
|
| 234 |
+
metrics['stop_accuracy'] = output.stop_accuracy
|
| 235 |
+
return output.loss, metrics
|
| 236 |
+
if objective == 'ar':
|
| 237 |
+
output = ar_objective(model, tokens, regions)
|
| 238 |
+
return output.loss, {'accuracy': output.accuracy}
|
| 239 |
+
mask_probability = None
|
| 240 |
+
if eval_mask_level is not None:
|
| 241 |
+
mask_probability = torch.full(
|
| 242 |
+
(tokens.shape[0],), eval_mask_level, device=tokens.device, dtype=torch.float32
|
| 243 |
+
)
|
| 244 |
+
logits, corruption, _ = diffusion_objective(
|
| 245 |
+
model,
|
| 246 |
+
tokens,
|
| 247 |
+
regions,
|
| 248 |
+
mask_eps=experiment.training.mask_eps,
|
| 249 |
+
mask_probability=mask_probability,
|
| 250 |
+
)
|
| 251 |
+
output = diffusion_cross_entropy(logits, tokens, corruption)
|
| 252 |
+
return output.loss, {'masked_accuracy': float(output.masked_accuracy)}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
@torch.no_grad()
|
| 256 |
+
def _evaluate(
|
| 257 |
+
model: DiffusionTransformer,
|
| 258 |
+
loader: DataLoader,
|
| 259 |
+
experiment: ReasoningExperiment,
|
| 260 |
+
device: torch.device,
|
| 261 |
+
precision: str,
|
| 262 |
+
) -> dict[str, float]:
|
| 263 |
+
state = capture_rng_state(device)
|
| 264 |
+
was_training = model.training
|
| 265 |
+
try:
|
| 266 |
+
seed_everything(0, device)
|
| 267 |
+
model.eval()
|
| 268 |
+
totals: dict[str, float] = {}
|
| 269 |
+
batches = 0
|
| 270 |
+
max_batches = experiment.training.eval_batches
|
| 271 |
+
for batch_index, batch in enumerate(loader):
|
| 272 |
+
if batch_index >= max_batches:
|
| 273 |
+
break
|
| 274 |
+
if experiment.reasoning.objective in PACKED_OBJECTIVES:
|
| 275 |
+
tokens, regions = batch, None
|
| 276 |
+
else:
|
| 277 |
+
tokens, regions = batch
|
| 278 |
+
regions = regions.to(device, non_blocking=True)
|
| 279 |
+
tokens = tokens.to(device, non_blocking=True)
|
| 280 |
+
level = experiment.training.mask_eps + (1.0 - experiment.training.mask_eps) * (
|
| 281 |
+
(batch_index + 0.5) / max_batches
|
| 282 |
+
)
|
| 283 |
+
with autocast_context(device, precision):
|
| 284 |
+
loss, metrics = _objective_step(
|
| 285 |
+
model, tokens, regions, experiment, eval_mask_level=level
|
| 286 |
+
)
|
| 287 |
+
totals['loss'] = totals.get('loss', 0.0) + float(loss)
|
| 288 |
+
for key, value in metrics.items():
|
| 289 |
+
totals[key] = totals.get(key, 0.0) + value
|
| 290 |
+
batches += 1
|
| 291 |
+
return {key: value / max(1, batches) for key, value in totals.items()}
|
| 292 |
+
finally:
|
| 293 |
+
restore_rng_state(state)
|
| 294 |
+
model.train(was_training)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def _save_checkpoint(
|
| 298 |
+
output_dir: Path,
|
| 299 |
+
model: DiffusionTransformer,
|
| 300 |
+
optimizer: torch.optim.Optimizer,
|
| 301 |
+
scaler: Any,
|
| 302 |
+
experiment: ReasoningExperiment,
|
| 303 |
+
step: int,
|
| 304 |
+
micro_batches_seen: int,
|
| 305 |
+
data_generator: torch.Generator,
|
| 306 |
+
) -> Path:
|
| 307 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 308 |
+
payload = {
|
| 309 |
+
'format': REASONING_CHECKPOINT_FORMAT,
|
| 310 |
+
'step': step,
|
| 311 |
+
'micro_batches_seen': micro_batches_seen,
|
| 312 |
+
'config': experiment.to_dict(),
|
| 313 |
+
'tokenizer_sha256': hashlib.sha256(
|
| 314 |
+
Path(experiment.training.tokenizer).read_bytes()
|
| 315 |
+
).hexdigest(),
|
| 316 |
+
'rng_state': capture_rng_state(next(model.parameters()).device),
|
| 317 |
+
'data_generator_state': data_generator.get_state(),
|
| 318 |
+
'model': model.state_dict(),
|
| 319 |
+
'optimizer': optimizer.state_dict(),
|
| 320 |
+
'scaler': scaler.state_dict(),
|
| 321 |
+
}
|
| 322 |
+
path = output_dir / 'latest.pt'
|
| 323 |
+
temporary = output_dir / '.checkpoint.tmp'
|
| 324 |
+
torch.save(payload, temporary)
|
| 325 |
+
temporary.replace(path)
|
| 326 |
+
|
| 327 |
+
inference_payload = {
|
| 328 |
+
'format': 'mini-diffusion-lm-reasoning-inference-v1',
|
| 329 |
+
'step': step,
|
| 330 |
+
'config': experiment.to_dict(),
|
| 331 |
+
'tokenizer_sha256': payload['tokenizer_sha256'],
|
| 332 |
+
'model': _inference_state_dict(model),
|
| 333 |
+
}
|
| 334 |
+
inference_temporary = output_dir / '.inference.tmp'
|
| 335 |
+
torch.save(inference_payload, inference_temporary)
|
| 336 |
+
inference_temporary.replace(output_dir / 'inference-latest.pt')
|
| 337 |
+
return path
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _model_architecture(config: ModelConfig) -> dict[str, Any]:
|
| 341 |
+
"""Config payload compared on resume; pretrained_path is machine-local and may move."""
|
| 342 |
+
|
| 343 |
+
fields = asdict(config)
|
| 344 |
+
fields.pop('pretrained_path', None)
|
| 345 |
+
return fields
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _load_init_weights(model: DiffusionTransformer, path: str | Path) -> None:
|
| 349 |
+
"""Initialize model weights from any project checkpoint, ignoring optimizer state.
|
| 350 |
+
|
| 351 |
+
Enables the pretrain-AR-then-adapt recipe: architectures must match tensor-wise,
|
| 352 |
+
while objective-level settings (forbidden outputs, objective) may differ.
|
| 353 |
+
"""
|
| 354 |
+
|
| 355 |
+
checkpoint = torch.load(path, map_location='cpu', weights_only=False)
|
| 356 |
+
state = checkpoint.get('model')
|
| 357 |
+
if state is None:
|
| 358 |
+
raise ValueError(f'{path} does not contain model weights')
|
| 359 |
+
model.load_state_dict(state)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def train(
|
| 363 |
+
experiment: ReasoningExperiment,
|
| 364 |
+
resume: str | Path | None = None,
|
| 365 |
+
max_run_steps: int | None = None,
|
| 366 |
+
init_weights: str | Path | None = None,
|
| 367 |
+
) -> Path:
|
| 368 |
+
config = experiment.training
|
| 369 |
+
device = resolve_device(config.device)
|
| 370 |
+
seed_everything(config.seed, device)
|
| 371 |
+
configure_cuda_backends(device, config.require_fused_attention)
|
| 372 |
+
precision = resolve_precision(config.precision, device)
|
| 373 |
+
|
| 374 |
+
is_packed = experiment.reasoning.objective in PACKED_OBJECTIVES
|
| 375 |
+
|
| 376 |
+
def _open_dataset(path: str):
|
| 377 |
+
if is_packed:
|
| 378 |
+
return load_packed_dataset(path, experiment.model.max_seq_len)
|
| 379 |
+
return ReasoningTokenDataset(path)
|
| 380 |
+
|
| 381 |
+
train_dataset = _open_dataset(config.train_data)
|
| 382 |
+
datasets = [train_dataset]
|
| 383 |
+
val_loader = None
|
| 384 |
+
if config.val_data is not None:
|
| 385 |
+
val_dataset = _open_dataset(config.val_data)
|
| 386 |
+
datasets.append(val_dataset)
|
| 387 |
+
val_loader = DataLoader(
|
| 388 |
+
val_dataset,
|
| 389 |
+
batch_size=config.batch_size,
|
| 390 |
+
shuffle=False,
|
| 391 |
+
num_workers=config.num_workers,
|
| 392 |
+
pin_memory=device.type == 'cuda',
|
| 393 |
+
)
|
| 394 |
+
_validate_inputs(experiment, datasets)
|
| 395 |
+
|
| 396 |
+
data_generator = torch.Generator().manual_seed(config.seed)
|
| 397 |
+
batch_sampler = DeterministicBatchSampler(
|
| 398 |
+
len(train_dataset), config.batch_size, seed=config.seed
|
| 399 |
+
)
|
| 400 |
+
train_loader = DataLoader(
|
| 401 |
+
train_dataset,
|
| 402 |
+
batch_sampler=batch_sampler,
|
| 403 |
+
num_workers=config.num_workers,
|
| 404 |
+
pin_memory=device.type == 'cuda',
|
| 405 |
+
generator=data_generator,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
load_pretrained = resume is None and init_weights is None
|
| 409 |
+
# fp32 master weights regardless of the checkpoint's serialized dtype (transformers 5
|
| 410 |
+
# defaults to 'auto'/bf16); compute precision comes from autocast like every project run.
|
| 411 |
+
model = build_denoiser(
|
| 412 |
+
experiment.model, load_pretrained=load_pretrained, dtype=torch.float32
|
| 413 |
+
).to(device)
|
| 414 |
+
if not is_packed:
|
| 415 |
+
model.reasoning_block = int(train_dataset.metadata['block'])
|
| 416 |
+
if experiment.reasoning.adaptive:
|
| 417 |
+
metadata = train_dataset.metadata
|
| 418 |
+
if 'size_token_ids' not in metadata:
|
| 419 |
+
raise ValueError(f'{config.train_data} lacks size_token_ids; rebuild it adaptively')
|
| 420 |
+
model.adaptive_size_ids = tuple(sorted(int(v) for v in metadata['size_token_ids'].values()))
|
| 421 |
+
model.adaptive_end_think_id = int(metadata['reasoning_token_ids']['end_think'])
|
| 422 |
+
if init_weights is not None:
|
| 423 |
+
if resume is not None:
|
| 424 |
+
raise ValueError('init_weights and resume are mutually exclusive')
|
| 425 |
+
_load_init_weights(model, init_weights)
|
| 426 |
+
print(json.dumps({'event': 'init_weights', 'path': str(init_weights)}))
|
| 427 |
+
optimizer = build_optimizer(model, config)
|
| 428 |
+
scaler = create_grad_scaler(device.type == 'cuda' and precision == 'float16')
|
| 429 |
+
|
| 430 |
+
start_step = 0
|
| 431 |
+
micro_batches_seen = 0
|
| 432 |
+
if resume is not None:
|
| 433 |
+
checkpoint = torch.load(resume, map_location='cpu', weights_only=False)
|
| 434 |
+
if checkpoint.get('format') != REASONING_CHECKPOINT_FORMAT:
|
| 435 |
+
raise ValueError('unsupported checkpoint format')
|
| 436 |
+
if _model_architecture(
|
| 437 |
+
ModelConfig(**checkpoint['config']['model'])
|
| 438 |
+
) != _model_architecture(experiment.model):
|
| 439 |
+
raise ValueError('checkpoint model configuration does not match the config')
|
| 440 |
+
model.load_state_dict(checkpoint['model'])
|
| 441 |
+
optimizer.load_state_dict(checkpoint['optimizer'])
|
| 442 |
+
scaler.load_state_dict(checkpoint.get('scaler', {}))
|
| 443 |
+
data_generator.set_state(checkpoint['data_generator_state'].cpu())
|
| 444 |
+
restore_rng_state(checkpoint['rng_state'])
|
| 445 |
+
start_step = int(checkpoint['step']) + 1
|
| 446 |
+
micro_batches_seen = int(checkpoint['micro_batches_seen'])
|
| 447 |
+
del checkpoint
|
| 448 |
+
|
| 449 |
+
batch_sampler.start_batch = micro_batches_seen
|
| 450 |
+
train_iterator = iter(train_loader)
|
| 451 |
+
|
| 452 |
+
output_dir = Path(config.output_dir)
|
| 453 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 454 |
+
with (output_dir / 'config.json').open('w', encoding='utf-8') as handle:
|
| 455 |
+
json.dump(experiment.to_dict(), handle, indent=2)
|
| 456 |
+
handle.write('\n')
|
| 457 |
+
|
| 458 |
+
print(
|
| 459 |
+
json.dumps(
|
| 460 |
+
{
|
| 461 |
+
'event': 'start',
|
| 462 |
+
'objective': experiment.reasoning.objective,
|
| 463 |
+
'device': str(device),
|
| 464 |
+
'precision': precision,
|
| 465 |
+
'parameters': model.num_parameters,
|
| 466 |
+
'parameters_human': format_parameter_count(model.num_parameters),
|
| 467 |
+
'training_examples': len(train_dataset),
|
| 468 |
+
'start_step': start_step,
|
| 469 |
+
}
|
| 470 |
+
)
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
model.train()
|
| 474 |
+
log_started = time.perf_counter()
|
| 475 |
+
log_loss = 0.0
|
| 476 |
+
log_metrics: dict[str, float] = {}
|
| 477 |
+
log_count = 0
|
| 478 |
+
end_step = config.max_steps
|
| 479 |
+
if max_run_steps is not None:
|
| 480 |
+
end_step = min(end_step, start_step + max_run_steps)
|
| 481 |
+
last_checkpoint = output_dir / 'latest.pt'
|
| 482 |
+
|
| 483 |
+
for step in range(start_step, end_step):
|
| 484 |
+
lr = learning_rate(step, config)
|
| 485 |
+
for group in optimizer.param_groups:
|
| 486 |
+
group['lr'] = lr
|
| 487 |
+
optimizer.zero_grad(set_to_none=True)
|
| 488 |
+
|
| 489 |
+
for _ in range(config.gradient_accumulation_steps):
|
| 490 |
+
batch = next(train_iterator)
|
| 491 |
+
if is_packed:
|
| 492 |
+
tokens, regions = batch, None
|
| 493 |
+
else:
|
| 494 |
+
tokens, regions = batch
|
| 495 |
+
regions = regions.to(device, non_blocking=True)
|
| 496 |
+
tokens = tokens.to(device, non_blocking=True)
|
| 497 |
+
micro_batches_seen += 1
|
| 498 |
+
with autocast_context(device, precision):
|
| 499 |
+
loss, metrics = _objective_step(model, tokens, regions, experiment, step=step)
|
| 500 |
+
scaled = loss / config.gradient_accumulation_steps
|
| 501 |
+
scaler.scale(scaled).backward()
|
| 502 |
+
log_loss += float(loss) / config.gradient_accumulation_steps
|
| 503 |
+
for key, value in metrics.items():
|
| 504 |
+
log_metrics[key] = (
|
| 505 |
+
log_metrics.get(key, 0.0) + value / config.gradient_accumulation_steps
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
scaler.unscale_(optimizer)
|
| 509 |
+
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_clip)
|
| 510 |
+
scaler.step(optimizer)
|
| 511 |
+
scaler.update()
|
| 512 |
+
log_count += 1
|
| 513 |
+
|
| 514 |
+
if (step + 1) % config.log_interval == 0:
|
| 515 |
+
elapsed = time.perf_counter() - log_started
|
| 516 |
+
payload = {
|
| 517 |
+
'event': 'train',
|
| 518 |
+
'step': step + 1,
|
| 519 |
+
'loss': log_loss / max(1, log_count),
|
| 520 |
+
'learning_rate': lr,
|
| 521 |
+
'grad_norm': float(grad_norm),
|
| 522 |
+
'steps_per_second': log_count / max(elapsed, 1e-9),
|
| 523 |
+
}
|
| 524 |
+
payload.update(
|
| 525 |
+
{key: value / max(1, log_count) for key, value in log_metrics.items()}
|
| 526 |
+
)
|
| 527 |
+
print(json.dumps(payload))
|
| 528 |
+
log_started = time.perf_counter()
|
| 529 |
+
log_loss = 0.0
|
| 530 |
+
log_metrics = {}
|
| 531 |
+
log_count = 0
|
| 532 |
+
|
| 533 |
+
if val_loader is not None and (step + 1) % config.eval_interval == 0:
|
| 534 |
+
metrics = _evaluate(model, val_loader, experiment, device, precision)
|
| 535 |
+
print(json.dumps({'event': 'validation', 'step': step + 1, **metrics}))
|
| 536 |
+
|
| 537 |
+
if (step + 1) % config.save_interval == 0:
|
| 538 |
+
last_checkpoint = _save_checkpoint(
|
| 539 |
+
output_dir,
|
| 540 |
+
model,
|
| 541 |
+
optimizer,
|
| 542 |
+
scaler,
|
| 543 |
+
experiment,
|
| 544 |
+
step,
|
| 545 |
+
micro_batches_seen,
|
| 546 |
+
data_generator,
|
| 547 |
+
)
|
| 548 |
+
print(json.dumps({'event': 'checkpoint', 'path': str(last_checkpoint)}))
|
| 549 |
+
|
| 550 |
+
final_step = end_step - 1
|
| 551 |
+
if not last_checkpoint.exists() or (final_step + 1) % config.save_interval != 0:
|
| 552 |
+
last_checkpoint = _save_checkpoint(
|
| 553 |
+
output_dir,
|
| 554 |
+
model,
|
| 555 |
+
optimizer,
|
| 556 |
+
scaler,
|
| 557 |
+
experiment,
|
| 558 |
+
final_step,
|
| 559 |
+
micro_batches_seen,
|
| 560 |
+
data_generator,
|
| 561 |
+
)
|
| 562 |
+
event = 'complete' if end_step == config.max_steps else 'paused'
|
| 563 |
+
print(json.dumps({'event': event, 'checkpoint': str(last_checkpoint)}))
|
| 564 |
+
return last_checkpoint
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def main() -> None:
|
| 568 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 569 |
+
parser.add_argument('--config', type=Path, required=True)
|
| 570 |
+
parser.add_argument('--resume', type=Path)
|
| 571 |
+
parser.add_argument('--init-weights', type=Path)
|
| 572 |
+
parser.add_argument('--max-run-steps', type=int)
|
| 573 |
+
parser.add_argument('--device')
|
| 574 |
+
parser.add_argument(
|
| 575 |
+
'--precision', choices=('auto', 'float32', 'bfloat16', 'float16')
|
| 576 |
+
)
|
| 577 |
+
args = parser.parse_args()
|
| 578 |
+
|
| 579 |
+
experiment = load_reasoning_config(args.config)
|
| 580 |
+
if args.device or args.precision:
|
| 581 |
+
from dataclasses import replace
|
| 582 |
+
|
| 583 |
+
training = replace(
|
| 584 |
+
experiment.training,
|
| 585 |
+
device=args.device or experiment.training.device,
|
| 586 |
+
precision=args.precision or experiment.training.precision,
|
| 587 |
+
)
|
| 588 |
+
experiment = ReasoningExperiment(
|
| 589 |
+
model=experiment.model, training=training, reasoning=experiment.reasoning
|
| 590 |
+
)
|
| 591 |
+
train(
|
| 592 |
+
experiment,
|
| 593 |
+
resume=args.resume,
|
| 594 |
+
max_run_steps=args.max_run_steps,
|
| 595 |
+
init_weights=args.init_weights,
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
if __name__ == '__main__':
|
| 600 |
+
main()
|
src/diffusion_lm/sample.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate fixed-length text by iteratively unmasking tokens."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import gc
|
| 7 |
+
import hashlib
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from diffusion_lm.config import ModelConfig
|
| 13 |
+
from diffusion_lm.diffusion import iterative_unmask
|
| 14 |
+
from diffusion_lm.model import DiffusionTransformer
|
| 15 |
+
from diffusion_lm.tokenizer import load_tokenizer, special_token_id, special_token_ids
|
| 16 |
+
from diffusion_lm.train import resolve_device
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_model(checkpoint_path: str | Path, device: torch.device) -> DiffusionTransformer:
|
| 20 |
+
try:
|
| 21 |
+
# mmap keeps unused optimizer tensors in a full training checkpoint off
|
| 22 |
+
# resident RAM. The compact inference export remains the preferred input.
|
| 23 |
+
checkpoint = torch.load(
|
| 24 |
+
checkpoint_path,
|
| 25 |
+
map_location="cpu",
|
| 26 |
+
weights_only=False,
|
| 27 |
+
mmap=True,
|
| 28 |
+
)
|
| 29 |
+
except TypeError: # PyTorch versions before mmap= support.
|
| 30 |
+
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 31 |
+
checkpoint_format = checkpoint.get("format")
|
| 32 |
+
if checkpoint_format not in {
|
| 33 |
+
"mini-diffusion-lm-checkpoint-v1",
|
| 34 |
+
"mini-diffusion-lm-inference-v1",
|
| 35 |
+
}:
|
| 36 |
+
raise ValueError("unsupported checkpoint format")
|
| 37 |
+
model_config = ModelConfig(**checkpoint["config"]["model"])
|
| 38 |
+
model = DiffusionTransformer(model_config)
|
| 39 |
+
if checkpoint_format == "mini-diffusion-lm-inference-v1" and device.type != "cpu":
|
| 40 |
+
# The weights-only export is BF16. Keep that dtype on accelerators instead
|
| 41 |
+
# of silently expanding a 1B model back to FP32 during load_state_dict.
|
| 42 |
+
first_weight = next(iter(checkpoint["model"].values()))
|
| 43 |
+
if first_weight.is_floating_point():
|
| 44 |
+
model = model.to(device=device, dtype=first_weight.dtype)
|
| 45 |
+
model.load_state_dict(checkpoint["model"])
|
| 46 |
+
model.tokenizer_sha256 = checkpoint.get("tokenizer_sha256")
|
| 47 |
+
del checkpoint
|
| 48 |
+
gc.collect()
|
| 49 |
+
return model.to(device).eval()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def generate(
|
| 53 |
+
model: DiffusionTransformer,
|
| 54 |
+
tokenizer_path: str | Path,
|
| 55 |
+
*,
|
| 56 |
+
prompt: str = "",
|
| 57 |
+
generation_length: int = 128,
|
| 58 |
+
num_samples: int = 1,
|
| 59 |
+
steps: int = 64,
|
| 60 |
+
temperature: float = 1.0,
|
| 61 |
+
strategy: str = "ancestral",
|
| 62 |
+
seed: int = 1337,
|
| 63 |
+
) -> list[str]:
|
| 64 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 65 |
+
tokenizer_hash = hashlib.sha256(Path(tokenizer_path).read_bytes()).hexdigest()
|
| 66 |
+
if model.tokenizer_sha256 is not None and tokenizer_hash != model.tokenizer_sha256:
|
| 67 |
+
raise ValueError("tokenizer file does not match the tokenizer used for training")
|
| 68 |
+
if tokenizer.get_vocab_size(with_added_tokens=True) != model.config.vocab_size:
|
| 69 |
+
raise ValueError("tokenizer vocabulary does not match the checkpoint")
|
| 70 |
+
mask_id = special_token_id(tokenizer, "mask")
|
| 71 |
+
if mask_id != model.config.mask_token_id:
|
| 72 |
+
raise ValueError("tokenizer mask id does not match the checkpoint")
|
| 73 |
+
if generation_length <= 0 or num_samples <= 0:
|
| 74 |
+
raise ValueError("generation_length and num_samples must be positive")
|
| 75 |
+
|
| 76 |
+
prompt_ids = tokenizer.encode(prompt).ids if prompt else []
|
| 77 |
+
total_length = len(prompt_ids) + generation_length
|
| 78 |
+
if total_length > model.config.max_seq_len:
|
| 79 |
+
raise ValueError(
|
| 80 |
+
f"prompt plus generation uses {total_length} tokens, but model limit is "
|
| 81 |
+
f"{model.config.max_seq_len}"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
device = next(model.parameters()).device
|
| 85 |
+
input_ids = torch.full(
|
| 86 |
+
(num_samples, total_length),
|
| 87 |
+
model.config.mask_token_id,
|
| 88 |
+
dtype=torch.long,
|
| 89 |
+
device=device,
|
| 90 |
+
)
|
| 91 |
+
if prompt_ids:
|
| 92 |
+
input_ids[:, : len(prompt_ids)] = torch.tensor(prompt_ids, device=device)
|
| 93 |
+
|
| 94 |
+
torch.manual_seed(seed)
|
| 95 |
+
if device.type == "cuda":
|
| 96 |
+
torch.cuda.manual_seed_all(seed)
|
| 97 |
+
elif device.type == "mps" and hasattr(torch.mps, "manual_seed"):
|
| 98 |
+
torch.mps.manual_seed(seed)
|
| 99 |
+
role_ids = special_token_ids(tokenizer)
|
| 100 |
+
blocked = tuple(role_ids[role] for role in ("pad", "unk", "bos", "mask"))
|
| 101 |
+
result = iterative_unmask(
|
| 102 |
+
model,
|
| 103 |
+
input_ids,
|
| 104 |
+
model.config.mask_token_id,
|
| 105 |
+
steps=steps,
|
| 106 |
+
temperature=temperature,
|
| 107 |
+
strategy=strategy, # type: ignore[arg-type]
|
| 108 |
+
blocked_token_ids=blocked,
|
| 109 |
+
).cpu()
|
| 110 |
+
|
| 111 |
+
eos_id = special_token_id(tokenizer, "eos")
|
| 112 |
+
texts: list[str] = []
|
| 113 |
+
for row in result.tolist():
|
| 114 |
+
if eos_id in row[len(prompt_ids) :]:
|
| 115 |
+
eos_position = row.index(eos_id, len(prompt_ids))
|
| 116 |
+
row = row[:eos_position]
|
| 117 |
+
texts.append(tokenizer.decode(row, skip_special_tokens=True))
|
| 118 |
+
return texts
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def main() -> None:
|
| 122 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 123 |
+
parser.add_argument("--checkpoint", type=Path, required=True)
|
| 124 |
+
parser.add_argument("--tokenizer", type=Path, required=True)
|
| 125 |
+
parser.add_argument("--prompt", default="")
|
| 126 |
+
parser.add_argument("--length", type=int, default=128, help="number of completion tokens")
|
| 127 |
+
parser.add_argument("--num-samples", type=int, default=1)
|
| 128 |
+
parser.add_argument("--steps", type=int, default=64)
|
| 129 |
+
parser.add_argument("--temperature", type=float, default=1.0)
|
| 130 |
+
parser.add_argument("--strategy", choices=("ancestral", "confidence"), default="ancestral")
|
| 131 |
+
parser.add_argument("--seed", type=int, default=1337)
|
| 132 |
+
parser.add_argument("--device", default="auto")
|
| 133 |
+
args = parser.parse_args()
|
| 134 |
+
|
| 135 |
+
device = resolve_device(args.device)
|
| 136 |
+
model = load_model(args.checkpoint, device)
|
| 137 |
+
texts = generate(
|
| 138 |
+
model,
|
| 139 |
+
args.tokenizer,
|
| 140 |
+
prompt=args.prompt,
|
| 141 |
+
generation_length=args.length,
|
| 142 |
+
num_samples=args.num_samples,
|
| 143 |
+
steps=args.steps,
|
| 144 |
+
temperature=args.temperature,
|
| 145 |
+
strategy=args.strategy,
|
| 146 |
+
seed=args.seed,
|
| 147 |
+
)
|
| 148 |
+
for index, text in enumerate(texts, start=1):
|
| 149 |
+
print(f"[{index}] {text}")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
main()
|
src/diffusion_lm/stories.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyStories preparation: AR-pretraining stream and instruct data with thought slots.
|
| 2 |
+
|
| 3 |
+
Two products over one shared tokenizer:
|
| 4 |
+
|
| 5 |
+
- ``prepare-pretrain``: TinyStoriesV2 stories packed as a continuous EOS-separated
|
| 6 |
+
uint16 stream for causal-LM pretraining.
|
| 7 |
+
- ``prepare-instruct``: TinyStories-Instruct records rendered with the reasoning
|
| 8 |
+
slot geometry — prompt holds the writing instruction, thought slots hold the
|
| 9 |
+
requirement restatements plus the story plan (the summary, which never appears
|
| 10 |
+
in the prompt), and the answer region holds the story.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import hashlib
|
| 17 |
+
import json
|
| 18 |
+
import re
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Iterator
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
from diffusion_lm.reasoning import (
|
| 25 |
+
ExampleEncoder,
|
| 26 |
+
LayoutSpec,
|
| 27 |
+
ReasoningExample,
|
| 28 |
+
REASONING_SPECIAL_TOKENS,
|
| 29 |
+
_write_packed,
|
| 30 |
+
)
|
| 31 |
+
from diffusion_lm.tokenizer import (
|
| 32 |
+
load_tokenizer,
|
| 33 |
+
special_token_ids,
|
| 34 |
+
train_tokenizer_from_iterator,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
_FIELD_RE = re.compile(r'^(Features|Words|Summary|Random sentence):\s*(.+)$', re.MULTILINE)
|
| 38 |
+
_STORY_RE = re.compile(r'^Story:\s*$', re.MULTILINE)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def iter_stories(path: Path) -> Iterator[str]:
|
| 42 |
+
"""Yield individual stories from an ``<|endoftext|>``-separated text file."""
|
| 43 |
+
|
| 44 |
+
buffer = ''
|
| 45 |
+
with path.open('r', encoding='utf-8') as handle:
|
| 46 |
+
while True:
|
| 47 |
+
chunk = handle.read(1 << 24)
|
| 48 |
+
if not chunk:
|
| 49 |
+
break
|
| 50 |
+
buffer += chunk
|
| 51 |
+
*complete, buffer = buffer.split('<|endoftext|>')
|
| 52 |
+
for piece in complete:
|
| 53 |
+
story = piece.strip()
|
| 54 |
+
if story:
|
| 55 |
+
yield story
|
| 56 |
+
tail = buffer.strip()
|
| 57 |
+
if tail:
|
| 58 |
+
yield tail
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def iter_instruct_records(path: Path) -> Iterator[tuple[dict[str, str], str]]:
|
| 62 |
+
"""Yield ``(fields, story)`` pairs from a TinyStories-Instruct dump."""
|
| 63 |
+
|
| 64 |
+
for record in iter_stories(path):
|
| 65 |
+
story_match = _STORY_RE.search(record)
|
| 66 |
+
if not story_match:
|
| 67 |
+
continue
|
| 68 |
+
header = record[: story_match.start()]
|
| 69 |
+
story = record[story_match.end():].strip()
|
| 70 |
+
fields = dict(_FIELD_RE.findall(header))
|
| 71 |
+
if story and fields:
|
| 72 |
+
yield fields, story
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def instruct_example(fields: dict[str, str], story: str) -> ReasoningExample | None:
|
| 76 |
+
"""Render one record as prompt, thought steps, and the story answer.
|
| 77 |
+
|
| 78 |
+
The summary becomes plan thoughts and is deliberately excluded from the
|
| 79 |
+
prompt, so planning is generative rather than copyable.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
summary = ' '.join(fields.get('Summary', '').split())
|
| 83 |
+
if not summary:
|
| 84 |
+
return None
|
| 85 |
+
prompt_parts = ['Write a short story.']
|
| 86 |
+
thoughts: list[str] = []
|
| 87 |
+
features = ' '.join(fields.get('Features', '').split())
|
| 88 |
+
words = ' '.join(fields.get('Words', '').split())
|
| 89 |
+
sentence = ' '.join(fields.get('Random sentence', '').split())
|
| 90 |
+
if features:
|
| 91 |
+
prompt_parts.append(f'It should feature: {features}.')
|
| 92 |
+
thoughts.append(f'The story needs these elements: {features}.')
|
| 93 |
+
if words:
|
| 94 |
+
prompt_parts.append(f'Use the words: {words}.')
|
| 95 |
+
thoughts.append(f'I have to work in the words {words}.')
|
| 96 |
+
if sentence:
|
| 97 |
+
prompt_parts.append(f'Include the sentence: {sentence}')
|
| 98 |
+
thoughts.append(f'The sentence "{sentence}" must appear.')
|
| 99 |
+
thoughts.append(f'Plan: {summary}')
|
| 100 |
+
story = '\n'.join(line.strip() for line in story.splitlines() if line.strip())
|
| 101 |
+
return ReasoningExample(
|
| 102 |
+
problem=' '.join(prompt_parts),
|
| 103 |
+
steps=tuple(thoughts),
|
| 104 |
+
answer=story,
|
| 105 |
+
expected_answer='',
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def prepare_pretrain(args: argparse.Namespace) -> None:
|
| 110 |
+
tokenizer_path = Path(args.tokenizer)
|
| 111 |
+
if tokenizer_path.is_file():
|
| 112 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 113 |
+
print(f'reusing tokenizer {tokenizer_path}')
|
| 114 |
+
else:
|
| 115 |
+
def _texts() -> Iterator[str]:
|
| 116 |
+
for index, story in enumerate(iter_stories(Path(args.train))):
|
| 117 |
+
if index >= args.tokenizer_sample:
|
| 118 |
+
break
|
| 119 |
+
yield story
|
| 120 |
+
|
| 121 |
+
tokenizer = train_tokenizer_from_iterator(
|
| 122 |
+
_texts(),
|
| 123 |
+
tokenizer_path,
|
| 124 |
+
vocab_size=args.vocab_size,
|
| 125 |
+
min_frequency=4,
|
| 126 |
+
length=args.tokenizer_sample,
|
| 127 |
+
extra_special_tokens=REASONING_SPECIAL_TOKENS,
|
| 128 |
+
)
|
| 129 |
+
print(f'trained tokenizer {tokenizer_path}')
|
| 130 |
+
|
| 131 |
+
eos_id = special_token_ids(tokenizer)['eos']
|
| 132 |
+
output_dir = Path(args.output_dir)
|
| 133 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 134 |
+
for split, source in (('train', args.train), ('validation', args.validation)):
|
| 135 |
+
source_path = Path(source)
|
| 136 |
+
out_path = output_dir / f'{split}.bin'
|
| 137 |
+
token_count = 0
|
| 138 |
+
document_count = 0
|
| 139 |
+
batch: list[str] = []
|
| 140 |
+
with out_path.open('wb') as destination:
|
| 141 |
+
def _flush(batch: list[str]) -> tuple[int, int]:
|
| 142 |
+
encodings = tokenizer.encode_batch(batch, add_special_tokens=False)
|
| 143 |
+
tokens = 0
|
| 144 |
+
for encoding in encodings:
|
| 145 |
+
ids = encoding.ids + [eos_id]
|
| 146 |
+
np.asarray(ids, dtype=np.uint16).tofile(destination)
|
| 147 |
+
tokens += len(ids)
|
| 148 |
+
return tokens, len(encodings)
|
| 149 |
+
|
| 150 |
+
for story in iter_stories(source_path):
|
| 151 |
+
batch.append(story)
|
| 152 |
+
if len(batch) >= 2048:
|
| 153 |
+
tokens, docs = _flush(batch)
|
| 154 |
+
token_count += tokens
|
| 155 |
+
document_count += docs
|
| 156 |
+
batch = []
|
| 157 |
+
if batch:
|
| 158 |
+
tokens, docs = _flush(batch)
|
| 159 |
+
token_count += tokens
|
| 160 |
+
document_count += docs
|
| 161 |
+
|
| 162 |
+
metadata = {
|
| 163 |
+
'format': 'mini-diffusion-lm-packed-tokens-v1',
|
| 164 |
+
'dtype': 'uint16',
|
| 165 |
+
'token_count': token_count,
|
| 166 |
+
'document_count': document_count,
|
| 167 |
+
'vocab_size': tokenizer.get_vocab_size(with_added_tokens=True),
|
| 168 |
+
'mask_token_id': special_token_ids(tokenizer)['mask'],
|
| 169 |
+
'eos_token_id': eos_id,
|
| 170 |
+
'special_token_ids': special_token_ids(tokenizer),
|
| 171 |
+
'tokenizer_sha256': hashlib.sha256(tokenizer_path.read_bytes()).hexdigest(),
|
| 172 |
+
'source_files': [str(source_path)],
|
| 173 |
+
}
|
| 174 |
+
with (out_path.parent / f'{out_path.name}.json').open('w', encoding='utf-8') as handle:
|
| 175 |
+
json.dump(metadata, handle, indent=2)
|
| 176 |
+
handle.write('\n')
|
| 177 |
+
print(f'{split}: {document_count:,} stories, {token_count:,} tokens -> {out_path}')
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def prepare_instruct(args: argparse.Namespace) -> None:
|
| 181 |
+
tokenizer = load_tokenizer(Path(args.tokenizer))
|
| 182 |
+
spec = LayoutSpec(seq_len=args.seq_len, block=args.block, max_slots=args.max_slots)
|
| 183 |
+
encoder = ExampleEncoder(tokenizer, spec)
|
| 184 |
+
output_dir = Path(args.output_dir)
|
| 185 |
+
|
| 186 |
+
for split, source in (('train', args.train), ('validation', args.validation)):
|
| 187 |
+
flat, flat_regions, slotted, slotted_regions = [], [], [], []
|
| 188 |
+
prompts: list[dict[str, str]] = []
|
| 189 |
+
dropped = 0
|
| 190 |
+
count = 0
|
| 191 |
+
for fields, story in iter_instruct_records(Path(source)):
|
| 192 |
+
if args.limit and count >= args.limit:
|
| 193 |
+
break
|
| 194 |
+
example = instruct_example(fields, story)
|
| 195 |
+
if example is None:
|
| 196 |
+
dropped += 1
|
| 197 |
+
continue
|
| 198 |
+
encoded = encoder.encode_example(example)
|
| 199 |
+
if encoded is None:
|
| 200 |
+
dropped += 1
|
| 201 |
+
continue
|
| 202 |
+
count += 1
|
| 203 |
+
flat.append(encoded.flat)
|
| 204 |
+
flat_regions.append(encoded.flat_regions)
|
| 205 |
+
slotted.append(encoded.slotted)
|
| 206 |
+
slotted_regions.append(encoded.slotted_regions)
|
| 207 |
+
if split == 'validation' and len(prompts) < 500:
|
| 208 |
+
prompts.append({'problem': example.problem, 'expected_answer': ''})
|
| 209 |
+
if not flat:
|
| 210 |
+
raise ValueError(f'no usable records in {source}')
|
| 211 |
+
for layout, tokens, regions in (
|
| 212 |
+
('flat', flat, flat_regions),
|
| 213 |
+
('slotted', slotted, slotted_regions),
|
| 214 |
+
):
|
| 215 |
+
_write_packed(
|
| 216 |
+
output_dir / f'{split}-{layout}.bin',
|
| 217 |
+
np.stack(tokens),
|
| 218 |
+
np.stack(regions),
|
| 219 |
+
layout=layout,
|
| 220 |
+
spec=spec,
|
| 221 |
+
tokenizer_path=Path(args.tokenizer),
|
| 222 |
+
tokenizer=tokenizer,
|
| 223 |
+
)
|
| 224 |
+
print(f'{split}: {count:,} examples ({dropped:,} dropped) -> {output_dir}')
|
| 225 |
+
if split == 'validation':
|
| 226 |
+
with (output_dir / 'validation-problems.jsonl').open('w', encoding='utf-8') as fh:
|
| 227 |
+
for record in prompts:
|
| 228 |
+
fh.write(json.dumps(record, ensure_ascii=False) + '\n')
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 232 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 233 |
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
| 234 |
+
|
| 235 |
+
pretrain = subparsers.add_parser('prepare-pretrain')
|
| 236 |
+
pretrain.add_argument('--train', required=True)
|
| 237 |
+
pretrain.add_argument('--validation', required=True)
|
| 238 |
+
pretrain.add_argument('--output-dir', required=True)
|
| 239 |
+
pretrain.add_argument('--tokenizer', required=True)
|
| 240 |
+
pretrain.add_argument('--vocab-size', type=int, default=8192)
|
| 241 |
+
pretrain.add_argument('--tokenizer-sample', type=int, default=400_000)
|
| 242 |
+
|
| 243 |
+
instruct = subparsers.add_parser('prepare-instruct')
|
| 244 |
+
instruct.add_argument('--train', required=True)
|
| 245 |
+
instruct.add_argument('--validation', required=True)
|
| 246 |
+
instruct.add_argument('--output-dir', required=True)
|
| 247 |
+
instruct.add_argument('--tokenizer', required=True)
|
| 248 |
+
instruct.add_argument('--seq-len', type=int, default=768)
|
| 249 |
+
instruct.add_argument('--block', type=int, default=32)
|
| 250 |
+
instruct.add_argument('--max-slots', type=int, default=9)
|
| 251 |
+
instruct.add_argument('--limit', type=int, default=0)
|
| 252 |
+
return parser
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def main() -> None:
|
| 256 |
+
args = _build_parser().parse_args()
|
| 257 |
+
if args.command == 'prepare-pretrain':
|
| 258 |
+
prepare_pretrain(args)
|
| 259 |
+
elif args.command == 'prepare-instruct':
|
| 260 |
+
prepare_instruct(args)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
if __name__ == '__main__':
|
| 264 |
+
main()
|
src/diffusion_lm/tokenizer.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train byte-level BPE tokenizers and encode packed token files."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Iterable, Literal, Protocol, TypeAlias
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from tokenizers import Tokenizer, decoders, models, normalizers, pre_tokenizers, trainers
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
SpecialTokenRole: TypeAlias = Literal["pad", "unk", "bos", "eos", "mask"]
|
| 17 |
+
SPECIAL_TOKEN_ROLES: tuple[SpecialTokenRole, ...] = ("pad", "unk", "bos", "eos", "mask")
|
| 18 |
+
|
| 19 |
+
# These sentinels deliberately include a project-specific random namespace. Common names such as
|
| 20 |
+
# ``[MASK]`` occur in documentation and source code often enough to stop a web-scale encoder.
|
| 21 |
+
_SPECIAL_TOKEN_NAMESPACE = "mdlm-v1-8f4e2a6c-917b-4d31-b3f7-6a2c8d5e0f19"
|
| 22 |
+
SPECIAL_TOKENS = tuple(
|
| 23 |
+
f"<|{_SPECIAL_TOKEN_NAMESPACE}:{role}|>" for role in SPECIAL_TOKEN_ROLES
|
| 24 |
+
)
|
| 25 |
+
LEGACY_SPECIAL_TOKENS = ("[PAD]", "[UNK]", "[BOS]", "[EOS]", "[MASK]")
|
| 26 |
+
# Native spellings honored on pretrained-backbone tokenizers (e.g. Qwen3). Only roles with a
|
| 27 |
+
# trained native token map here; the rest are added as namespaced sentinels on free vocab rows.
|
| 28 |
+
PRETRAINED_SPECIAL_TOKENS: tuple[str | None, ...] = (None, None, None, "<|endoftext|>", None)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _role_token_candidates(index: int) -> tuple[str, ...]:
|
| 32 |
+
candidates = [SPECIAL_TOKENS[index], LEGACY_SPECIAL_TOKENS[index]]
|
| 33 |
+
pretrained = PRETRAINED_SPECIAL_TOKENS[index]
|
| 34 |
+
if pretrained is not None:
|
| 35 |
+
candidates.append(pretrained)
|
| 36 |
+
return tuple(candidates)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class _TokenizerLike(Protocol):
|
| 40 |
+
def token_to_id(self, token: str) -> int | None: ...
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _raw_tokenizer(tokenizer: _TokenizerLike) -> _TokenizerLike:
|
| 44 |
+
return getattr(tokenizer, "raw_tokenizer", tokenizer)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class RoleAwareTokenizer:
|
| 48 |
+
"""Proxy a Tokenizer while keeping legacy role spellings usable by old callers.
|
| 49 |
+
|
| 50 |
+
Encoding is always delegated unchanged, so strings such as ``[MASK]`` remain ordinary text in
|
| 51 |
+
newly trained tokenizers. Only explicit ``token_to_id`` lookups receive alias compatibility.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, tokenizer: Tokenizer) -> None:
|
| 55 |
+
self.raw_tokenizer = tokenizer
|
| 56 |
+
|
| 57 |
+
def token_to_id(self, token: str) -> int | None:
|
| 58 |
+
token_id = self.raw_tokenizer.token_to_id(token)
|
| 59 |
+
if token_id is not None:
|
| 60 |
+
return token_id
|
| 61 |
+
for index in range(len(SPECIAL_TOKEN_ROLES)):
|
| 62 |
+
candidates = _role_token_candidates(index)
|
| 63 |
+
if token in candidates:
|
| 64 |
+
for concrete in candidates:
|
| 65 |
+
token_id = self.raw_tokenizer.token_to_id(concrete)
|
| 66 |
+
if token_id is not None:
|
| 67 |
+
return token_id
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
def __getattr__(self, name: str):
|
| 71 |
+
return getattr(self.raw_tokenizer, name)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _role_index(role: SpecialTokenRole | str) -> int:
|
| 75 |
+
try:
|
| 76 |
+
return SPECIAL_TOKEN_ROLES.index(role) # type: ignore[arg-type]
|
| 77 |
+
except ValueError as exc:
|
| 78 |
+
choices = ", ".join(SPECIAL_TOKEN_ROLES)
|
| 79 |
+
raise ValueError(f"unknown special-token role {role!r}; expected one of {choices}") from exc
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def special_token_string(tokenizer: _TokenizerLike, role: SpecialTokenRole | str) -> str:
|
| 83 |
+
"""Return the concrete token string used for ``role`` by a new or legacy tokenizer."""
|
| 84 |
+
|
| 85 |
+
index = _role_index(role)
|
| 86 |
+
raw = _raw_tokenizer(tokenizer)
|
| 87 |
+
for token in _role_token_candidates(index):
|
| 88 |
+
if raw.token_to_id(token) is not None:
|
| 89 |
+
return token
|
| 90 |
+
raise ValueError(f"tokenizer is missing the {role!r} special token")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def special_token_id(tokenizer: _TokenizerLike, role: SpecialTokenRole | str) -> int:
|
| 94 |
+
"""Resolve a special-token ID by semantic role, independent of its serialized spelling."""
|
| 95 |
+
|
| 96 |
+
token = special_token_string(tokenizer, role)
|
| 97 |
+
token_id = _raw_tokenizer(tokenizer).token_to_id(token)
|
| 98 |
+
if token_id is None: # Kept defensive for non-Tokenizer protocol implementations.
|
| 99 |
+
raise ValueError(f"tokenizer is missing the {role!r} special token")
|
| 100 |
+
return token_id
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def special_token_ids(tokenizer: _TokenizerLike) -> dict[SpecialTokenRole, int]:
|
| 104 |
+
"""Return all semantic special-token IDs for a new or legacy tokenizer."""
|
| 105 |
+
|
| 106 |
+
return {role: special_token_id(tokenizer, role) for role in SPECIAL_TOKEN_ROLES}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _build_tokenizer() -> Tokenizer:
|
| 110 |
+
tokenizer = Tokenizer(models.BPE(unk_token=SPECIAL_TOKENS[1]))
|
| 111 |
+
tokenizer.normalizer = normalizers.NFC()
|
| 112 |
+
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
|
| 113 |
+
tokenizer.decoder = decoders.ByteLevel()
|
| 114 |
+
return tokenizer
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _build_trainer(
|
| 118 |
+
*,
|
| 119 |
+
vocab_size: int,
|
| 120 |
+
min_frequency: int,
|
| 121 |
+
max_token_length: int,
|
| 122 |
+
extra_special_tokens: tuple[str, ...] = (),
|
| 123 |
+
) -> trainers.BpeTrainer:
|
| 124 |
+
special_tokens = list(SPECIAL_TOKENS) + list(extra_special_tokens)
|
| 125 |
+
if len(set(special_tokens)) != len(special_tokens):
|
| 126 |
+
raise ValueError('extra_special_tokens must not duplicate the role tokens or each other')
|
| 127 |
+
if vocab_size <= len(special_tokens) + 256:
|
| 128 |
+
raise ValueError("vocab_size must leave room for the byte alphabet and special tokens")
|
| 129 |
+
if min_frequency <= 0:
|
| 130 |
+
raise ValueError("min_frequency must be positive")
|
| 131 |
+
if max_token_length <= 0:
|
| 132 |
+
raise ValueError("max_token_length must be positive")
|
| 133 |
+
return trainers.BpeTrainer(
|
| 134 |
+
vocab_size=vocab_size,
|
| 135 |
+
min_frequency=min_frequency,
|
| 136 |
+
special_tokens=special_tokens,
|
| 137 |
+
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
|
| 138 |
+
max_token_length=max_token_length,
|
| 139 |
+
show_progress=True,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _validate_special_token_layout(tokenizer: Tokenizer) -> None:
|
| 144 |
+
for expected_id, role in enumerate(SPECIAL_TOKEN_ROLES):
|
| 145 |
+
actual_id = special_token_id(tokenizer, role)
|
| 146 |
+
if actual_id != expected_id:
|
| 147 |
+
raise ValueError(
|
| 148 |
+
f"expected the {role!r} special token at id {expected_id}, found {actual_id}"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _save_tokenizer(tokenizer: Tokenizer, output_path: str | Path) -> None:
|
| 153 |
+
output = Path(output_path)
|
| 154 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 155 |
+
temporary = output.with_name(f".{output.name}.tmp")
|
| 156 |
+
temporary.unlink(missing_ok=True)
|
| 157 |
+
tokenizer.save(str(temporary), pretty=True)
|
| 158 |
+
temporary.replace(output)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
TokenizerTrainingItem: TypeAlias = str | list[str] | tuple[str, ...]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def train_tokenizer_from_iterator(
|
| 165 |
+
texts: Iterable[TokenizerTrainingItem],
|
| 166 |
+
output_path: str | Path,
|
| 167 |
+
*,
|
| 168 |
+
vocab_size: int = 32_768,
|
| 169 |
+
min_frequency: int = 10,
|
| 170 |
+
max_token_length: int = 64,
|
| 171 |
+
length: int | None = None,
|
| 172 |
+
extra_special_tokens: tuple[str, ...] = (),
|
| 173 |
+
) -> RoleAwareTokenizer:
|
| 174 |
+
"""Train a collision-resistant byte-level BPE directly from a text iterator.
|
| 175 |
+
|
| 176 |
+
Iterator items may be individual strings or batches of strings. Batched iteration avoids
|
| 177 |
+
materializing a large text export and is the intended entry point for Parquet corpora.
|
| 178 |
+
``extra_special_tokens`` are assigned the ids directly after the role tokens.
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
if length is not None and length < 0:
|
| 182 |
+
raise ValueError("length must be non-negative")
|
| 183 |
+
tokenizer = _build_tokenizer()
|
| 184 |
+
trainer = _build_trainer(
|
| 185 |
+
vocab_size=vocab_size,
|
| 186 |
+
min_frequency=min_frequency,
|
| 187 |
+
max_token_length=max_token_length,
|
| 188 |
+
extra_special_tokens=extra_special_tokens,
|
| 189 |
+
)
|
| 190 |
+
tokenizer.train_from_iterator(texts, trainer=trainer, length=length)
|
| 191 |
+
_validate_special_token_layout(tokenizer)
|
| 192 |
+
_save_tokenizer(tokenizer, output_path)
|
| 193 |
+
return RoleAwareTokenizer(tokenizer)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def train_tokenizer(
|
| 197 |
+
input_paths: Iterable[str | Path],
|
| 198 |
+
output_path: str | Path,
|
| 199 |
+
*,
|
| 200 |
+
vocab_size: int = 32_768,
|
| 201 |
+
min_frequency: int = 10,
|
| 202 |
+
max_token_length: int = 64,
|
| 203 |
+
) -> RoleAwareTokenizer:
|
| 204 |
+
"""Train a byte-level BPE from local text files."""
|
| 205 |
+
|
| 206 |
+
paths = [str(Path(path)) for path in input_paths]
|
| 207 |
+
if not paths:
|
| 208 |
+
raise ValueError("at least one input text file is required")
|
| 209 |
+
missing = [path for path in paths if not Path(path).is_file()]
|
| 210 |
+
if missing:
|
| 211 |
+
raise FileNotFoundError(f"missing tokenizer inputs: {missing}")
|
| 212 |
+
|
| 213 |
+
tokenizer = _build_tokenizer()
|
| 214 |
+
trainer = _build_trainer(
|
| 215 |
+
vocab_size=vocab_size,
|
| 216 |
+
min_frequency=min_frequency,
|
| 217 |
+
max_token_length=max_token_length,
|
| 218 |
+
)
|
| 219 |
+
tokenizer.train(paths, trainer)
|
| 220 |
+
_validate_special_token_layout(tokenizer)
|
| 221 |
+
_save_tokenizer(tokenizer, output_path)
|
| 222 |
+
return RoleAwareTokenizer(tokenizer)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def load_tokenizer(path: str | Path) -> RoleAwareTokenizer:
|
| 226 |
+
"""Load and validate a project, legacy, or pretrained-backbone token layout.
|
| 227 |
+
|
| 228 |
+
Project-trained tokenizers place the role tokens at ids 0-4 and keep the strict layout
|
| 229 |
+
check. Pretrained-backbone tokenizers (role tokens appended on free vocab rows, ``pad``
|
| 230 |
+
far from 0) only need every role to resolve to some id.
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
tokenizer_path = Path(path)
|
| 234 |
+
if not tokenizer_path.is_file():
|
| 235 |
+
raise FileNotFoundError(f"tokenizer not found: {tokenizer_path}")
|
| 236 |
+
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
| 237 |
+
if special_token_id(tokenizer, "pad") == 0:
|
| 238 |
+
_validate_special_token_layout(tokenizer)
|
| 239 |
+
else:
|
| 240 |
+
special_token_ids(tokenizer)
|
| 241 |
+
return RoleAwareTokenizer(tokenizer)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def token_metadata_path(token_path: str | Path) -> Path:
|
| 245 |
+
path = Path(token_path)
|
| 246 |
+
return path.with_suffix(path.suffix + ".json")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def encode_files(
|
| 250 |
+
tokenizer_path: str | Path,
|
| 251 |
+
input_paths: Iterable[str | Path],
|
| 252 |
+
output_path: str | Path,
|
| 253 |
+
) -> dict[str, object]:
|
| 254 |
+
"""Encode newline-delimited documents to a compact, memory-mappable file.
|
| 255 |
+
|
| 256 |
+
This compatibility path remains useful for small corpora. Web-scale Parquet preparation lives
|
| 257 |
+
in :mod:`diffusion_lm.corpus` and preserves embedded newlines within each document.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 261 |
+
inputs = [Path(path) for path in input_paths]
|
| 262 |
+
if not inputs:
|
| 263 |
+
raise ValueError("at least one input text file is required")
|
| 264 |
+
missing = [str(path) for path in inputs if not path.is_file()]
|
| 265 |
+
if missing:
|
| 266 |
+
raise FileNotFoundError(f"missing corpus inputs: {missing}")
|
| 267 |
+
|
| 268 |
+
vocab_size = tokenizer.get_vocab_size(with_added_tokens=True)
|
| 269 |
+
dtype = np.dtype("uint16" if vocab_size <= np.iinfo(np.uint16).max else "uint32")
|
| 270 |
+
role_ids = special_token_ids(tokenizer)
|
| 271 |
+
eos_id = role_ids["eos"]
|
| 272 |
+
reserved_ids = set(role_ids.values())
|
| 273 |
+
|
| 274 |
+
output = Path(output_path)
|
| 275 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 276 |
+
token_count = 0
|
| 277 |
+
document_count = 0
|
| 278 |
+
with output.open("wb") as destination:
|
| 279 |
+
for input_path in inputs:
|
| 280 |
+
with input_path.open("r", encoding="utf-8") as source:
|
| 281 |
+
for line_number, line in enumerate(
|
| 282 |
+
tqdm(source, desc=f"encoding {input_path.name}", unit="docs"), start=1
|
| 283 |
+
):
|
| 284 |
+
text = line.rstrip("\r\n")
|
| 285 |
+
if not text:
|
| 286 |
+
continue
|
| 287 |
+
token_ids = tokenizer.encode(text, add_special_tokens=False).ids
|
| 288 |
+
encountered = reserved_ids.intersection(token_ids)
|
| 289 |
+
if encountered:
|
| 290 |
+
raise ValueError(
|
| 291 |
+
f"{input_path}:{line_number} encodes reserved special-token ids "
|
| 292 |
+
f"{sorted(encountered)}; remove literal special tokens from the corpus"
|
| 293 |
+
)
|
| 294 |
+
token_ids.append(eos_id)
|
| 295 |
+
np.asarray(token_ids, dtype=dtype).tofile(destination)
|
| 296 |
+
token_count += len(token_ids)
|
| 297 |
+
document_count += 1
|
| 298 |
+
|
| 299 |
+
tokenizer_bytes = Path(tokenizer_path).read_bytes()
|
| 300 |
+
metadata: dict[str, object] = {
|
| 301 |
+
"format": "mini-diffusion-lm-packed-tokens-v1",
|
| 302 |
+
"dtype": dtype.name,
|
| 303 |
+
"token_count": token_count,
|
| 304 |
+
"document_count": document_count,
|
| 305 |
+
"vocab_size": vocab_size,
|
| 306 |
+
"mask_token_id": role_ids["mask"],
|
| 307 |
+
"eos_token_id": eos_id,
|
| 308 |
+
"special_token_ids": role_ids,
|
| 309 |
+
"tokenizer_sha256": hashlib.sha256(tokenizer_bytes).hexdigest(),
|
| 310 |
+
"source_files": [str(path) for path in inputs],
|
| 311 |
+
}
|
| 312 |
+
metadata_path = token_metadata_path(output)
|
| 313 |
+
with metadata_path.open("w", encoding="utf-8") as handle:
|
| 314 |
+
json.dump(metadata, handle, indent=2)
|
| 315 |
+
handle.write("\n")
|
| 316 |
+
return metadata
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 320 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 321 |
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
| 322 |
+
|
| 323 |
+
train_parser = subparsers.add_parser("train", help="train a byte-level BPE tokenizer")
|
| 324 |
+
train_parser.add_argument("--input", type=Path, nargs="+", required=True)
|
| 325 |
+
train_parser.add_argument("--output", type=Path, required=True)
|
| 326 |
+
train_parser.add_argument("--vocab-size", type=int, default=32_768)
|
| 327 |
+
train_parser.add_argument("--min-frequency", type=int, default=10)
|
| 328 |
+
train_parser.add_argument("--max-token-length", type=int, default=64)
|
| 329 |
+
|
| 330 |
+
encode_parser = subparsers.add_parser("encode", help="encode text into packed tokens")
|
| 331 |
+
encode_parser.add_argument("--tokenizer", type=Path, required=True)
|
| 332 |
+
encode_parser.add_argument("--input", type=Path, nargs="+", required=True)
|
| 333 |
+
encode_parser.add_argument("--output", type=Path, required=True)
|
| 334 |
+
return parser
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def main() -> None:
|
| 338 |
+
args = _build_parser().parse_args()
|
| 339 |
+
if args.command == "train":
|
| 340 |
+
tokenizer = train_tokenizer(
|
| 341 |
+
args.input,
|
| 342 |
+
args.output,
|
| 343 |
+
vocab_size=args.vocab_size,
|
| 344 |
+
min_frequency=args.min_frequency,
|
| 345 |
+
max_token_length=args.max_token_length,
|
| 346 |
+
)
|
| 347 |
+
print(f"saved {tokenizer.get_vocab_size():,}-token tokenizer to {args.output}")
|
| 348 |
+
elif args.command == "encode":
|
| 349 |
+
metadata = encode_files(args.tokenizer, args.input, args.output)
|
| 350 |
+
print(
|
| 351 |
+
f"wrote {metadata['token_count']:,} tokens from "
|
| 352 |
+
f"{metadata['document_count']:,} documents to {args.output}"
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
if __name__ == "__main__":
|
| 357 |
+
main()
|
src/diffusion_lm/train.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single-device training loop for the masked discrete diffusion LM."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import contextlib
|
| 7 |
+
import gc
|
| 8 |
+
import hashlib
|
| 9 |
+
import json
|
| 10 |
+
import math
|
| 11 |
+
import random
|
| 12 |
+
import shutil
|
| 13 |
+
import time
|
| 14 |
+
from dataclasses import replace
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
from torch import Tensor
|
| 19 |
+
from torch.optim import AdamW, Optimizer
|
| 20 |
+
from torch.utils.data import DataLoader
|
| 21 |
+
|
| 22 |
+
from diffusion_lm.config import ExperimentConfig, ModelConfig, TrainingConfig, load_config
|
| 23 |
+
from diffusion_lm.data import DeterministicBatchSampler, load_packed_dataset
|
| 24 |
+
from diffusion_lm.diffusion import corrupt_tokens, diffusion_cross_entropy
|
| 25 |
+
from diffusion_lm.model import DiffusionTransformer, format_parameter_count
|
| 26 |
+
from diffusion_lm.tokenizer import load_tokenizer, special_token_id
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def seed_everything(seed: int, device: torch.device) -> None:
|
| 30 |
+
random.seed(seed)
|
| 31 |
+
np.random.seed(seed)
|
| 32 |
+
torch.manual_seed(seed)
|
| 33 |
+
if device.type == "cuda":
|
| 34 |
+
torch.cuda.manual_seed_all(seed)
|
| 35 |
+
if device.type == "mps" and hasattr(torch.mps, "manual_seed"):
|
| 36 |
+
# Materialize the lazy MPS runtime before setting its RNG. Seeding before
|
| 37 |
+
# the first allocation can otherwise be overwritten during initialization.
|
| 38 |
+
torch.empty((), device="mps")
|
| 39 |
+
torch.mps.synchronize()
|
| 40 |
+
torch.mps.manual_seed(seed)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def capture_rng_state(device: torch.device) -> dict[str, object]:
|
| 44 |
+
state: dict[str, object] = {
|
| 45 |
+
"python": random.getstate(),
|
| 46 |
+
"numpy": np.random.get_state(),
|
| 47 |
+
"torch": torch.get_rng_state(),
|
| 48 |
+
}
|
| 49 |
+
if device.type == "cuda":
|
| 50 |
+
state["cuda"] = torch.cuda.get_rng_state_all()
|
| 51 |
+
if device.type == "mps" and hasattr(torch.mps, "get_rng_state"):
|
| 52 |
+
state["mps"] = torch.mps.get_rng_state()
|
| 53 |
+
return state
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def restore_rng_state(state: dict[str, object]) -> None:
|
| 57 |
+
random.setstate(state["python"]) # type: ignore[arg-type]
|
| 58 |
+
np.random.set_state(state["numpy"]) # type: ignore[arg-type]
|
| 59 |
+
torch.set_rng_state(state["torch"].cpu()) # type: ignore[union-attr]
|
| 60 |
+
if torch.cuda.is_available() and "cuda" in state:
|
| 61 |
+
torch.cuda.set_rng_state_all( # type: ignore[arg-type]
|
| 62 |
+
[rng_state.cpu() for rng_state in state["cuda"]] # type: ignore[union-attr]
|
| 63 |
+
)
|
| 64 |
+
if (
|
| 65 |
+
torch.backends.mps.is_available()
|
| 66 |
+
and "mps" in state
|
| 67 |
+
and hasattr(torch.mps, "set_rng_state")
|
| 68 |
+
):
|
| 69 |
+
torch.mps.set_rng_state(state["mps"].cpu()) # type: ignore[union-attr]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def resolve_device(requested: str) -> torch.device:
|
| 73 |
+
if requested != "auto":
|
| 74 |
+
device = torch.device(requested)
|
| 75 |
+
if device.type == "cuda" and not torch.cuda.is_available():
|
| 76 |
+
raise RuntimeError("CUDA was requested but is unavailable")
|
| 77 |
+
if device.type == "mps" and not torch.backends.mps.is_available():
|
| 78 |
+
raise RuntimeError("MPS was requested but is unavailable")
|
| 79 |
+
return device
|
| 80 |
+
if torch.cuda.is_available():
|
| 81 |
+
return torch.device("cuda")
|
| 82 |
+
if torch.backends.mps.is_available():
|
| 83 |
+
return torch.device("mps")
|
| 84 |
+
return torch.device("cpu")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def resolve_precision(requested: str, device: torch.device) -> str:
|
| 88 |
+
if requested != "auto":
|
| 89 |
+
if device.type == "cpu" and requested == "float16":
|
| 90 |
+
raise ValueError("float16 training on CPU is unsupported; use float32 or bfloat16")
|
| 91 |
+
return requested
|
| 92 |
+
if device.type == "cuda":
|
| 93 |
+
return "bfloat16" if torch.cuda.is_bf16_supported() else "float16"
|
| 94 |
+
# Float32 is the most reliable default for CPU and Apple Silicon in this MVP.
|
| 95 |
+
return "float32"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def autocast_context(device: torch.device, precision: str):
|
| 99 |
+
if precision == "float32":
|
| 100 |
+
return contextlib.nullcontext()
|
| 101 |
+
dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16}[precision]
|
| 102 |
+
return torch.autocast(device_type=device.type, dtype=dtype)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def configure_cuda_backends(device: torch.device, require_fused_attention: bool) -> None:
|
| 106 |
+
"""Enable Ampere-friendly kernels and optionally forbid quadratic math attention."""
|
| 107 |
+
|
| 108 |
+
if device.type != "cuda":
|
| 109 |
+
if require_fused_attention:
|
| 110 |
+
raise ValueError("fused attention can only be required on CUDA")
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
torch.set_float32_matmul_precision("high")
|
| 114 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 115 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 116 |
+
torch.backends.cuda.enable_flash_sdp(True)
|
| 117 |
+
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
| 118 |
+
# MultiheadAttention delegates to scaled_dot_product_attention when it is
|
| 119 |
+
# called with need_weights=False (as TransformerEncoderLayer does). When
|
| 120 |
+
# required, disabling math makes an unsupported fused path fail fast rather
|
| 121 |
+
# than silently allocating an L x L attention matrix. Set it in both branches
|
| 122 |
+
# so repeated train()/benchmark calls in one process cannot leak backend state.
|
| 123 |
+
torch.backends.cuda.enable_math_sdp(not require_fused_attention)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def build_optimizer(model: DiffusionTransformer, config: TrainingConfig) -> Optimizer:
|
| 127 |
+
decay: list[Tensor] = []
|
| 128 |
+
no_decay: list[Tensor] = []
|
| 129 |
+
for parameter in model.parameters():
|
| 130 |
+
if not parameter.requires_grad:
|
| 131 |
+
continue
|
| 132 |
+
(decay if parameter.ndim >= 2 else no_decay).append(parameter)
|
| 133 |
+
parameter_groups = [
|
| 134 |
+
{"params": decay, "weight_decay": config.weight_decay},
|
| 135 |
+
{"params": no_decay, "weight_decay": 0.0},
|
| 136 |
+
]
|
| 137 |
+
common = {
|
| 138 |
+
"lr": config.learning_rate,
|
| 139 |
+
"betas": (0.9, 0.95),
|
| 140 |
+
"eps": 1e-8,
|
| 141 |
+
}
|
| 142 |
+
if config.optimizer == "adamw8bit":
|
| 143 |
+
try:
|
| 144 |
+
import bitsandbytes as bnb
|
| 145 |
+
except ImportError as exc:
|
| 146 |
+
raise RuntimeError(
|
| 147 |
+
'optimizer=adamw8bit requires bitsandbytes; install the "gpu" extra'
|
| 148 |
+
) from exc
|
| 149 |
+
if config.optimizer_embedding_32bit:
|
| 150 |
+
# Bitsandbytes recommends higher-precision optimizer state for NLP
|
| 151 |
+
# embeddings. The LM head is tied to this exact Parameter, so one
|
| 152 |
+
# override protects both roles at a modest memory cost.
|
| 153 |
+
manager = bnb.optim.GlobalOptimManager.get_instance()
|
| 154 |
+
manager.register_module_override(
|
| 155 |
+
model.token_embedding,
|
| 156 |
+
"weight",
|
| 157 |
+
{"optim_bits": 32},
|
| 158 |
+
)
|
| 159 |
+
return bnb.optim.AdamW8bit(
|
| 160 |
+
parameter_groups,
|
| 161 |
+
min_8bit_size=config.optimizer_min_8bit_size,
|
| 162 |
+
**common,
|
| 163 |
+
)
|
| 164 |
+
return AdamW(parameter_groups, foreach=False, **common)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def accumulation_mask_probabilities(
|
| 168 |
+
batch_size: int,
|
| 169 |
+
micro_batch_index: int,
|
| 170 |
+
config: TrainingConfig,
|
| 171 |
+
offset: Tensor,
|
| 172 |
+
device: torch.device,
|
| 173 |
+
) -> Tensor:
|
| 174 |
+
"""Stratify diffusion times across a complete gradient-accumulation step."""
|
| 175 |
+
|
| 176 |
+
slots = config.batch_size * config.gradient_accumulation_steps
|
| 177 |
+
start = micro_batch_index * config.batch_size
|
| 178 |
+
indices = torch.arange(start, start + batch_size, device=device, dtype=torch.float32)
|
| 179 |
+
unit = (offset + indices / max(1, slots)) % 1.0
|
| 180 |
+
return config.mask_eps + (1.0 - config.mask_eps) * unit
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def learning_rate(step: int, config: TrainingConfig) -> float:
|
| 184 |
+
if step < config.warmup_steps:
|
| 185 |
+
return config.learning_rate * (step + 1) / max(1, config.warmup_steps)
|
| 186 |
+
progress = (step - config.warmup_steps) / max(1, config.max_steps - config.warmup_steps - 1)
|
| 187 |
+
cosine = 0.5 * (1.0 + math.cos(math.pi * min(progress, 1.0)))
|
| 188 |
+
return config.min_learning_rate + cosine * (
|
| 189 |
+
config.learning_rate - config.min_learning_rate
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def create_grad_scaler(enabled: bool):
|
| 194 |
+
"""Use the unified API when available and retain PyTorch 2.2 support."""
|
| 195 |
+
|
| 196 |
+
unified_scaler = getattr(torch.amp, "GradScaler", None)
|
| 197 |
+
if unified_scaler is not None:
|
| 198 |
+
return unified_scaler("cuda", enabled=enabled)
|
| 199 |
+
return torch.cuda.amp.GradScaler(enabled=enabled)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def validate_inputs(config: ExperimentConfig) -> None:
|
| 203 |
+
tokenizer = load_tokenizer(config.training.tokenizer)
|
| 204 |
+
actual_vocab = tokenizer.get_vocab_size(with_added_tokens=True)
|
| 205 |
+
actual_mask = special_token_id(tokenizer, "mask")
|
| 206 |
+
if actual_vocab != config.model.vocab_size:
|
| 207 |
+
raise ValueError(
|
| 208 |
+
f"config vocab_size is {config.model.vocab_size}, tokenizer has {actual_vocab}; "
|
| 209 |
+
"train the requested tokenizer or update the model budget"
|
| 210 |
+
)
|
| 211 |
+
if actual_mask != config.model.mask_token_id:
|
| 212 |
+
raise ValueError(
|
| 213 |
+
f"config mask_token_id is {config.model.mask_token_id}, tokenizer uses {actual_mask}"
|
| 214 |
+
)
|
| 215 |
+
tokenizer_hash = hashlib.sha256(Path(config.training.tokenizer).read_bytes()).hexdigest()
|
| 216 |
+
for data_path in (config.training.train_data, config.training.val_data):
|
| 217 |
+
if data_path is None:
|
| 218 |
+
continue
|
| 219 |
+
dataset = load_packed_dataset(data_path, config.model.max_seq_len)
|
| 220 |
+
metadata = dataset.metadata
|
| 221 |
+
if int(metadata["vocab_size"]) != config.model.vocab_size:
|
| 222 |
+
raise ValueError(f"{data_path} was encoded with a different vocabulary size")
|
| 223 |
+
if metadata["tokenizer_sha256"] != tokenizer_hash:
|
| 224 |
+
raise ValueError(f"{data_path} was encoded with a different tokenizer file")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@torch.no_grad()
|
| 228 |
+
def evaluate(
|
| 229 |
+
model: DiffusionTransformer,
|
| 230 |
+
loader: DataLoader[Tensor],
|
| 231 |
+
device: torch.device,
|
| 232 |
+
precision: str,
|
| 233 |
+
mask_eps: float,
|
| 234 |
+
max_batches: int,
|
| 235 |
+
) -> dict[str, float]:
|
| 236 |
+
training_rng_state = capture_rng_state(device)
|
| 237 |
+
was_training = model.training
|
| 238 |
+
try:
|
| 239 |
+
# Fixed corruption masks make validation checkpoints directly comparable.
|
| 240 |
+
# The complete caller RNG state is restored below, so evaluation remains
|
| 241 |
+
# invisible to the subsequent training trajectory.
|
| 242 |
+
seed_everything(0, device)
|
| 243 |
+
model.eval()
|
| 244 |
+
losses: list[float] = []
|
| 245 |
+
correct_weighted = 0.0
|
| 246 |
+
masked_total = 0
|
| 247 |
+
for batch_index, clean_tokens in enumerate(loader):
|
| 248 |
+
if batch_index >= max_batches:
|
| 249 |
+
break
|
| 250 |
+
clean_tokens = clean_tokens.to(device, non_blocking=True)
|
| 251 |
+
# Cover the full noise range deterministically even when evaluation uses
|
| 252 |
+
# microbatch one. Random batch-1 evaluation can otherwise miss the hard
|
| 253 |
+
# near-fully-masked regime for many consecutive checkpoints.
|
| 254 |
+
level = mask_eps + (1.0 - mask_eps) * (batch_index + 0.5) / max_batches
|
| 255 |
+
mask_probability = torch.full(
|
| 256 |
+
(clean_tokens.shape[0],), level, device=device, dtype=torch.float32
|
| 257 |
+
)
|
| 258 |
+
corruption = corrupt_tokens(
|
| 259 |
+
clean_tokens,
|
| 260 |
+
model.config.mask_token_id,
|
| 261 |
+
mask_probability=mask_probability,
|
| 262 |
+
eps=mask_eps,
|
| 263 |
+
)
|
| 264 |
+
with autocast_context(device, precision):
|
| 265 |
+
logits = model(corruption.noisy_tokens, output_positions=corruption.mask)
|
| 266 |
+
output = diffusion_cross_entropy(logits, clean_tokens, corruption)
|
| 267 |
+
losses.append(float(output.loss))
|
| 268 |
+
correct_weighted += float(output.masked_accuracy) * output.masked_tokens
|
| 269 |
+
masked_total += output.masked_tokens
|
| 270 |
+
return {
|
| 271 |
+
"loss": sum(losses) / max(1, len(losses)),
|
| 272 |
+
"masked_accuracy": correct_weighted / max(1, masked_total),
|
| 273 |
+
}
|
| 274 |
+
finally:
|
| 275 |
+
restore_rng_state(training_rng_state)
|
| 276 |
+
model.train(was_training)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def save_checkpoint(
|
| 280 |
+
output_dir: Path,
|
| 281 |
+
model: DiffusionTransformer,
|
| 282 |
+
optimizer: Optimizer,
|
| 283 |
+
scaler,
|
| 284 |
+
experiment: ExperimentConfig,
|
| 285 |
+
step: int,
|
| 286 |
+
tokens_seen: int,
|
| 287 |
+
keep_last_checkpoints: int,
|
| 288 |
+
data_generator: torch.Generator,
|
| 289 |
+
micro_batches_seen: int,
|
| 290 |
+
) -> Path:
|
| 291 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 292 |
+
checkpoint = {
|
| 293 |
+
"format": "mini-diffusion-lm-checkpoint-v1",
|
| 294 |
+
"step": step,
|
| 295 |
+
"tokens_seen": tokens_seen,
|
| 296 |
+
"config": experiment.to_dict(),
|
| 297 |
+
"tokenizer_sha256": hashlib.sha256(
|
| 298 |
+
Path(experiment.training.tokenizer).read_bytes()
|
| 299 |
+
).hexdigest(),
|
| 300 |
+
"rng_state": capture_rng_state(next(model.parameters()).device),
|
| 301 |
+
"data_generator_state": data_generator.get_state(),
|
| 302 |
+
"micro_batches_seen": micro_batches_seen,
|
| 303 |
+
"model": model.state_dict(),
|
| 304 |
+
"optimizer": optimizer.state_dict(),
|
| 305 |
+
"scaler": scaler.state_dict(),
|
| 306 |
+
}
|
| 307 |
+
numbered_path = output_dir / f"step-{step:08d}.pt"
|
| 308 |
+
temporary_path = output_dir / ".checkpoint.tmp"
|
| 309 |
+
torch.save(checkpoint, temporary_path)
|
| 310 |
+
temporary_path.replace(numbered_path)
|
| 311 |
+
|
| 312 |
+
# Keep `latest.pt` as a hard link when possible so a large optimizer state is
|
| 313 |
+
# not stored twice. The temporary name makes replacement atomic.
|
| 314 |
+
latest_path = output_dir / "latest.pt"
|
| 315 |
+
latest_temporary = output_dir / ".latest.tmp"
|
| 316 |
+
latest_temporary.unlink(missing_ok=True)
|
| 317 |
+
try:
|
| 318 |
+
latest_temporary.hardlink_to(numbered_path)
|
| 319 |
+
except OSError:
|
| 320 |
+
shutil.copyfile(numbered_path, latest_temporary)
|
| 321 |
+
latest_temporary.replace(latest_path)
|
| 322 |
+
|
| 323 |
+
if keep_last_checkpoints:
|
| 324 |
+
numbered_checkpoints = sorted(output_dir.glob("step-*.pt"))
|
| 325 |
+
for old_checkpoint in numbered_checkpoints[:-keep_last_checkpoints]:
|
| 326 |
+
old_checkpoint.unlink()
|
| 327 |
+
if experiment.training.save_inference_checkpoint:
|
| 328 |
+
save_inference_checkpoint(output_dir, model, experiment, step, tokens_seen)
|
| 329 |
+
return numbered_path
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _inference_state_dict(model: DiffusionTransformer) -> dict[str, Tensor]:
|
| 333 |
+
"""Copy weights to CPU BF16 while preserving tied tensor storage."""
|
| 334 |
+
|
| 335 |
+
converted: dict[str, Tensor] = {}
|
| 336 |
+
shared: dict[tuple[object, ...], Tensor] = {}
|
| 337 |
+
for name, tensor in model.state_dict().items():
|
| 338 |
+
key = (
|
| 339 |
+
tensor.untyped_storage().data_ptr(),
|
| 340 |
+
tensor.storage_offset(),
|
| 341 |
+
tuple(tensor.shape),
|
| 342 |
+
tuple(tensor.stride()),
|
| 343 |
+
)
|
| 344 |
+
value = shared.get(key)
|
| 345 |
+
if value is None:
|
| 346 |
+
dtype = torch.bfloat16 if tensor.is_floating_point() else tensor.dtype
|
| 347 |
+
value = tensor.detach().to(device="cpu", dtype=dtype)
|
| 348 |
+
shared[key] = value
|
| 349 |
+
converted[name] = value
|
| 350 |
+
return converted
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def save_inference_checkpoint(
|
| 354 |
+
output_dir: Path,
|
| 355 |
+
model: DiffusionTransformer,
|
| 356 |
+
experiment: ExperimentConfig,
|
| 357 |
+
step: int,
|
| 358 |
+
tokens_seen: int,
|
| 359 |
+
) -> Path:
|
| 360 |
+
"""Write a compact weights-only checkpoint for sampling and the playground."""
|
| 361 |
+
|
| 362 |
+
path = output_dir / "inference-latest.pt"
|
| 363 |
+
temporary = output_dir / ".inference.tmp"
|
| 364 |
+
state = _inference_state_dict(model)
|
| 365 |
+
payload = {
|
| 366 |
+
"format": "mini-diffusion-lm-inference-v1",
|
| 367 |
+
"step": step,
|
| 368 |
+
"tokens_seen": tokens_seen,
|
| 369 |
+
"config": experiment.to_dict(),
|
| 370 |
+
"tokenizer_sha256": hashlib.sha256(
|
| 371 |
+
Path(experiment.training.tokenizer).read_bytes()
|
| 372 |
+
).hexdigest(),
|
| 373 |
+
"model": state,
|
| 374 |
+
}
|
| 375 |
+
torch.save(payload, temporary)
|
| 376 |
+
temporary.replace(path)
|
| 377 |
+
del payload, state
|
| 378 |
+
gc.collect()
|
| 379 |
+
return path
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def train(
|
| 383 |
+
experiment: ExperimentConfig,
|
| 384 |
+
resume: str | Path | None = None,
|
| 385 |
+
max_run_steps: int | None = None,
|
| 386 |
+
) -> Path:
|
| 387 |
+
config = experiment.training
|
| 388 |
+
if max_run_steps is not None and max_run_steps <= 0:
|
| 389 |
+
raise ValueError("max_run_steps must be positive")
|
| 390 |
+
device = resolve_device(config.device)
|
| 391 |
+
seed_everything(config.seed, device)
|
| 392 |
+
configure_cuda_backends(device, config.require_fused_attention)
|
| 393 |
+
validate_inputs(experiment)
|
| 394 |
+
|
| 395 |
+
precision = resolve_precision(config.precision, device)
|
| 396 |
+
train_dataset = load_packed_dataset(config.train_data, experiment.model.max_seq_len)
|
| 397 |
+
data_generator = torch.Generator().manual_seed(config.seed)
|
| 398 |
+
train_batch_sampler = DeterministicBatchSampler(
|
| 399 |
+
len(train_dataset), config.batch_size, seed=config.seed
|
| 400 |
+
)
|
| 401 |
+
train_loader = DataLoader(
|
| 402 |
+
train_dataset,
|
| 403 |
+
batch_sampler=train_batch_sampler,
|
| 404 |
+
num_workers=config.num_workers,
|
| 405 |
+
pin_memory=device.type == "cuda",
|
| 406 |
+
generator=data_generator,
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
val_loader = None
|
| 410 |
+
if config.val_data is not None:
|
| 411 |
+
val_dataset = load_packed_dataset(config.val_data, experiment.model.max_seq_len)
|
| 412 |
+
val_loader = DataLoader(
|
| 413 |
+
val_dataset,
|
| 414 |
+
batch_size=config.batch_size,
|
| 415 |
+
shuffle=False,
|
| 416 |
+
num_workers=config.num_workers,
|
| 417 |
+
pin_memory=device.type == "cuda",
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
model = DiffusionTransformer(experiment.model).to(device)
|
| 421 |
+
optimizer = build_optimizer(model, config)
|
| 422 |
+
scaler = create_grad_scaler(device.type == "cuda" and precision == "float16")
|
| 423 |
+
start_step = 0
|
| 424 |
+
tokens_seen = 0
|
| 425 |
+
micro_batches_seen = 0
|
| 426 |
+
if resume is not None:
|
| 427 |
+
checkpoint = torch.load(resume, map_location="cpu", weights_only=False)
|
| 428 |
+
if checkpoint.get("format") != "mini-diffusion-lm-checkpoint-v1":
|
| 429 |
+
raise ValueError("unsupported checkpoint format")
|
| 430 |
+
if ModelConfig(**checkpoint["config"]["model"]) != experiment.model:
|
| 431 |
+
raise ValueError("checkpoint model configuration does not match the requested config")
|
| 432 |
+
checkpoint_training = checkpoint["config"].get("training", {})
|
| 433 |
+
checkpoint_optimizer = checkpoint_training.get("optimizer", "adamw")
|
| 434 |
+
if checkpoint_optimizer != config.optimizer:
|
| 435 |
+
raise ValueError(
|
| 436 |
+
f"checkpoint optimizer is {checkpoint_optimizer}, requested {config.optimizer}"
|
| 437 |
+
)
|
| 438 |
+
if config.optimizer == "adamw8bit":
|
| 439 |
+
checkpoint_min_size = int(
|
| 440 |
+
checkpoint_training.get("optimizer_min_8bit_size", 4096)
|
| 441 |
+
)
|
| 442 |
+
checkpoint_embedding_32bit = bool(
|
| 443 |
+
# An absent legacy field means no explicit 32-bit override was
|
| 444 |
+
# guaranteed. Never silently reinterpret it as the safer setting.
|
| 445 |
+
checkpoint_training.get("optimizer_embedding_32bit", False)
|
| 446 |
+
)
|
| 447 |
+
if checkpoint_min_size != config.optimizer_min_8bit_size:
|
| 448 |
+
raise ValueError("checkpoint 8-bit optimizer minimum tensor size does not match")
|
| 449 |
+
if checkpoint_embedding_32bit != config.optimizer_embedding_32bit:
|
| 450 |
+
raise ValueError("checkpoint embedding optimizer precision does not match")
|
| 451 |
+
current_tokenizer_hash = hashlib.sha256(Path(config.tokenizer).read_bytes()).hexdigest()
|
| 452 |
+
if checkpoint.get("tokenizer_sha256") != current_tokenizer_hash:
|
| 453 |
+
raise ValueError("checkpoint was trained with a different tokenizer")
|
| 454 |
+
model.load_state_dict(checkpoint["model"])
|
| 455 |
+
optimizer.load_state_dict(checkpoint["optimizer"])
|
| 456 |
+
optimizer.param_groups[0]["weight_decay"] = config.weight_decay
|
| 457 |
+
optimizer.param_groups[1]["weight_decay"] = 0.0
|
| 458 |
+
for group in optimizer.param_groups:
|
| 459 |
+
group["betas"] = (0.9, 0.95)
|
| 460 |
+
group["eps"] = 1e-8
|
| 461 |
+
scaler.load_state_dict(checkpoint.get("scaler", {}))
|
| 462 |
+
if "data_generator_state" in checkpoint:
|
| 463 |
+
data_generator.set_state(checkpoint["data_generator_state"].cpu())
|
| 464 |
+
if "rng_state" in checkpoint:
|
| 465 |
+
restore_rng_state(checkpoint["rng_state"])
|
| 466 |
+
start_step = int(checkpoint["step"]) + 1
|
| 467 |
+
tokens_seen = int(checkpoint.get("tokens_seen", 0))
|
| 468 |
+
micro_batches_seen = int(
|
| 469 |
+
checkpoint.get(
|
| 470 |
+
"micro_batches_seen",
|
| 471 |
+
start_step * int(checkpoint["config"]["training"]["gradient_accumulation_steps"]),
|
| 472 |
+
)
|
| 473 |
+
)
|
| 474 |
+
del checkpoint
|
| 475 |
+
gc.collect()
|
| 476 |
+
|
| 477 |
+
train_batch_sampler.start_batch = micro_batches_seen
|
| 478 |
+
train_iterator = iter(train_loader)
|
| 479 |
+
|
| 480 |
+
output_dir = Path(config.output_dir)
|
| 481 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 482 |
+
with (output_dir / "config.json").open("w", encoding="utf-8") as handle:
|
| 483 |
+
json.dump(experiment.to_dict(), handle, indent=2)
|
| 484 |
+
handle.write("\n")
|
| 485 |
+
|
| 486 |
+
print(
|
| 487 |
+
json.dumps(
|
| 488 |
+
{
|
| 489 |
+
"event": "start",
|
| 490 |
+
"device": str(device),
|
| 491 |
+
"precision": precision,
|
| 492 |
+
"parameters": model.num_parameters,
|
| 493 |
+
"parameters_human": format_parameter_count(model.num_parameters),
|
| 494 |
+
"optimizer": config.optimizer,
|
| 495 |
+
"activation_checkpointing": experiment.model.activation_checkpointing,
|
| 496 |
+
"fused_attention_required": config.require_fused_attention,
|
| 497 |
+
"training_blocks": len(train_dataset),
|
| 498 |
+
"start_step": start_step,
|
| 499 |
+
}
|
| 500 |
+
)
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
last_checkpoint = output_dir / "latest.pt"
|
| 504 |
+
model.train()
|
| 505 |
+
log_started = time.perf_counter()
|
| 506 |
+
log_loss = torch.zeros((), device=device)
|
| 507 |
+
log_accuracy = torch.zeros((), device=device)
|
| 508 |
+
log_tokens = 0
|
| 509 |
+
end_step = config.max_steps
|
| 510 |
+
if max_run_steps is not None:
|
| 511 |
+
end_step = min(end_step, start_step + max_run_steps)
|
| 512 |
+
for step in range(start_step, end_step):
|
| 513 |
+
lr = learning_rate(step, config)
|
| 514 |
+
for group in optimizer.param_groups:
|
| 515 |
+
group["lr"] = lr
|
| 516 |
+
optimizer.zero_grad(set_to_none=True)
|
| 517 |
+
step_loss = torch.zeros((), device=device)
|
| 518 |
+
step_accuracy = torch.zeros((), device=device)
|
| 519 |
+
noise_offset = torch.rand((), device=device)
|
| 520 |
+
|
| 521 |
+
for micro_batch_index in range(config.gradient_accumulation_steps):
|
| 522 |
+
clean_tokens = next(train_iterator).to(device, non_blocking=True)
|
| 523 |
+
micro_batches_seen += 1
|
| 524 |
+
mask_probability = accumulation_mask_probabilities(
|
| 525 |
+
clean_tokens.shape[0],
|
| 526 |
+
micro_batch_index,
|
| 527 |
+
config,
|
| 528 |
+
noise_offset,
|
| 529 |
+
device,
|
| 530 |
+
)
|
| 531 |
+
corruption = corrupt_tokens(
|
| 532 |
+
clean_tokens,
|
| 533 |
+
experiment.model.mask_token_id,
|
| 534 |
+
mask_probability=mask_probability,
|
| 535 |
+
eps=config.mask_eps,
|
| 536 |
+
)
|
| 537 |
+
with autocast_context(device, precision):
|
| 538 |
+
logits = model(corruption.noisy_tokens, output_positions=corruption.mask)
|
| 539 |
+
output = diffusion_cross_entropy(logits, clean_tokens, corruption)
|
| 540 |
+
scaled_loss = output.loss / config.gradient_accumulation_steps
|
| 541 |
+
scaler.scale(scaled_loss).backward()
|
| 542 |
+
step_loss += output.loss.detach() / config.gradient_accumulation_steps
|
| 543 |
+
step_accuracy += output.masked_accuracy.detach() / config.gradient_accumulation_steps
|
| 544 |
+
tokens_seen += clean_tokens.numel()
|
| 545 |
+
log_tokens += clean_tokens.numel()
|
| 546 |
+
|
| 547 |
+
scaler.unscale_(optimizer)
|
| 548 |
+
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_clip)
|
| 549 |
+
scaler.step(optimizer)
|
| 550 |
+
scaler.update()
|
| 551 |
+
log_loss += step_loss
|
| 552 |
+
log_accuracy += step_accuracy
|
| 553 |
+
|
| 554 |
+
if (step + 1) % config.log_interval == 0:
|
| 555 |
+
elapsed = time.perf_counter() - log_started
|
| 556 |
+
print(
|
| 557 |
+
json.dumps(
|
| 558 |
+
{
|
| 559 |
+
"event": "train",
|
| 560 |
+
"step": step + 1,
|
| 561 |
+
"loss": float(log_loss / config.log_interval),
|
| 562 |
+
"masked_accuracy": float(log_accuracy / config.log_interval),
|
| 563 |
+
"learning_rate": lr,
|
| 564 |
+
"grad_norm": float(grad_norm),
|
| 565 |
+
"tokens_seen": tokens_seen,
|
| 566 |
+
"tokens_per_second": log_tokens / max(elapsed, 1e-9),
|
| 567 |
+
"memory_allocated_gib": (
|
| 568 |
+
torch.cuda.memory_allocated(device) / 1024**3
|
| 569 |
+
if device.type == "cuda"
|
| 570 |
+
else 0.0
|
| 571 |
+
),
|
| 572 |
+
"memory_reserved_gib": (
|
| 573 |
+
torch.cuda.memory_reserved(device) / 1024**3
|
| 574 |
+
if device.type == "cuda"
|
| 575 |
+
else 0.0
|
| 576 |
+
),
|
| 577 |
+
"peak_memory_allocated_gib": (
|
| 578 |
+
torch.cuda.max_memory_allocated(device) / 1024**3
|
| 579 |
+
if device.type == "cuda"
|
| 580 |
+
else 0.0
|
| 581 |
+
),
|
| 582 |
+
}
|
| 583 |
+
)
|
| 584 |
+
)
|
| 585 |
+
log_started = time.perf_counter()
|
| 586 |
+
log_loss.zero_()
|
| 587 |
+
log_accuracy.zero_()
|
| 588 |
+
log_tokens = 0
|
| 589 |
+
|
| 590 |
+
if val_loader is not None and (step + 1) % config.eval_interval == 0:
|
| 591 |
+
metrics = evaluate(
|
| 592 |
+
model,
|
| 593 |
+
val_loader,
|
| 594 |
+
device,
|
| 595 |
+
precision,
|
| 596 |
+
config.mask_eps,
|
| 597 |
+
config.eval_batches,
|
| 598 |
+
)
|
| 599 |
+
print(json.dumps({"event": "validation", "step": step + 1, **metrics}))
|
| 600 |
+
|
| 601 |
+
if (step + 1) % config.save_interval == 0:
|
| 602 |
+
last_checkpoint = save_checkpoint(
|
| 603 |
+
output_dir,
|
| 604 |
+
model,
|
| 605 |
+
optimizer,
|
| 606 |
+
scaler,
|
| 607 |
+
experiment,
|
| 608 |
+
step,
|
| 609 |
+
tokens_seen,
|
| 610 |
+
config.keep_last_checkpoints,
|
| 611 |
+
data_generator,
|
| 612 |
+
micro_batches_seen,
|
| 613 |
+
)
|
| 614 |
+
print(json.dumps({"event": "checkpoint", "path": str(last_checkpoint)}))
|
| 615 |
+
|
| 616 |
+
final_step = end_step - 1
|
| 617 |
+
if final_step < start_step:
|
| 618 |
+
raise ValueError("checkpoint step is already at or beyond max_steps")
|
| 619 |
+
if not last_checkpoint.exists() or (final_step + 1) % config.save_interval != 0:
|
| 620 |
+
last_checkpoint = save_checkpoint(
|
| 621 |
+
output_dir,
|
| 622 |
+
model,
|
| 623 |
+
optimizer,
|
| 624 |
+
scaler,
|
| 625 |
+
experiment,
|
| 626 |
+
final_step,
|
| 627 |
+
tokens_seen,
|
| 628 |
+
config.keep_last_checkpoints,
|
| 629 |
+
data_generator,
|
| 630 |
+
micro_batches_seen,
|
| 631 |
+
)
|
| 632 |
+
event = "complete" if end_step == config.max_steps else "paused"
|
| 633 |
+
print(json.dumps({"event": event, "checkpoint": str(last_checkpoint)}))
|
| 634 |
+
return last_checkpoint
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def main() -> None:
|
| 638 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 639 |
+
parser.add_argument("--config", type=Path, required=True)
|
| 640 |
+
parser.add_argument("--resume", type=Path)
|
| 641 |
+
parser.add_argument(
|
| 642 |
+
"--max-run-steps",
|
| 643 |
+
type=int,
|
| 644 |
+
help="stop safely after this many optimizer steps (useful for scheduled jobs/tests)",
|
| 645 |
+
)
|
| 646 |
+
parser.add_argument("--device", help="override config device, e.g. cpu, mps, cuda")
|
| 647 |
+
parser.add_argument(
|
| 648 |
+
"--precision",
|
| 649 |
+
choices=("auto", "float32", "bfloat16", "float16"),
|
| 650 |
+
help="override config precision",
|
| 651 |
+
)
|
| 652 |
+
args = parser.parse_args()
|
| 653 |
+
|
| 654 |
+
experiment = load_config(args.config)
|
| 655 |
+
if args.device or args.precision:
|
| 656 |
+
training = replace(
|
| 657 |
+
experiment.training,
|
| 658 |
+
device=args.device or experiment.training.device,
|
| 659 |
+
precision=args.precision or experiment.training.precision,
|
| 660 |
+
)
|
| 661 |
+
experiment = replace(experiment, training=training)
|
| 662 |
+
train(experiment, resume=args.resume, max_run_steps=args.max_run_steps)
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
if __name__ == "__main__":
|
| 666 |
+
main()
|
tokenizer-qwen3-adaptive.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:561587f831d1efad45896c86901cc142551f8f22c8ddbe097890f8f0f0c1bf15
|
| 3 |
+
size 11424780
|