Transformers
Safetensors
English
hev
diffusion
hydra
state-space-model
system-one
decision-model
calibrated
jev
typed-decisions
custom_code
Eval Results
Instructions to use EryriLabs/DiffusionGemma-26B-A4B-Hev with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use EryriLabs/DiffusionGemma-26B-A4B-Hev with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("EryriLabs/DiffusionGemma-26B-A4B-Hev", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Card: compare with the leaderboard's specialist rows; Jev and Decider 1 are zero-shot generalists there. LF line endings.
Browse files- README.md +115 -115
- cache.py +185 -185
- client.py +104 -104
- config.json +312 -312
- configuration_hev.py +47 -47
- data.py +216 -216
- eval/results.json +222 -222
- features.py +345 -345
- head.py +119 -119
- hydra_torch.py +156 -156
- modeling_hev.py +163 -163
- prompting.py +118 -118
- schema.py +104 -104
README.md
CHANGED
|
@@ -1,116 +1,116 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: apache-2.0
|
| 3 |
-
base_model: google/diffusiongemma-26B-A4B-it
|
| 4 |
-
language: [en]
|
| 5 |
-
library_name: transformers
|
| 6 |
-
tags: [diffusion, hydra, state-space-model, system-one, decision-model, calibrated, jev, typed-decisions]
|
| 7 |
-
datasets: [LocalLLaMA/typed-decisions, Praveenrajus/jev-bench]
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# Hev: DiffusionGemma with a Hydra decision head
|
| 11 |
-
|
| 12 |
-
By Dwain Barnes. First published 2026-09-24.
|
| 13 |
-
|
| 14 |
-
## What it does
|
| 15 |
-
|
| 16 |
-
Give Hev a lump of text (a support ticket, an agent log, a security alert, whatever your program is looking at) and a list of typed questions, and it gives you back probabilities rather than prose. "Which team should handle this?" comes back as `billing 0.88, technical 0.12, sales 0.00`. "Is this urgent?" comes back as `0.67`. That is the job TypeSafe's Jev does; Hev does it with open weights you can run yourself.
|
| 17 |
-
|
| 18 |
-
There are two parts:
|
| 19 |
-
|
| 20 |
-
1. DiffusionGemma, Google's open text-diffusion model, frozen. I never fine-tuned it. One pass of its encoder reads your text and one pass of its bidirectional denoiser reads a small JSON answer canvas.
|
| 21 |
-
2. A Hydra head I trained on top. Hydra is a bidirectional state-space model, the two-way cousin of Mamba. It reads every token of your text and every answer slot together, in linear time, and turns that into calibrated probabilities. It is an ensemble of 3 heads (seeds 42, 43, 44) whose probabilities are averaged, 44,476,866 parameters in total, and it trains in minutes.
|
| 22 |
-
|
| 23 |
-
## What is new here
|
| 24 |
-
|
| 25 |
-
As far as I can tell (checked 2026-09-24), nobody had done any of these three things before:
|
| 26 |
-
|
| 27 |
-
- put Hydra on top of a diffusion language model's representations;
|
| 28 |
-
- used a state-space model anywhere inside a System-One or Jev-style decision model. Every open clone I found is an autoregressive Qwen or Gemma with LoRA, or a BERT-style encoder with a linear head;
|
| 29 |
-
- published an open-weights
|
| 30 |
-
|
| 31 |
-
The idea, the name and the direction are mine. If you build on it, please cite this repo; the commit history is the timestamp.
|
| 32 |
-
|
| 33 |
-
## Results
|
| 34 |
-
|
| 35 |
-
Scored on the `test` split of [LocalLLaMA/typed-decisions](https://huggingface.co/datasets/LocalLLaMA/typed-decisions) (400 cases, 2,000 decisions) in the specialist setting (the head is fitted on the train split), using the metrics defined on that dataset's card (see the evaluation notes under Limits). The "zero-shot canvas read" row is DiffusionGemma with no head, reading its own option probabilities off the canvas, which is what the vLLM structured-diffusion mode does.
|
| 36 |
-
|
| 37 |
-
| Model | n | acc | soft_acc | macro_f1 | kl | tv | brier | nll | ece | score_mae | within_1 |
|
| 38 |
-
|---|---|---|---|---|---|---|---|---|---|---|---|
|
| 39 |
-
| DiffusionGemma zero-shot canvas read | 2000 | 0.708 | 0.573 | 0.551 | 1.333 | 0.362 | 0.304 | 2.100 | 0.213 | 0.411 | 0.934 |
|
| 40 |
-
| Hev | 2000 | 0.731 | 0.584 | 0.626 | 0.185 | 0.213 | 0.094 | 0.952 | 0.162 | 0.303 | 0.976 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
## How to run it
|
| 45 |
-
|
| 46 |
-
You need a GPU with 80 GB of memory for the bf16 backbone. The head itself is tiny and runs anywhere; the backbone is the heavy part. Loading with `load_in_4bit=True` is untested: DiffusionGemma's fused expert parameters are not `nn.Linear`, so bitsandbytes leaves most weights in bf16 and the memory saving is small.
|
| 47 |
-
|
| 48 |
-
Install:
|
| 49 |
-
|
| 50 |
-
```bash
|
| 51 |
-
pip install "transformers>=5.17" accelerate datasets safetensors
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
```python
|
| 55 |
-
from transformers import AutoModel
|
| 56 |
-
|
| 57 |
-
model = AutoModel.from_pretrained("EryriLabs/DiffusionGemma-26B-A4B-Hev", trust_remote_code=True)
|
| 58 |
-
answer = model.decide(
|
| 59 |
-
state="Help! My payouts have been failing for 3 days.",
|
| 60 |
-
questions={
|
| 61 |
-
"department": {"type": "choice", "instructions": "Which team should handle this?",
|
| 62 |
-
"criteria": {"billing": "Payments, invoicing, refunds",
|
| 63 |
-
"technical": "Bugs, outages, integrations",
|
| 64 |
-
"sales": "Pricing, upgrades, new accounts"}},
|
| 65 |
-
"urgent": {"type": "noul", "instructions": "This needs attention today."},
|
| 66 |
-
"severity": {"type": "score", "instructions": "How bad is the customer impact?",
|
| 67 |
-
"criteria": ["none", "minor", "major", "critical"]},
|
| 68 |
-
},
|
| 69 |
-
)
|
| 70 |
-
print(answer["answers"])
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
The request and response shapes match Jev's `/v1/systemone` API, so existing client code needs no changes. To serve that endpoint locally, `pip install fastapi uvicorn` and run:
|
| 74 |
-
|
| 75 |
-
```python
|
| 76 |
-
import uvicorn
|
| 77 |
-
from transformers import AutoModel
|
| 78 |
-
from transformers.dynamic_module_utils import get_class_from_dynamic_module
|
| 79 |
-
|
| 80 |
-
model = AutoModel.from_pretrained("EryriLabs/DiffusionGemma-26B-A4B-Hev", trust_remote_code=True)
|
| 81 |
-
build_app = get_class_from_dynamic_module("client.build_app", "EryriLabs/DiffusionGemma-26B-A4B-Hev")
|
| 82 |
-
uvicorn.run(build_app(model), host="127.0.0.1", port=8080)
|
| 83 |
-
```
|
| 84 |
-
|
| 85 |
-
The `.py` files in this repo are a flattened copy of the `hev` package and use relative imports, so they cannot be run as scripts.
|
| 86 |
-
|
| 87 |
-
## How it was trained
|
| 88 |
-
|
| 89 |
-
- Features: one forward pass per example through the frozen backbone, canvas length 64, slot seed mode `pad`, hidden states from the final encoder and decoder norms, plus the unit-RMS outputs of layers 14 and 22 of the encoder for the prompt rows and of the decoder for the answer rows, so each row carries 8448 features.
|
| 90 |
-
- Head: linear projection 8448 to 512, 3 Hydra blocks, pointer readout (each answer slot scored against each option's description span), plus a learned residual on the backbone's own option log-probabilities, plus fitted temperatures per question type for each member (seed 42: [1.339, 1.776, 1.816]; seed 43: [1.401, 1.899, 1.832]; seed 44: [1.39, 1.616, 1.888]).
|
| 91 |
-
- Loss: KL to the gold distribution plus 0.1 times Brier. AdamW, cosine schedule, early stopping on validation log loss.
|
| 92 |
-
- Data: typed-decisions train split plus samples from 19 jev-bench sources. Dataset licences are as declared by each source on the Hub.
|
| 93 |
-
- Hydra is implemented in plain PyTorch in `hydra_torch.py`. Check against the official CUDA kernels: skipped (session 4 reuses session 2's kernel check).
|
| 94 |
-
|
| 95 |
-
## Limits
|
| 96 |
-
|
| 97 |
-
- Accuracy on typed-decisions measures agreement with a synthetic teacher, not ground truth. The teacher's own self-agreement is 0.735.
|
| 98 |
-
- Version one supports 2 to 64 options per choice question, 2 to 10 score levels, and prompts up to 1,024 tokens. English only. Text only.
|
| 99 |
-
- Request size: every question takes a few canvas tokens (its id plus the JSON around the answer slot), so the 64-token answer canvas holds about 8 questions with short ids. A longer request fails with a clear error (HTTP 422 from the server).
|
| 100 |
-
- Evaluation notes: `ece` in the table is top-label expected calibration error with 15 equal-width bins, and is not comparable with the ECE column on the dataset card. `acc` counts a hit when the top option matches the card's discrete `label` for each typed-decisions question, so ties in the gold distribution are broken the way the dataset breaks them; `soft_acc` is the gold probability of the top option.
|
| 101 |
-
- No GGUF yet. llama.cpp's DiffusionGemma support is still a draft branch and cannot expose the hidden states the head needs, and there is no Hydra kernel in ggml. If that changes I will port it. Until then this ships as safetensors.
|
| 102 |
-
- On fastino/fast-decisions, 17 domains the head never saw, it scores 0.575 exact match on the 2,600 single-label tasks against 0.611 for the backbone's own zero-shot read and 0.670 for GLiNER2.5-Decide run the same way, so as a generalist the head is a step back from the backbone; it is a specialist for the schema it was trained on.
|
| 103 |
-
- The backbone is Google's, unchanged, Apache-2.0. Everything else here is Apache-2.0 too.
|
| 104 |
-
|
| 105 |
-
## Citation
|
| 106 |
-
|
| 107 |
-
```bibtex
|
| 108 |
-
@misc{barnes2026hev,
|
| 109 |
-
title = {Hev: A Hydra decision head on frozen DiffusionGemma for calibrated System-One decisions},
|
| 110 |
-
author = {Barnes, Dwain},
|
| 111 |
-
year = {2026},
|
| 112 |
-
url = {https://huggingface.co/EryriLabs/DiffusionGemma-26B-A4B-Hev}
|
| 113 |
-
}
|
| 114 |
-
```
|
| 115 |
-
|
| 116 |
Thanks to Google DeepMind for DiffusionGemma, to Hwang, Lahoti, Dao and Gu for Hydra, to Matt Mastracci for showing that a diffusion model's canvas is a decision engine, and to the LocalLLaMA community for the typed-decisions benchmark.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: google/diffusiongemma-26B-A4B-it
|
| 4 |
+
language: [en]
|
| 5 |
+
library_name: transformers
|
| 6 |
+
tags: [diffusion, hydra, state-space-model, system-one, decision-model, calibrated, jev, typed-decisions]
|
| 7 |
+
datasets: [LocalLLaMA/typed-decisions, Praveenrajus/jev-bench]
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Hev: DiffusionGemma with a Hydra decision head
|
| 11 |
+
|
| 12 |
+
By Dwain Barnes. First published 2026-09-24.
|
| 13 |
+
|
| 14 |
+
## What it does
|
| 15 |
+
|
| 16 |
+
Give Hev a lump of text (a support ticket, an agent log, a security alert, whatever your program is looking at) and a list of typed questions, and it gives you back probabilities rather than prose. "Which team should handle this?" comes back as `billing 0.88, technical 0.12, sales 0.00`. "Is this urgent?" comes back as `0.67`. That is the job TypeSafe's Jev does; Hev does it with open weights you can run yourself.
|
| 17 |
+
|
| 18 |
+
There are two parts:
|
| 19 |
+
|
| 20 |
+
1. DiffusionGemma, Google's open text-diffusion model, frozen. I never fine-tuned it. One pass of its encoder reads your text and one pass of its bidirectional denoiser reads a small JSON answer canvas.
|
| 21 |
+
2. A Hydra head I trained on top. Hydra is a bidirectional state-space model, the two-way cousin of Mamba. It reads every token of your text and every answer slot together, in linear time, and turns that into calibrated probabilities. It is an ensemble of 3 heads (seeds 42, 43, 44) whose probabilities are averaged, 44,476,866 parameters in total, and it trains in minutes.
|
| 22 |
+
|
| 23 |
+
## What is new here
|
| 24 |
+
|
| 25 |
+
As far as I can tell (checked 2026-09-24), nobody had done any of these three things before:
|
| 26 |
+
|
| 27 |
+
- put Hydra on top of a diffusion language model's representations;
|
| 28 |
+
- used a state-space model anywhere inside a System-One or Jev-style decision model. Every open clone I found is an autoregressive Qwen or Gemma with LoRA, or a BERT-style encoder with a linear head;
|
| 29 |
+
- published an open-weights specialist that leads the specialist rows of the typed-decisions leaderboard (fitted on the train split, as they are). How it stands next to the zero-shot generalists, Jev included, is spelled out under Results, with the dataset's own warning that the two kinds of number are not comparable.
|
| 30 |
+
|
| 31 |
+
The idea, the name and the direction are mine. If you build on it, please cite this repo; the commit history is the timestamp.
|
| 32 |
+
|
| 33 |
+
## Results
|
| 34 |
+
|
| 35 |
+
Scored on the `test` split of [LocalLLaMA/typed-decisions](https://huggingface.co/datasets/LocalLLaMA/typed-decisions) (400 cases, 2,000 decisions) in the specialist setting (the head is fitted on the train split), using the metrics defined on that dataset's card (see the evaluation notes under Limits). The "zero-shot canvas read" row is DiffusionGemma with no head, reading its own option probabilities off the canvas, which is what the vLLM structured-diffusion mode does.
|
| 36 |
+
|
| 37 |
+
| Model | n | acc | soft_acc | macro_f1 | kl | tv | brier | nll | ece | score_mae | within_1 |
|
| 38 |
+
|---|---|---|---|---|---|---|---|---|---|---|---|
|
| 39 |
+
| DiffusionGemma zero-shot canvas read | 2000 | 0.708 | 0.573 | 0.551 | 1.333 | 0.362 | 0.304 | 2.100 | 0.213 | 0.411 | 0.934 |
|
| 40 |
+
| Hev | 2000 | 0.731 | 0.584 | 0.626 | 0.185 | 0.213 | 0.094 | 0.952 | 0.162 | 0.303 | 0.976 |
|
| 41 |
+
|
| 42 |
+
Hev is a specialist: its head was fitted on the train split of the same four workflows it is scored on, and it cannot answer a question schema it was not fitted for. On the dataset's leaderboard (read 2026-09-25) the comparable rows are the specialists fitted per workflow: ModernBERT-base at accuracy 0.646 and Brier 0.119, and MiniLM-L6 at 0.587 and 0.143. Hev is 8.5 points above the best of them on accuracy and its Brier is 0.025 lower. The generalist rows, scored zero-shot on workflows they never saw, are TypeSafe Jev 1.13.0 at 0.727 and 0.148 and meraGPT Decider 1 at 0.768 and 0.052. Hev's accuracy is 0.4 points above Jev's (less than one standard error, 1.0 points, on 2,000 decisions) and its Brier is 0.054 lower, but the dataset card is explicit that a specialist number next to a generalist one is not a ranking, and Decider 1 leads both on accuracy and on Brier. Hev's head lifts the frozen backbone's zero-shot accuracy from 0.708 to 0.731 and reaches Brier 0.094.
|
| 43 |
+
|
| 44 |
+
## How to run it
|
| 45 |
+
|
| 46 |
+
You need a GPU with 80 GB of memory for the bf16 backbone. The head itself is tiny and runs anywhere; the backbone is the heavy part. Loading with `load_in_4bit=True` is untested: DiffusionGemma's fused expert parameters are not `nn.Linear`, so bitsandbytes leaves most weights in bf16 and the memory saving is small.
|
| 47 |
+
|
| 48 |
+
Install:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
pip install "transformers>=5.17" accelerate datasets safetensors
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
from transformers import AutoModel
|
| 56 |
+
|
| 57 |
+
model = AutoModel.from_pretrained("EryriLabs/DiffusionGemma-26B-A4B-Hev", trust_remote_code=True)
|
| 58 |
+
answer = model.decide(
|
| 59 |
+
state="Help! My payouts have been failing for 3 days.",
|
| 60 |
+
questions={
|
| 61 |
+
"department": {"type": "choice", "instructions": "Which team should handle this?",
|
| 62 |
+
"criteria": {"billing": "Payments, invoicing, refunds",
|
| 63 |
+
"technical": "Bugs, outages, integrations",
|
| 64 |
+
"sales": "Pricing, upgrades, new accounts"}},
|
| 65 |
+
"urgent": {"type": "noul", "instructions": "This needs attention today."},
|
| 66 |
+
"severity": {"type": "score", "instructions": "How bad is the customer impact?",
|
| 67 |
+
"criteria": ["none", "minor", "major", "critical"]},
|
| 68 |
+
},
|
| 69 |
+
)
|
| 70 |
+
print(answer["answers"])
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
The request and response shapes match Jev's `/v1/systemone` API, so existing client code needs no changes. To serve that endpoint locally, `pip install fastapi uvicorn` and run:
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
import uvicorn
|
| 77 |
+
from transformers import AutoModel
|
| 78 |
+
from transformers.dynamic_module_utils import get_class_from_dynamic_module
|
| 79 |
+
|
| 80 |
+
model = AutoModel.from_pretrained("EryriLabs/DiffusionGemma-26B-A4B-Hev", trust_remote_code=True)
|
| 81 |
+
build_app = get_class_from_dynamic_module("client.build_app", "EryriLabs/DiffusionGemma-26B-A4B-Hev")
|
| 82 |
+
uvicorn.run(build_app(model), host="127.0.0.1", port=8080)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
The `.py` files in this repo are a flattened copy of the `hev` package and use relative imports, so they cannot be run as scripts.
|
| 86 |
+
|
| 87 |
+
## How it was trained
|
| 88 |
+
|
| 89 |
+
- Features: one forward pass per example through the frozen backbone, canvas length 64, slot seed mode `pad`, hidden states from the final encoder and decoder norms, plus the unit-RMS outputs of layers 14 and 22 of the encoder for the prompt rows and of the decoder for the answer rows, so each row carries 8448 features.
|
| 90 |
+
- Head: linear projection 8448 to 512, 3 Hydra blocks, pointer readout (each answer slot scored against each option's description span), plus a learned residual on the backbone's own option log-probabilities, plus fitted temperatures per question type for each member (seed 42: [1.339, 1.776, 1.816]; seed 43: [1.401, 1.899, 1.832]; seed 44: [1.39, 1.616, 1.888]).
|
| 91 |
+
- Loss: KL to the gold distribution plus 0.1 times Brier. AdamW, cosine schedule, early stopping on validation log loss.
|
| 92 |
+
- Data: typed-decisions train split plus samples from 19 jev-bench sources. Dataset licences are as declared by each source on the Hub.
|
| 93 |
+
- Hydra is implemented in plain PyTorch in `hydra_torch.py`. Check against the official CUDA kernels: skipped (session 4 reuses session 2's kernel check).
|
| 94 |
+
|
| 95 |
+
## Limits
|
| 96 |
+
|
| 97 |
+
- Accuracy on typed-decisions measures agreement with a synthetic teacher, not ground truth. The teacher's own self-agreement is 0.735.
|
| 98 |
+
- Version one supports 2 to 64 options per choice question, 2 to 10 score levels, and prompts up to 1,024 tokens. English only. Text only.
|
| 99 |
+
- Request size: every question takes a few canvas tokens (its id plus the JSON around the answer slot), so the 64-token answer canvas holds about 8 questions with short ids. A longer request fails with a clear error (HTTP 422 from the server).
|
| 100 |
+
- Evaluation notes: `ece` in the table is top-label expected calibration error with 15 equal-width bins, and is not comparable with the ECE column on the dataset card. `acc` counts a hit when the top option matches the card's discrete `label` for each typed-decisions question, so ties in the gold distribution are broken the way the dataset breaks them; `soft_acc` is the gold probability of the top option.
|
| 101 |
+
- No GGUF yet. llama.cpp's DiffusionGemma support is still a draft branch and cannot expose the hidden states the head needs, and there is no Hydra kernel in ggml. If that changes I will port it. Until then this ships as safetensors.
|
| 102 |
+
- On fastino/fast-decisions, 17 domains the head never saw, it scores 0.575 exact match on the 2,600 single-label tasks against 0.611 for the backbone's own zero-shot read and 0.670 for GLiNER2.5-Decide run the same way, so as a generalist the head is a step back from the backbone; it is a specialist for the schema it was trained on.
|
| 103 |
+
- The backbone is Google's, unchanged, Apache-2.0. Everything else here is Apache-2.0 too.
|
| 104 |
+
|
| 105 |
+
## Citation
|
| 106 |
+
|
| 107 |
+
```bibtex
|
| 108 |
+
@misc{barnes2026hev,
|
| 109 |
+
title = {Hev: A Hydra decision head on frozen DiffusionGemma for calibrated System-One decisions},
|
| 110 |
+
author = {Barnes, Dwain},
|
| 111 |
+
year = {2026},
|
| 112 |
+
url = {https://huggingface.co/EryriLabs/DiffusionGemma-26B-A4B-Hev}
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
Thanks to Google DeepMind for DiffusionGemma, to Hwang, Lahoti, Dao and Gu for Hydra, to Matt Mastracci for showing that a diffusion model's canvas is a decision engine, and to the LocalLLaMA community for the typed-decisions benchmark.
|
cache.py
CHANGED
|
@@ -1,185 +1,185 @@
|
|
| 1 |
-
"""On-disk feature cache: one safetensors file per example plus a JSONL manifest."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
from dataclasses import asdict, dataclass, field
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from typing import Optional
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
from safetensors.torch import load_file, save_file
|
| 11 |
-
|
| 12 |
-
from .schema import TYPE_INDEX
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@dataclass
|
| 16 |
-
class Decision:
|
| 17 |
-
qid: str
|
| 18 |
-
type: str
|
| 19 |
-
slot: int
|
| 20 |
-
spans: list[list[int]]
|
| 21 |
-
alias: list[int]
|
| 22 |
-
prior: list[float]
|
| 23 |
-
gold: Optional[list[float]]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
@dataclass
|
| 27 |
-
class Record:
|
| 28 |
-
id: str
|
| 29 |
-
source: str
|
| 30 |
-
path: str
|
| 31 |
-
n_prompt: int
|
| 32 |
-
n_canvas: int
|
| 33 |
-
decisions: list[Decision] = field(default_factory=list)
|
| 34 |
-
|
| 35 |
-
def to_json(self) -> str:
|
| 36 |
-
return json.dumps(asdict(self))
|
| 37 |
-
|
| 38 |
-
@classmethod
|
| 39 |
-
def from_json(cls, line: str) -> "Record":
|
| 40 |
-
d = json.loads(line)
|
| 41 |
-
d["decisions"] = [Decision(**x) for x in d["decisions"]]
|
| 42 |
-
return cls(**d)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
class CacheWriter:
|
| 46 |
-
"""Writes one safetensors file per example and a JSONL manifest.
|
| 47 |
-
|
| 48 |
-
A writer always starts a fresh manifest, so re-running into the same folder does not duplicate records.
|
| 49 |
-
"""
|
| 50 |
-
|
| 51 |
-
def __init__(self, out_dir):
|
| 52 |
-
self.dir = Path(out_dir)
|
| 53 |
-
self.dir.mkdir(parents=True, exist_ok=True)
|
| 54 |
-
self._f = (self.dir / "manifest.jsonl").open("w", encoding="utf-8")
|
| 55 |
-
|
| 56 |
-
def write(self, record: Record, feats: torch.Tensor) -> None:
|
| 57 |
-
save_file({"feats": feats.detach().to("cpu", torch.float16).contiguous()}, str(self.dir / record.path))
|
| 58 |
-
self._f.write(record.to_json() + "\n")
|
| 59 |
-
self._f.flush()
|
| 60 |
-
|
| 61 |
-
def close(self) -> None:
|
| 62 |
-
self._f.close()
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
class FeatureCache:
|
| 66 |
-
def __init__(self, cache_dir):
|
| 67 |
-
self.dir = Path(cache_dir)
|
| 68 |
-
with (self.dir / "manifest.jsonl").open("r", encoding="utf-8") as f:
|
| 69 |
-
self.records = [Record.from_json(line) for line in f if line.strip()]
|
| 70 |
-
|
| 71 |
-
def __len__(self) -> int:
|
| 72 |
-
return len(self.records)
|
| 73 |
-
|
| 74 |
-
def __getitem__(self, i: int) -> tuple[Record, torch.Tensor]:
|
| 75 |
-
rec = self.records[i]
|
| 76 |
-
return rec, load_file(str(self.dir / rec.path))["feats"]
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
@dataclass
|
| 80 |
-
class Batch:
|
| 81 |
-
"""A padded feature batch plus two views of its decisions.
|
| 82 |
-
|
| 83 |
-
`decisions` is the per-decision list (metadata, spans, prior, gold) that evaluation, serving and the tests
|
| 84 |
-
read. The flat tensors are what the head and the losses compute on: options are numbered decision by
|
| 85 |
-
decision in that same order, so one gather, one index_add and one padded log-softmax cover the whole batch
|
| 86 |
-
instead of a Python loop issuing hundreds of tiny ops per decision (session 3's CPU-bound trainer).
|
| 87 |
-
"""
|
| 88 |
-
feats: torch.Tensor
|
| 89 |
-
seg: torch.Tensor
|
| 90 |
-
mask: torch.Tensor
|
| 91 |
-
decisions: list[dict]
|
| 92 |
-
dec_b: torch.Tensor # (n_dec,) batch row of each decision
|
| 93 |
-
dec_slot: torch.Tensor # (n_dec,) answer-slot row of each decision
|
| 94 |
-
dec_type: torch.Tensor # (n_dec,) question type id
|
| 95 |
-
dec_has_gold: torch.Tensor # (n_dec,) bool
|
| 96 |
-
opt_dec: torch.Tensor # (n_opt,) decision index of each option
|
| 97 |
-
opt_pos: torch.Tensor # (n_opt,) position of each option within its decision
|
| 98 |
-
opt_prior: torch.Tensor # (n_opt,) backbone log-prior per option
|
| 99 |
-
opt_gold: torch.Tensor # (n_opt,) gold probability per option, 0 where the decision has no gold
|
| 100 |
-
span_rows: torch.Tensor # (n_span_rows,) row index into the padded (B * L) feature rows, option by option
|
| 101 |
-
span_opt: torch.Tensor # (n_span_rows,) option index of each span row
|
| 102 |
-
span_len: torch.Tensor # (n_opt,) span length per option, float
|
| 103 |
-
counts: list[int] # options per decision; plain Python so torch.split never syncs on it
|
| 104 |
-
n_gold: int # decisions with gold; plain Python so the losses never sync on it
|
| 105 |
-
|
| 106 |
-
_FLAT = ("dec_b", "dec_slot", "dec_type", "dec_has_gold", "opt_dec", "opt_pos", "opt_prior", "opt_gold",
|
| 107 |
-
"span_rows", "span_opt", "span_len")
|
| 108 |
-
|
| 109 |
-
def to(self, device) -> "Batch":
|
| 110 |
-
# spans stay on the CPU: consumers read them back as Python ints, so a device copy would only add a
|
| 111 |
-
# device-to-host sync per decision.
|
| 112 |
-
return Batch(self.feats.to(device), self.seg.to(device), self.mask.to(device),
|
| 113 |
-
[{**d, "prior": d["prior"].to(device),
|
| 114 |
-
"gold": None if d["gold"] is None else d["gold"].to(device)} for d in self.decisions],
|
| 115 |
-
*(getattr(self, name).to(device) for name in self._FLAT), self.counts, self.n_gold)
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def _check_record(rec: Record, n_rows: int) -> None:
|
| 119 |
-
"""Raise a ValueError naming the record and question when a record does not fit its feature rows.
|
| 120 |
-
|
| 121 |
-
Rows [0, n_prompt) hold the prompt and [n_prompt, n_prompt + n_canvas) the canvas. Without this check a bad
|
| 122 |
-
record would be read silently from padding or from the wrong segment.
|
| 123 |
-
"""
|
| 124 |
-
if n_rows != rec.n_prompt + rec.n_canvas:
|
| 125 |
-
raise ValueError(f"record {rec.id!r}: {n_rows} feature rows, but n_prompt {rec.n_prompt} + n_canvas "
|
| 126 |
-
f"{rec.n_canvas} = {rec.n_prompt + rec.n_canvas}")
|
| 127 |
-
for d in rec.decisions:
|
| 128 |
-
where = f"record {rec.id!r}, question {d.qid!r}"
|
| 129 |
-
if not rec.n_prompt <= d.slot < n_rows:
|
| 130 |
-
raise ValueError(f"{where}: slot {d.slot} is outside the canvas rows [{rec.n_prompt}, {n_rows})")
|
| 131 |
-
for s, e in d.spans:
|
| 132 |
-
if not 0 <= s < e <= rec.n_prompt:
|
| 133 |
-
raise ValueError(f"{where}: option span [{s}, {e}) is empty or outside the prompt rows "
|
| 134 |
-
f"[0, {rec.n_prompt})")
|
| 135 |
-
if len(d.prior) != len(d.spans):
|
| 136 |
-
raise ValueError(f"{where}: {len(d.spans)} option spans but {len(d.prior)} prior values")
|
| 137 |
-
if d.gold is not None and len(d.gold) != len(d.spans):
|
| 138 |
-
raise ValueError(f"{where}: {len(d.spans)} option spans but {len(d.gold)} gold values")
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
def collate(items: list[tuple[Record, torch.Tensor]]) -> Batch:
|
| 142 |
-
lmax = max(f.shape[0] for _, f in items)
|
| 143 |
-
dim = items[0][1].shape[1]
|
| 144 |
-
feats = torch.zeros(len(items), lmax, dim, dtype=torch.float16)
|
| 145 |
-
seg = torch.zeros(len(items), lmax, dtype=torch.long)
|
| 146 |
-
mask = torch.zeros(len(items), lmax, dtype=torch.bool)
|
| 147 |
-
decisions = []
|
| 148 |
-
dec_b, dec_slot, dec_type, dec_has_gold, counts = [], [], [], [], []
|
| 149 |
-
opt_dec, opt_pos, opt_prior, opt_gold, span_rows, span_opt, span_len = [], [], [], [], [], [], []
|
| 150 |
-
for b, (rec, f) in enumerate(items):
|
| 151 |
-
n = f.shape[0]
|
| 152 |
-
_check_record(rec, n)
|
| 153 |
-
feats[b, :n] = f.to(torch.float16)
|
| 154 |
-
mask[b, :n] = True
|
| 155 |
-
seg[b, rec.n_prompt:n] = 1
|
| 156 |
-
for d in rec.decisions:
|
| 157 |
-
i = len(decisions)
|
| 158 |
-
decisions.append({
|
| 159 |
-
"b": b, "slot": d.slot, "type_id": TYPE_INDEX[d.type], "qid": d.qid, "type": d.type,
|
| 160 |
-
"spans": torch.tensor(d.spans, dtype=torch.long),
|
| 161 |
-
"prior": torch.tensor(d.prior, dtype=torch.float32),
|
| 162 |
-
"gold": None if d.gold is None else torch.tensor(d.gold, dtype=torch.float32),
|
| 163 |
-
})
|
| 164 |
-
dec_b.append(b)
|
| 165 |
-
dec_slot.append(d.slot)
|
| 166 |
-
dec_type.append(TYPE_INDEX[d.type])
|
| 167 |
-
dec_has_gold.append(d.gold is not None)
|
| 168 |
-
counts.append(len(d.spans))
|
| 169 |
-
for j, (s, e) in enumerate(d.spans):
|
| 170 |
-
o = len(opt_dec)
|
| 171 |
-
opt_dec.append(i)
|
| 172 |
-
opt_pos.append(j)
|
| 173 |
-
opt_prior.append(d.prior[j])
|
| 174 |
-
opt_gold.append(0.0 if d.gold is None else d.gold[j])
|
| 175 |
-
span_rows.extend(range(b * lmax + s, b * lmax + e))
|
| 176 |
-
span_opt.extend([o] * (e - s))
|
| 177 |
-
span_len.append(float(e - s))
|
| 178 |
-
long, f32 = torch.long, torch.float32
|
| 179 |
-
return Batch(feats=feats, seg=seg, mask=mask, decisions=decisions,
|
| 180 |
-
dec_b=torch.tensor(dec_b, dtype=long), dec_slot=torch.tensor(dec_slot, dtype=long),
|
| 181 |
-
dec_type=torch.tensor(dec_type, dtype=long), dec_has_gold=torch.tensor(dec_has_gold, dtype=torch.bool),
|
| 182 |
-
opt_dec=torch.tensor(opt_dec, dtype=long), opt_pos=torch.tensor(opt_pos, dtype=long),
|
| 183 |
-
opt_prior=torch.tensor(opt_prior, dtype=f32), opt_gold=torch.tensor(opt_gold, dtype=f32),
|
| 184 |
-
span_rows=torch.tensor(span_rows, dtype=long), span_opt=torch.tensor(span_opt, dtype=long),
|
| 185 |
-
span_len=torch.tensor(span_len, dtype=f32), counts=counts, n_gold=sum(dec_has_gold))
|
|
|
|
| 1 |
+
"""On-disk feature cache: one safetensors file per example plus a JSONL manifest."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from dataclasses import asdict, dataclass, field
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from safetensors.torch import load_file, save_file
|
| 11 |
+
|
| 12 |
+
from .schema import TYPE_INDEX
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class Decision:
|
| 17 |
+
qid: str
|
| 18 |
+
type: str
|
| 19 |
+
slot: int
|
| 20 |
+
spans: list[list[int]]
|
| 21 |
+
alias: list[int]
|
| 22 |
+
prior: list[float]
|
| 23 |
+
gold: Optional[list[float]]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class Record:
|
| 28 |
+
id: str
|
| 29 |
+
source: str
|
| 30 |
+
path: str
|
| 31 |
+
n_prompt: int
|
| 32 |
+
n_canvas: int
|
| 33 |
+
decisions: list[Decision] = field(default_factory=list)
|
| 34 |
+
|
| 35 |
+
def to_json(self) -> str:
|
| 36 |
+
return json.dumps(asdict(self))
|
| 37 |
+
|
| 38 |
+
@classmethod
|
| 39 |
+
def from_json(cls, line: str) -> "Record":
|
| 40 |
+
d = json.loads(line)
|
| 41 |
+
d["decisions"] = [Decision(**x) for x in d["decisions"]]
|
| 42 |
+
return cls(**d)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class CacheWriter:
|
| 46 |
+
"""Writes one safetensors file per example and a JSONL manifest.
|
| 47 |
+
|
| 48 |
+
A writer always starts a fresh manifest, so re-running into the same folder does not duplicate records.
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def __init__(self, out_dir):
|
| 52 |
+
self.dir = Path(out_dir)
|
| 53 |
+
self.dir.mkdir(parents=True, exist_ok=True)
|
| 54 |
+
self._f = (self.dir / "manifest.jsonl").open("w", encoding="utf-8")
|
| 55 |
+
|
| 56 |
+
def write(self, record: Record, feats: torch.Tensor) -> None:
|
| 57 |
+
save_file({"feats": feats.detach().to("cpu", torch.float16).contiguous()}, str(self.dir / record.path))
|
| 58 |
+
self._f.write(record.to_json() + "\n")
|
| 59 |
+
self._f.flush()
|
| 60 |
+
|
| 61 |
+
def close(self) -> None:
|
| 62 |
+
self._f.close()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class FeatureCache:
|
| 66 |
+
def __init__(self, cache_dir):
|
| 67 |
+
self.dir = Path(cache_dir)
|
| 68 |
+
with (self.dir / "manifest.jsonl").open("r", encoding="utf-8") as f:
|
| 69 |
+
self.records = [Record.from_json(line) for line in f if line.strip()]
|
| 70 |
+
|
| 71 |
+
def __len__(self) -> int:
|
| 72 |
+
return len(self.records)
|
| 73 |
+
|
| 74 |
+
def __getitem__(self, i: int) -> tuple[Record, torch.Tensor]:
|
| 75 |
+
rec = self.records[i]
|
| 76 |
+
return rec, load_file(str(self.dir / rec.path))["feats"]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@dataclass
|
| 80 |
+
class Batch:
|
| 81 |
+
"""A padded feature batch plus two views of its decisions.
|
| 82 |
+
|
| 83 |
+
`decisions` is the per-decision list (metadata, spans, prior, gold) that evaluation, serving and the tests
|
| 84 |
+
read. The flat tensors are what the head and the losses compute on: options are numbered decision by
|
| 85 |
+
decision in that same order, so one gather, one index_add and one padded log-softmax cover the whole batch
|
| 86 |
+
instead of a Python loop issuing hundreds of tiny ops per decision (session 3's CPU-bound trainer).
|
| 87 |
+
"""
|
| 88 |
+
feats: torch.Tensor
|
| 89 |
+
seg: torch.Tensor
|
| 90 |
+
mask: torch.Tensor
|
| 91 |
+
decisions: list[dict]
|
| 92 |
+
dec_b: torch.Tensor # (n_dec,) batch row of each decision
|
| 93 |
+
dec_slot: torch.Tensor # (n_dec,) answer-slot row of each decision
|
| 94 |
+
dec_type: torch.Tensor # (n_dec,) question type id
|
| 95 |
+
dec_has_gold: torch.Tensor # (n_dec,) bool
|
| 96 |
+
opt_dec: torch.Tensor # (n_opt,) decision index of each option
|
| 97 |
+
opt_pos: torch.Tensor # (n_opt,) position of each option within its decision
|
| 98 |
+
opt_prior: torch.Tensor # (n_opt,) backbone log-prior per option
|
| 99 |
+
opt_gold: torch.Tensor # (n_opt,) gold probability per option, 0 where the decision has no gold
|
| 100 |
+
span_rows: torch.Tensor # (n_span_rows,) row index into the padded (B * L) feature rows, option by option
|
| 101 |
+
span_opt: torch.Tensor # (n_span_rows,) option index of each span row
|
| 102 |
+
span_len: torch.Tensor # (n_opt,) span length per option, float
|
| 103 |
+
counts: list[int] # options per decision; plain Python so torch.split never syncs on it
|
| 104 |
+
n_gold: int # decisions with gold; plain Python so the losses never sync on it
|
| 105 |
+
|
| 106 |
+
_FLAT = ("dec_b", "dec_slot", "dec_type", "dec_has_gold", "opt_dec", "opt_pos", "opt_prior", "opt_gold",
|
| 107 |
+
"span_rows", "span_opt", "span_len")
|
| 108 |
+
|
| 109 |
+
def to(self, device) -> "Batch":
|
| 110 |
+
# spans stay on the CPU: consumers read them back as Python ints, so a device copy would only add a
|
| 111 |
+
# device-to-host sync per decision.
|
| 112 |
+
return Batch(self.feats.to(device), self.seg.to(device), self.mask.to(device),
|
| 113 |
+
[{**d, "prior": d["prior"].to(device),
|
| 114 |
+
"gold": None if d["gold"] is None else d["gold"].to(device)} for d in self.decisions],
|
| 115 |
+
*(getattr(self, name).to(device) for name in self._FLAT), self.counts, self.n_gold)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _check_record(rec: Record, n_rows: int) -> None:
|
| 119 |
+
"""Raise a ValueError naming the record and question when a record does not fit its feature rows.
|
| 120 |
+
|
| 121 |
+
Rows [0, n_prompt) hold the prompt and [n_prompt, n_prompt + n_canvas) the canvas. Without this check a bad
|
| 122 |
+
record would be read silently from padding or from the wrong segment.
|
| 123 |
+
"""
|
| 124 |
+
if n_rows != rec.n_prompt + rec.n_canvas:
|
| 125 |
+
raise ValueError(f"record {rec.id!r}: {n_rows} feature rows, but n_prompt {rec.n_prompt} + n_canvas "
|
| 126 |
+
f"{rec.n_canvas} = {rec.n_prompt + rec.n_canvas}")
|
| 127 |
+
for d in rec.decisions:
|
| 128 |
+
where = f"record {rec.id!r}, question {d.qid!r}"
|
| 129 |
+
if not rec.n_prompt <= d.slot < n_rows:
|
| 130 |
+
raise ValueError(f"{where}: slot {d.slot} is outside the canvas rows [{rec.n_prompt}, {n_rows})")
|
| 131 |
+
for s, e in d.spans:
|
| 132 |
+
if not 0 <= s < e <= rec.n_prompt:
|
| 133 |
+
raise ValueError(f"{where}: option span [{s}, {e}) is empty or outside the prompt rows "
|
| 134 |
+
f"[0, {rec.n_prompt})")
|
| 135 |
+
if len(d.prior) != len(d.spans):
|
| 136 |
+
raise ValueError(f"{where}: {len(d.spans)} option spans but {len(d.prior)} prior values")
|
| 137 |
+
if d.gold is not None and len(d.gold) != len(d.spans):
|
| 138 |
+
raise ValueError(f"{where}: {len(d.spans)} option spans but {len(d.gold)} gold values")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def collate(items: list[tuple[Record, torch.Tensor]]) -> Batch:
|
| 142 |
+
lmax = max(f.shape[0] for _, f in items)
|
| 143 |
+
dim = items[0][1].shape[1]
|
| 144 |
+
feats = torch.zeros(len(items), lmax, dim, dtype=torch.float16)
|
| 145 |
+
seg = torch.zeros(len(items), lmax, dtype=torch.long)
|
| 146 |
+
mask = torch.zeros(len(items), lmax, dtype=torch.bool)
|
| 147 |
+
decisions = []
|
| 148 |
+
dec_b, dec_slot, dec_type, dec_has_gold, counts = [], [], [], [], []
|
| 149 |
+
opt_dec, opt_pos, opt_prior, opt_gold, span_rows, span_opt, span_len = [], [], [], [], [], [], []
|
| 150 |
+
for b, (rec, f) in enumerate(items):
|
| 151 |
+
n = f.shape[0]
|
| 152 |
+
_check_record(rec, n)
|
| 153 |
+
feats[b, :n] = f.to(torch.float16)
|
| 154 |
+
mask[b, :n] = True
|
| 155 |
+
seg[b, rec.n_prompt:n] = 1
|
| 156 |
+
for d in rec.decisions:
|
| 157 |
+
i = len(decisions)
|
| 158 |
+
decisions.append({
|
| 159 |
+
"b": b, "slot": d.slot, "type_id": TYPE_INDEX[d.type], "qid": d.qid, "type": d.type,
|
| 160 |
+
"spans": torch.tensor(d.spans, dtype=torch.long),
|
| 161 |
+
"prior": torch.tensor(d.prior, dtype=torch.float32),
|
| 162 |
+
"gold": None if d.gold is None else torch.tensor(d.gold, dtype=torch.float32),
|
| 163 |
+
})
|
| 164 |
+
dec_b.append(b)
|
| 165 |
+
dec_slot.append(d.slot)
|
| 166 |
+
dec_type.append(TYPE_INDEX[d.type])
|
| 167 |
+
dec_has_gold.append(d.gold is not None)
|
| 168 |
+
counts.append(len(d.spans))
|
| 169 |
+
for j, (s, e) in enumerate(d.spans):
|
| 170 |
+
o = len(opt_dec)
|
| 171 |
+
opt_dec.append(i)
|
| 172 |
+
opt_pos.append(j)
|
| 173 |
+
opt_prior.append(d.prior[j])
|
| 174 |
+
opt_gold.append(0.0 if d.gold is None else d.gold[j])
|
| 175 |
+
span_rows.extend(range(b * lmax + s, b * lmax + e))
|
| 176 |
+
span_opt.extend([o] * (e - s))
|
| 177 |
+
span_len.append(float(e - s))
|
| 178 |
+
long, f32 = torch.long, torch.float32
|
| 179 |
+
return Batch(feats=feats, seg=seg, mask=mask, decisions=decisions,
|
| 180 |
+
dec_b=torch.tensor(dec_b, dtype=long), dec_slot=torch.tensor(dec_slot, dtype=long),
|
| 181 |
+
dec_type=torch.tensor(dec_type, dtype=long), dec_has_gold=torch.tensor(dec_has_gold, dtype=torch.bool),
|
| 182 |
+
opt_dec=torch.tensor(opt_dec, dtype=long), opt_pos=torch.tensor(opt_pos, dtype=long),
|
| 183 |
+
opt_prior=torch.tensor(opt_prior, dtype=f32), opt_gold=torch.tensor(opt_gold, dtype=f32),
|
| 184 |
+
span_rows=torch.tensor(span_rows, dtype=long), span_opt=torch.tensor(span_opt, dtype=long),
|
| 185 |
+
span_len=torch.tensor(span_len, dtype=f32), counts=counts, n_gold=sum(dec_has_gold))
|
client.py
CHANGED
|
@@ -1,104 +1,104 @@
|
|
| 1 |
-
"""Jev-shaped request/response wrapper around HevForDecisions, plus an optional FastAPI server."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
import threading
|
| 7 |
-
|
| 8 |
-
from .schema import Question
|
| 9 |
-
|
| 10 |
-
MAX_QUESTIONS = 32
|
| 11 |
-
# A question's canvas entry (its id, the JSON around it and the answer slot) takes 6 or more DiffusionGemma tokens,
|
| 12 |
-
# so 4 is a safe floor for the early check below.
|
| 13 |
-
MIN_CANVAS_TOKENS_PER_QUESTION = 4
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def max_questions(model) -> int:
|
| 17 |
-
"""Upper bound on questions per request for `model`'s answer canvas (32 when the model has no canvas_length).
|
| 18 |
-
|
| 19 |
-
Every question needs more than MIN_CANVAS_TOKENS_PER_QUESTION canvas tokens, so this never rejects a request that
|
| 20 |
-
would fit. With short ids a 64-token canvas holds about 8 questions (9 one-letter ids, 7 word ids); the exact
|
| 21 |
-
check happens when the canvas is built and raises ValueError, which the server turns into a 422.
|
| 22 |
-
"""
|
| 23 |
-
canvas = getattr(getattr(model, "config", None), "canvas_length", None)
|
| 24 |
-
if not isinstance(canvas, int) or isinstance(canvas, bool) or canvas <= 0:
|
| 25 |
-
return MAX_QUESTIONS
|
| 26 |
-
return max(1, min(MAX_QUESTIONS, canvas // MIN_CANVAS_TOKENS_PER_QUESTION))
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def validate_request(request: dict, max_q: int = MAX_QUESTIONS) -> None:
|
| 30 |
-
if not isinstance(request, dict) or "state" not in request:
|
| 31 |
-
raise ValueError("request needs a 'state' (string, object or array)")
|
| 32 |
-
qs = request.get("questions")
|
| 33 |
-
if not isinstance(qs, dict) or not qs:
|
| 34 |
-
raise ValueError("request needs a non-empty 'questions' mapping")
|
| 35 |
-
for qid, spec in qs.items():
|
| 36 |
-
if not isinstance(spec, dict):
|
| 37 |
-
raise ValueError(f"question {qid!r} must be an object")
|
| 38 |
-
try:
|
| 39 |
-
Question.from_jev(qid, spec)
|
| 40 |
-
except ValueError as err:
|
| 41 |
-
raise ValueError(f"question {qid!r}: {err}") from err
|
| 42 |
-
n = len(qs)
|
| 43 |
-
if n > max_q:
|
| 44 |
-
raise ValueError(f"at most {max_q} questions per request fit this model's answer canvas, got {n}")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _state_text(state) -> str:
|
| 48 |
-
return state if isinstance(state, str) else json.dumps(state, ensure_ascii=False)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def decide(model, request: dict) -> dict:
|
| 52 |
-
validate_request(request, max_questions(model))
|
| 53 |
-
resp = model.decide(_state_text(request["state"]), request["questions"])
|
| 54 |
-
resp["model"] = request.get("model", "hev")
|
| 55 |
-
return resp
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def build_app(model):
|
| 59 |
-
"""FastAPI app serving `model` at POST /v1/systemone and GET /health.
|
| 60 |
-
|
| 61 |
-
FastAPI runs plain `def` endpoints in a thread pool, and HevForDecisions.decide() is not
|
| 62 |
-
thread-safe (it shares one hook-capture buffer on the backbone), so calls to the model are
|
| 63 |
-
serialised with a lock: concurrent requests queue and are answered one at a time.
|
| 64 |
-
"""
|
| 65 |
-
from fastapi import FastAPI, HTTPException
|
| 66 |
-
|
| 67 |
-
app = FastAPI(title="Hev", version="0.1.0")
|
| 68 |
-
model_lock = threading.Lock()
|
| 69 |
-
|
| 70 |
-
@app.get("/health")
|
| 71 |
-
def health():
|
| 72 |
-
return {"ok": True}
|
| 73 |
-
|
| 74 |
-
@app.post("/v1/systemone")
|
| 75 |
-
def systemone(request: dict):
|
| 76 |
-
try:
|
| 77 |
-
with model_lock:
|
| 78 |
-
return decide(model, request)
|
| 79 |
-
except ValueError as err:
|
| 80 |
-
raise HTTPException(status_code=422, detail=str(err)) from err
|
| 81 |
-
|
| 82 |
-
return app
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def main():
|
| 86 |
-
ap = argparse.ArgumentParser()
|
| 87 |
-
ap.add_argument("--repo", required=True)
|
| 88 |
-
ap.add_argument("--port", type=int, default=8080)
|
| 89 |
-
ap.add_argument("--host", default="127.0.0.1")
|
| 90 |
-
ap.add_argument("--load-in-4bit", action="store_true")
|
| 91 |
-
ap.add_argument("--request", default=None, help="JSON file: answer once and exit")
|
| 92 |
-
args = ap.parse_args()
|
| 93 |
-
from .modeling_hev import HevForDecisions
|
| 94 |
-
model = HevForDecisions.from_pretrained(args.repo, load_in_4bit=args.load_in_4bit)
|
| 95 |
-
if args.request:
|
| 96 |
-
with open(args.request, "r", encoding="utf-8") as f:
|
| 97 |
-
print(json.dumps(decide(model, json.load(f)), indent=2))
|
| 98 |
-
return
|
| 99 |
-
import uvicorn
|
| 100 |
-
uvicorn.run(build_app(model), host=args.host, port=args.port)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
if __name__ == "__main__":
|
| 104 |
-
main()
|
|
|
|
| 1 |
+
"""Jev-shaped request/response wrapper around HevForDecisions, plus an optional FastAPI server."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import threading
|
| 7 |
+
|
| 8 |
+
from .schema import Question
|
| 9 |
+
|
| 10 |
+
MAX_QUESTIONS = 32
|
| 11 |
+
# A question's canvas entry (its id, the JSON around it and the answer slot) takes 6 or more DiffusionGemma tokens,
|
| 12 |
+
# so 4 is a safe floor for the early check below.
|
| 13 |
+
MIN_CANVAS_TOKENS_PER_QUESTION = 4
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def max_questions(model) -> int:
|
| 17 |
+
"""Upper bound on questions per request for `model`'s answer canvas (32 when the model has no canvas_length).
|
| 18 |
+
|
| 19 |
+
Every question needs more than MIN_CANVAS_TOKENS_PER_QUESTION canvas tokens, so this never rejects a request that
|
| 20 |
+
would fit. With short ids a 64-token canvas holds about 8 questions (9 one-letter ids, 7 word ids); the exact
|
| 21 |
+
check happens when the canvas is built and raises ValueError, which the server turns into a 422.
|
| 22 |
+
"""
|
| 23 |
+
canvas = getattr(getattr(model, "config", None), "canvas_length", None)
|
| 24 |
+
if not isinstance(canvas, int) or isinstance(canvas, bool) or canvas <= 0:
|
| 25 |
+
return MAX_QUESTIONS
|
| 26 |
+
return max(1, min(MAX_QUESTIONS, canvas // MIN_CANVAS_TOKENS_PER_QUESTION))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def validate_request(request: dict, max_q: int = MAX_QUESTIONS) -> None:
|
| 30 |
+
if not isinstance(request, dict) or "state" not in request:
|
| 31 |
+
raise ValueError("request needs a 'state' (string, object or array)")
|
| 32 |
+
qs = request.get("questions")
|
| 33 |
+
if not isinstance(qs, dict) or not qs:
|
| 34 |
+
raise ValueError("request needs a non-empty 'questions' mapping")
|
| 35 |
+
for qid, spec in qs.items():
|
| 36 |
+
if not isinstance(spec, dict):
|
| 37 |
+
raise ValueError(f"question {qid!r} must be an object")
|
| 38 |
+
try:
|
| 39 |
+
Question.from_jev(qid, spec)
|
| 40 |
+
except ValueError as err:
|
| 41 |
+
raise ValueError(f"question {qid!r}: {err}") from err
|
| 42 |
+
n = len(qs)
|
| 43 |
+
if n > max_q:
|
| 44 |
+
raise ValueError(f"at most {max_q} questions per request fit this model's answer canvas, got {n}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _state_text(state) -> str:
|
| 48 |
+
return state if isinstance(state, str) else json.dumps(state, ensure_ascii=False)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def decide(model, request: dict) -> dict:
|
| 52 |
+
validate_request(request, max_questions(model))
|
| 53 |
+
resp = model.decide(_state_text(request["state"]), request["questions"])
|
| 54 |
+
resp["model"] = request.get("model", "hev")
|
| 55 |
+
return resp
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_app(model):
|
| 59 |
+
"""FastAPI app serving `model` at POST /v1/systemone and GET /health.
|
| 60 |
+
|
| 61 |
+
FastAPI runs plain `def` endpoints in a thread pool, and HevForDecisions.decide() is not
|
| 62 |
+
thread-safe (it shares one hook-capture buffer on the backbone), so calls to the model are
|
| 63 |
+
serialised with a lock: concurrent requests queue and are answered one at a time.
|
| 64 |
+
"""
|
| 65 |
+
from fastapi import FastAPI, HTTPException
|
| 66 |
+
|
| 67 |
+
app = FastAPI(title="Hev", version="0.1.0")
|
| 68 |
+
model_lock = threading.Lock()
|
| 69 |
+
|
| 70 |
+
@app.get("/health")
|
| 71 |
+
def health():
|
| 72 |
+
return {"ok": True}
|
| 73 |
+
|
| 74 |
+
@app.post("/v1/systemone")
|
| 75 |
+
def systemone(request: dict):
|
| 76 |
+
try:
|
| 77 |
+
with model_lock:
|
| 78 |
+
return decide(model, request)
|
| 79 |
+
except ValueError as err:
|
| 80 |
+
raise HTTPException(status_code=422, detail=str(err)) from err
|
| 81 |
+
|
| 82 |
+
return app
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def main():
|
| 86 |
+
ap = argparse.ArgumentParser()
|
| 87 |
+
ap.add_argument("--repo", required=True)
|
| 88 |
+
ap.add_argument("--port", type=int, default=8080)
|
| 89 |
+
ap.add_argument("--host", default="127.0.0.1")
|
| 90 |
+
ap.add_argument("--load-in-4bit", action="store_true")
|
| 91 |
+
ap.add_argument("--request", default=None, help="JSON file: answer once and exit")
|
| 92 |
+
args = ap.parse_args()
|
| 93 |
+
from .modeling_hev import HevForDecisions
|
| 94 |
+
model = HevForDecisions.from_pretrained(args.repo, load_in_4bit=args.load_in_4bit)
|
| 95 |
+
if args.request:
|
| 96 |
+
with open(args.request, "r", encoding="utf-8") as f:
|
| 97 |
+
print(json.dumps(decide(model, json.load(f)), indent=2))
|
| 98 |
+
return
|
| 99 |
+
import uvicorn
|
| 100 |
+
uvicorn.run(build_app(model), host=args.host, port=args.port)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
main()
|
config.json
CHANGED
|
@@ -1,312 +1,312 @@
|
|
| 1 |
-
{
|
| 2 |
-
"aliases": [
|
| 3 |
-
"A",
|
| 4 |
-
"B",
|
| 5 |
-
"C",
|
| 6 |
-
"D",
|
| 7 |
-
"E",
|
| 8 |
-
"F",
|
| 9 |
-
"G",
|
| 10 |
-
"H",
|
| 11 |
-
"I",
|
| 12 |
-
"J",
|
| 13 |
-
"K",
|
| 14 |
-
"L",
|
| 15 |
-
"M",
|
| 16 |
-
"N",
|
| 17 |
-
"O",
|
| 18 |
-
"P",
|
| 19 |
-
"Q",
|
| 20 |
-
"R",
|
| 21 |
-
"S",
|
| 22 |
-
"T",
|
| 23 |
-
"U",
|
| 24 |
-
"V",
|
| 25 |
-
"W",
|
| 26 |
-
"X",
|
| 27 |
-
"Y",
|
| 28 |
-
"Z",
|
| 29 |
-
"a",
|
| 30 |
-
"b",
|
| 31 |
-
"c",
|
| 32 |
-
"d",
|
| 33 |
-
"e",
|
| 34 |
-
"f",
|
| 35 |
-
"g",
|
| 36 |
-
"h",
|
| 37 |
-
"i",
|
| 38 |
-
"j",
|
| 39 |
-
"k",
|
| 40 |
-
"l",
|
| 41 |
-
"m",
|
| 42 |
-
"n",
|
| 43 |
-
"o",
|
| 44 |
-
"p",
|
| 45 |
-
"q",
|
| 46 |
-
"r",
|
| 47 |
-
"s",
|
| 48 |
-
"t",
|
| 49 |
-
"u",
|
| 50 |
-
"v",
|
| 51 |
-
"w",
|
| 52 |
-
"x",
|
| 53 |
-
"y",
|
| 54 |
-
"z",
|
| 55 |
-
"AA",
|
| 56 |
-
"AB",
|
| 57 |
-
"AC",
|
| 58 |
-
"AD",
|
| 59 |
-
"AE",
|
| 60 |
-
"AF",
|
| 61 |
-
"AG",
|
| 62 |
-
"AH",
|
| 63 |
-
"AI",
|
| 64 |
-
"AJ",
|
| 65 |
-
"AK",
|
| 66 |
-
"AL"
|
| 67 |
-
],
|
| 68 |
-
"auto_map": {
|
| 69 |
-
"AutoConfig": "configuration_hev.HevConfig",
|
| 70 |
-
"AutoModel": "modeling_hev.HevForDecisions"
|
| 71 |
-
},
|
| 72 |
-
"backbone_config": {
|
| 73 |
-
"_name_or_path": "google/diffusiongemma-26B-A4B-it",
|
| 74 |
-
"architectures": [
|
| 75 |
-
"DiffusionGemmaForBlockDiffusion"
|
| 76 |
-
],
|
| 77 |
-
"boi_token_id": 255999,
|
| 78 |
-
"canvas_length": 256,
|
| 79 |
-
"chunk_size_feed_forward": 0,
|
| 80 |
-
"dtype": "bfloat16",
|
| 81 |
-
"eoi_token_id": 258882,
|
| 82 |
-
"eos_token_id": [
|
| 83 |
-
1,
|
| 84 |
-
106
|
| 85 |
-
],
|
| 86 |
-
"id2label": {
|
| 87 |
-
"0": "LABEL_0",
|
| 88 |
-
"1": "LABEL_1"
|
| 89 |
-
},
|
| 90 |
-
"image_token_id": 258880,
|
| 91 |
-
"initializer_range": 0.02,
|
| 92 |
-
"is_encoder_decoder": false,
|
| 93 |
-
"label2id": {
|
| 94 |
-
"LABEL_0": 0,
|
| 95 |
-
"LABEL_1": 1
|
| 96 |
-
},
|
| 97 |
-
"model_type": "diffusion_gemma",
|
| 98 |
-
"output_attentions": false,
|
| 99 |
-
"output_hidden_states": false,
|
| 100 |
-
"problem_type": null,
|
| 101 |
-
"return_dict": true,
|
| 102 |
-
"text_config": {
|
| 103 |
-
"_name_or_path": "",
|
| 104 |
-
"architectures": null,
|
| 105 |
-
"attention_bias": false,
|
| 106 |
-
"attention_dropout": 0.0,
|
| 107 |
-
"bos_token_id": 2,
|
| 108 |
-
"chunk_size_feed_forward": 0,
|
| 109 |
-
"dtype": "bfloat16",
|
| 110 |
-
"eos_token_id": 1,
|
| 111 |
-
"final_logit_softcapping": 30.0,
|
| 112 |
-
"head_dim": 256,
|
| 113 |
-
"hidden_activation": "gelu_pytorch_tanh",
|
| 114 |
-
"hidden_size": 2816,
|
| 115 |
-
"id2label": {
|
| 116 |
-
"0": "LABEL_0",
|
| 117 |
-
"1": "LABEL_1"
|
| 118 |
-
},
|
| 119 |
-
"initializer_range": 0.02,
|
| 120 |
-
"intermediate_size": 2112,
|
| 121 |
-
"is_encoder_decoder": false,
|
| 122 |
-
"label2id": {
|
| 123 |
-
"LABEL_0": 0,
|
| 124 |
-
"LABEL_1": 1
|
| 125 |
-
},
|
| 126 |
-
"layer_types": [
|
| 127 |
-
"sliding_attention",
|
| 128 |
-
"sliding_attention",
|
| 129 |
-
"sliding_attention",
|
| 130 |
-
"sliding_attention",
|
| 131 |
-
"sliding_attention",
|
| 132 |
-
"full_attention",
|
| 133 |
-
"sliding_attention",
|
| 134 |
-
"sliding_attention",
|
| 135 |
-
"sliding_attention",
|
| 136 |
-
"sliding_attention",
|
| 137 |
-
"sliding_attention",
|
| 138 |
-
"full_attention",
|
| 139 |
-
"sliding_attention",
|
| 140 |
-
"sliding_attention",
|
| 141 |
-
"sliding_attention",
|
| 142 |
-
"sliding_attention",
|
| 143 |
-
"sliding_attention",
|
| 144 |
-
"full_attention",
|
| 145 |
-
"sliding_attention",
|
| 146 |
-
"sliding_attention",
|
| 147 |
-
"sliding_attention",
|
| 148 |
-
"sliding_attention",
|
| 149 |
-
"sliding_attention",
|
| 150 |
-
"full_attention",
|
| 151 |
-
"sliding_attention",
|
| 152 |
-
"sliding_attention",
|
| 153 |
-
"sliding_attention",
|
| 154 |
-
"sliding_attention",
|
| 155 |
-
"sliding_attention",
|
| 156 |
-
"full_attention"
|
| 157 |
-
],
|
| 158 |
-
"max_position_embeddings": 262144,
|
| 159 |
-
"model_type": "diffusion_gemma_text",
|
| 160 |
-
"moe_intermediate_size": 704,
|
| 161 |
-
"num_attention_heads": 16,
|
| 162 |
-
"num_experts": 128,
|
| 163 |
-
"num_hidden_layers": 30,
|
| 164 |
-
"num_key_value_heads": 8,
|
| 165 |
-
"output_attentions": false,
|
| 166 |
-
"output_hidden_states": false,
|
| 167 |
-
"pad_token_id": 0,
|
| 168 |
-
"per_layer_config": {
|
| 169 |
-
"05": {
|
| 170 |
-
"head_dim": 512,
|
| 171 |
-
"num_key_value_heads": 2
|
| 172 |
-
},
|
| 173 |
-
"11": {
|
| 174 |
-
"head_dim": 512,
|
| 175 |
-
"num_key_value_heads": 2
|
| 176 |
-
},
|
| 177 |
-
"17": {
|
| 178 |
-
"head_dim": 512,
|
| 179 |
-
"num_key_value_heads": 2
|
| 180 |
-
},
|
| 181 |
-
"23": {
|
| 182 |
-
"head_dim": 512,
|
| 183 |
-
"num_key_value_heads": 2
|
| 184 |
-
},
|
| 185 |
-
"29": {
|
| 186 |
-
"head_dim": 512,
|
| 187 |
-
"num_key_value_heads": 2
|
| 188 |
-
}
|
| 189 |
-
},
|
| 190 |
-
"problem_type": null,
|
| 191 |
-
"return_dict": true,
|
| 192 |
-
"rms_norm_eps": 1e-06,
|
| 193 |
-
"rope_parameters": {
|
| 194 |
-
"full_attention": {
|
| 195 |
-
"partial_rotary_factor": 0.25,
|
| 196 |
-
"rope_theta": 1000000.0,
|
| 197 |
-
"rope_type": "proportional"
|
| 198 |
-
},
|
| 199 |
-
"sliding_attention": {
|
| 200 |
-
"rope_theta": 10000.0,
|
| 201 |
-
"rope_type": "default"
|
| 202 |
-
}
|
| 203 |
-
},
|
| 204 |
-
"sliding_window": 1024,
|
| 205 |
-
"tie_word_embeddings": true,
|
| 206 |
-
"top_k_experts": 8,
|
| 207 |
-
"use_bidirectional_attention": "vision",
|
| 208 |
-
"vocab_size": 262144
|
| 209 |
-
},
|
| 210 |
-
"tie_word_embeddings": true,
|
| 211 |
-
"transformers_version": "5.17.0",
|
| 212 |
-
"vision_config": {
|
| 213 |
-
"_name_or_path": "",
|
| 214 |
-
"architectures": null,
|
| 215 |
-
"attention_bias": false,
|
| 216 |
-
"attention_dropout": 0.0,
|
| 217 |
-
"chunk_size_feed_forward": 0,
|
| 218 |
-
"default_output_length": 280,
|
| 219 |
-
"dtype": "bfloat16",
|
| 220 |
-
"global_head_dim": 72,
|
| 221 |
-
"head_dim": 72,
|
| 222 |
-
"hidden_activation": "gelu_pytorch_tanh",
|
| 223 |
-
"hidden_size": 1152,
|
| 224 |
-
"id2label": {
|
| 225 |
-
"0": "LABEL_0",
|
| 226 |
-
"1": "LABEL_1"
|
| 227 |
-
},
|
| 228 |
-
"initializer_range": 0.02,
|
| 229 |
-
"intermediate_size": 4304,
|
| 230 |
-
"is_encoder_decoder": false,
|
| 231 |
-
"label2id": {
|
| 232 |
-
"LABEL_0": 0,
|
| 233 |
-
"LABEL_1": 1
|
| 234 |
-
},
|
| 235 |
-
"max_position_embeddings": 131072,
|
| 236 |
-
"model_type": "gemma4_vision",
|
| 237 |
-
"num_attention_heads": 16,
|
| 238 |
-
"num_hidden_layers": 27,
|
| 239 |
-
"num_key_value_heads": 16,
|
| 240 |
-
"output_attentions": false,
|
| 241 |
-
"output_hidden_states": false,
|
| 242 |
-
"patch_size": 16,
|
| 243 |
-
"pooling_kernel_size": 3,
|
| 244 |
-
"position_embedding_size": 10240,
|
| 245 |
-
"problem_type": null,
|
| 246 |
-
"return_dict": true,
|
| 247 |
-
"rms_norm_eps": 1e-06,
|
| 248 |
-
"rope_parameters": {
|
| 249 |
-
"rope_theta": 100.0,
|
| 250 |
-
"rope_type": "axial"
|
| 251 |
-
},
|
| 252 |
-
"standardize": true,
|
| 253 |
-
"use_clipped_linears": false
|
| 254 |
-
},
|
| 255 |
-
"vision_soft_tokens_per_image": 280
|
| 256 |
-
},
|
| 257 |
-
"backbone_id": "google/diffusiongemma-26B-A4B-it",
|
| 258 |
-
"canvas_length": 64,
|
| 259 |
-
"decoder_norm": "model.decoder.norm",
|
| 260 |
-
"encoder_norm": "model.encoder.language_model.norm",
|
| 261 |
-
"extra_layers": {
|
| 262 |
-
"decoder": [
|
| 263 |
-
"model.decoder.layers.14",
|
| 264 |
-
"model.decoder.layers.22"
|
| 265 |
-
],
|
| 266 |
-
"encoder": [
|
| 267 |
-
"model.encoder.language_model.layers.14",
|
| 268 |
-
"model.encoder.language_model.layers.22"
|
| 269 |
-
]
|
| 270 |
-
},
|
| 271 |
-
"head_config": {
|
| 272 |
-
"chunk_size": 64,
|
| 273 |
-
"d_backbone": 8448,
|
| 274 |
-
"d_conv": 7,
|
| 275 |
-
"d_model": 512,
|
| 276 |
-
"d_state": 64,
|
| 277 |
-
"expand": 2,
|
| 278 |
-
"headdim": 64,
|
| 279 |
-
"mlp_mult": 2,
|
| 280 |
-
"n_blocks": 3,
|
| 281 |
-
"n_types": 3
|
| 282 |
-
},
|
| 283 |
-
"max_prompt_tokens": 1024,
|
| 284 |
-
"member_temperatures": [
|
| 285 |
-
[
|
| 286 |
-
1.338762879371643,
|
| 287 |
-
1.776159405708313,
|
| 288 |
-
1.816263198852539
|
| 289 |
-
],
|
| 290 |
-
[
|
| 291 |
-
1.4009417295455933,
|
| 292 |
-
1.899003267288208,
|
| 293 |
-
1.8320508003234863
|
| 294 |
-
],
|
| 295 |
-
[
|
| 296 |
-
1.3898975849151611,
|
| 297 |
-
1.61630380153656,
|
| 298 |
-
1.888205885887146
|
| 299 |
-
]
|
| 300 |
-
],
|
| 301 |
-
"model_type": "hev",
|
| 302 |
-
"n_heads": 3,
|
| 303 |
-
"prompt_format_version": 1,
|
| 304 |
-
"seed_mode": "pad",
|
| 305 |
-
"slot_token_id": 0,
|
| 306 |
-
"temperatures": [
|
| 307 |
-
1.338762879371643,
|
| 308 |
-
1.776159405708313,
|
| 309 |
-
1.816263198852539
|
| 310 |
-
],
|
| 311 |
-
"transformers_version": "5.17.0"
|
| 312 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"aliases": [
|
| 3 |
+
"A",
|
| 4 |
+
"B",
|
| 5 |
+
"C",
|
| 6 |
+
"D",
|
| 7 |
+
"E",
|
| 8 |
+
"F",
|
| 9 |
+
"G",
|
| 10 |
+
"H",
|
| 11 |
+
"I",
|
| 12 |
+
"J",
|
| 13 |
+
"K",
|
| 14 |
+
"L",
|
| 15 |
+
"M",
|
| 16 |
+
"N",
|
| 17 |
+
"O",
|
| 18 |
+
"P",
|
| 19 |
+
"Q",
|
| 20 |
+
"R",
|
| 21 |
+
"S",
|
| 22 |
+
"T",
|
| 23 |
+
"U",
|
| 24 |
+
"V",
|
| 25 |
+
"W",
|
| 26 |
+
"X",
|
| 27 |
+
"Y",
|
| 28 |
+
"Z",
|
| 29 |
+
"a",
|
| 30 |
+
"b",
|
| 31 |
+
"c",
|
| 32 |
+
"d",
|
| 33 |
+
"e",
|
| 34 |
+
"f",
|
| 35 |
+
"g",
|
| 36 |
+
"h",
|
| 37 |
+
"i",
|
| 38 |
+
"j",
|
| 39 |
+
"k",
|
| 40 |
+
"l",
|
| 41 |
+
"m",
|
| 42 |
+
"n",
|
| 43 |
+
"o",
|
| 44 |
+
"p",
|
| 45 |
+
"q",
|
| 46 |
+
"r",
|
| 47 |
+
"s",
|
| 48 |
+
"t",
|
| 49 |
+
"u",
|
| 50 |
+
"v",
|
| 51 |
+
"w",
|
| 52 |
+
"x",
|
| 53 |
+
"y",
|
| 54 |
+
"z",
|
| 55 |
+
"AA",
|
| 56 |
+
"AB",
|
| 57 |
+
"AC",
|
| 58 |
+
"AD",
|
| 59 |
+
"AE",
|
| 60 |
+
"AF",
|
| 61 |
+
"AG",
|
| 62 |
+
"AH",
|
| 63 |
+
"AI",
|
| 64 |
+
"AJ",
|
| 65 |
+
"AK",
|
| 66 |
+
"AL"
|
| 67 |
+
],
|
| 68 |
+
"auto_map": {
|
| 69 |
+
"AutoConfig": "configuration_hev.HevConfig",
|
| 70 |
+
"AutoModel": "modeling_hev.HevForDecisions"
|
| 71 |
+
},
|
| 72 |
+
"backbone_config": {
|
| 73 |
+
"_name_or_path": "google/diffusiongemma-26B-A4B-it",
|
| 74 |
+
"architectures": [
|
| 75 |
+
"DiffusionGemmaForBlockDiffusion"
|
| 76 |
+
],
|
| 77 |
+
"boi_token_id": 255999,
|
| 78 |
+
"canvas_length": 256,
|
| 79 |
+
"chunk_size_feed_forward": 0,
|
| 80 |
+
"dtype": "bfloat16",
|
| 81 |
+
"eoi_token_id": 258882,
|
| 82 |
+
"eos_token_id": [
|
| 83 |
+
1,
|
| 84 |
+
106
|
| 85 |
+
],
|
| 86 |
+
"id2label": {
|
| 87 |
+
"0": "LABEL_0",
|
| 88 |
+
"1": "LABEL_1"
|
| 89 |
+
},
|
| 90 |
+
"image_token_id": 258880,
|
| 91 |
+
"initializer_range": 0.02,
|
| 92 |
+
"is_encoder_decoder": false,
|
| 93 |
+
"label2id": {
|
| 94 |
+
"LABEL_0": 0,
|
| 95 |
+
"LABEL_1": 1
|
| 96 |
+
},
|
| 97 |
+
"model_type": "diffusion_gemma",
|
| 98 |
+
"output_attentions": false,
|
| 99 |
+
"output_hidden_states": false,
|
| 100 |
+
"problem_type": null,
|
| 101 |
+
"return_dict": true,
|
| 102 |
+
"text_config": {
|
| 103 |
+
"_name_or_path": "",
|
| 104 |
+
"architectures": null,
|
| 105 |
+
"attention_bias": false,
|
| 106 |
+
"attention_dropout": 0.0,
|
| 107 |
+
"bos_token_id": 2,
|
| 108 |
+
"chunk_size_feed_forward": 0,
|
| 109 |
+
"dtype": "bfloat16",
|
| 110 |
+
"eos_token_id": 1,
|
| 111 |
+
"final_logit_softcapping": 30.0,
|
| 112 |
+
"head_dim": 256,
|
| 113 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 114 |
+
"hidden_size": 2816,
|
| 115 |
+
"id2label": {
|
| 116 |
+
"0": "LABEL_0",
|
| 117 |
+
"1": "LABEL_1"
|
| 118 |
+
},
|
| 119 |
+
"initializer_range": 0.02,
|
| 120 |
+
"intermediate_size": 2112,
|
| 121 |
+
"is_encoder_decoder": false,
|
| 122 |
+
"label2id": {
|
| 123 |
+
"LABEL_0": 0,
|
| 124 |
+
"LABEL_1": 1
|
| 125 |
+
},
|
| 126 |
+
"layer_types": [
|
| 127 |
+
"sliding_attention",
|
| 128 |
+
"sliding_attention",
|
| 129 |
+
"sliding_attention",
|
| 130 |
+
"sliding_attention",
|
| 131 |
+
"sliding_attention",
|
| 132 |
+
"full_attention",
|
| 133 |
+
"sliding_attention",
|
| 134 |
+
"sliding_attention",
|
| 135 |
+
"sliding_attention",
|
| 136 |
+
"sliding_attention",
|
| 137 |
+
"sliding_attention",
|
| 138 |
+
"full_attention",
|
| 139 |
+
"sliding_attention",
|
| 140 |
+
"sliding_attention",
|
| 141 |
+
"sliding_attention",
|
| 142 |
+
"sliding_attention",
|
| 143 |
+
"sliding_attention",
|
| 144 |
+
"full_attention",
|
| 145 |
+
"sliding_attention",
|
| 146 |
+
"sliding_attention",
|
| 147 |
+
"sliding_attention",
|
| 148 |
+
"sliding_attention",
|
| 149 |
+
"sliding_attention",
|
| 150 |
+
"full_attention",
|
| 151 |
+
"sliding_attention",
|
| 152 |
+
"sliding_attention",
|
| 153 |
+
"sliding_attention",
|
| 154 |
+
"sliding_attention",
|
| 155 |
+
"sliding_attention",
|
| 156 |
+
"full_attention"
|
| 157 |
+
],
|
| 158 |
+
"max_position_embeddings": 262144,
|
| 159 |
+
"model_type": "diffusion_gemma_text",
|
| 160 |
+
"moe_intermediate_size": 704,
|
| 161 |
+
"num_attention_heads": 16,
|
| 162 |
+
"num_experts": 128,
|
| 163 |
+
"num_hidden_layers": 30,
|
| 164 |
+
"num_key_value_heads": 8,
|
| 165 |
+
"output_attentions": false,
|
| 166 |
+
"output_hidden_states": false,
|
| 167 |
+
"pad_token_id": 0,
|
| 168 |
+
"per_layer_config": {
|
| 169 |
+
"05": {
|
| 170 |
+
"head_dim": 512,
|
| 171 |
+
"num_key_value_heads": 2
|
| 172 |
+
},
|
| 173 |
+
"11": {
|
| 174 |
+
"head_dim": 512,
|
| 175 |
+
"num_key_value_heads": 2
|
| 176 |
+
},
|
| 177 |
+
"17": {
|
| 178 |
+
"head_dim": 512,
|
| 179 |
+
"num_key_value_heads": 2
|
| 180 |
+
},
|
| 181 |
+
"23": {
|
| 182 |
+
"head_dim": 512,
|
| 183 |
+
"num_key_value_heads": 2
|
| 184 |
+
},
|
| 185 |
+
"29": {
|
| 186 |
+
"head_dim": 512,
|
| 187 |
+
"num_key_value_heads": 2
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
"problem_type": null,
|
| 191 |
+
"return_dict": true,
|
| 192 |
+
"rms_norm_eps": 1e-06,
|
| 193 |
+
"rope_parameters": {
|
| 194 |
+
"full_attention": {
|
| 195 |
+
"partial_rotary_factor": 0.25,
|
| 196 |
+
"rope_theta": 1000000.0,
|
| 197 |
+
"rope_type": "proportional"
|
| 198 |
+
},
|
| 199 |
+
"sliding_attention": {
|
| 200 |
+
"rope_theta": 10000.0,
|
| 201 |
+
"rope_type": "default"
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
"sliding_window": 1024,
|
| 205 |
+
"tie_word_embeddings": true,
|
| 206 |
+
"top_k_experts": 8,
|
| 207 |
+
"use_bidirectional_attention": "vision",
|
| 208 |
+
"vocab_size": 262144
|
| 209 |
+
},
|
| 210 |
+
"tie_word_embeddings": true,
|
| 211 |
+
"transformers_version": "5.17.0",
|
| 212 |
+
"vision_config": {
|
| 213 |
+
"_name_or_path": "",
|
| 214 |
+
"architectures": null,
|
| 215 |
+
"attention_bias": false,
|
| 216 |
+
"attention_dropout": 0.0,
|
| 217 |
+
"chunk_size_feed_forward": 0,
|
| 218 |
+
"default_output_length": 280,
|
| 219 |
+
"dtype": "bfloat16",
|
| 220 |
+
"global_head_dim": 72,
|
| 221 |
+
"head_dim": 72,
|
| 222 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 223 |
+
"hidden_size": 1152,
|
| 224 |
+
"id2label": {
|
| 225 |
+
"0": "LABEL_0",
|
| 226 |
+
"1": "LABEL_1"
|
| 227 |
+
},
|
| 228 |
+
"initializer_range": 0.02,
|
| 229 |
+
"intermediate_size": 4304,
|
| 230 |
+
"is_encoder_decoder": false,
|
| 231 |
+
"label2id": {
|
| 232 |
+
"LABEL_0": 0,
|
| 233 |
+
"LABEL_1": 1
|
| 234 |
+
},
|
| 235 |
+
"max_position_embeddings": 131072,
|
| 236 |
+
"model_type": "gemma4_vision",
|
| 237 |
+
"num_attention_heads": 16,
|
| 238 |
+
"num_hidden_layers": 27,
|
| 239 |
+
"num_key_value_heads": 16,
|
| 240 |
+
"output_attentions": false,
|
| 241 |
+
"output_hidden_states": false,
|
| 242 |
+
"patch_size": 16,
|
| 243 |
+
"pooling_kernel_size": 3,
|
| 244 |
+
"position_embedding_size": 10240,
|
| 245 |
+
"problem_type": null,
|
| 246 |
+
"return_dict": true,
|
| 247 |
+
"rms_norm_eps": 1e-06,
|
| 248 |
+
"rope_parameters": {
|
| 249 |
+
"rope_theta": 100.0,
|
| 250 |
+
"rope_type": "axial"
|
| 251 |
+
},
|
| 252 |
+
"standardize": true,
|
| 253 |
+
"use_clipped_linears": false
|
| 254 |
+
},
|
| 255 |
+
"vision_soft_tokens_per_image": 280
|
| 256 |
+
},
|
| 257 |
+
"backbone_id": "google/diffusiongemma-26B-A4B-it",
|
| 258 |
+
"canvas_length": 64,
|
| 259 |
+
"decoder_norm": "model.decoder.norm",
|
| 260 |
+
"encoder_norm": "model.encoder.language_model.norm",
|
| 261 |
+
"extra_layers": {
|
| 262 |
+
"decoder": [
|
| 263 |
+
"model.decoder.layers.14",
|
| 264 |
+
"model.decoder.layers.22"
|
| 265 |
+
],
|
| 266 |
+
"encoder": [
|
| 267 |
+
"model.encoder.language_model.layers.14",
|
| 268 |
+
"model.encoder.language_model.layers.22"
|
| 269 |
+
]
|
| 270 |
+
},
|
| 271 |
+
"head_config": {
|
| 272 |
+
"chunk_size": 64,
|
| 273 |
+
"d_backbone": 8448,
|
| 274 |
+
"d_conv": 7,
|
| 275 |
+
"d_model": 512,
|
| 276 |
+
"d_state": 64,
|
| 277 |
+
"expand": 2,
|
| 278 |
+
"headdim": 64,
|
| 279 |
+
"mlp_mult": 2,
|
| 280 |
+
"n_blocks": 3,
|
| 281 |
+
"n_types": 3
|
| 282 |
+
},
|
| 283 |
+
"max_prompt_tokens": 1024,
|
| 284 |
+
"member_temperatures": [
|
| 285 |
+
[
|
| 286 |
+
1.338762879371643,
|
| 287 |
+
1.776159405708313,
|
| 288 |
+
1.816263198852539
|
| 289 |
+
],
|
| 290 |
+
[
|
| 291 |
+
1.4009417295455933,
|
| 292 |
+
1.899003267288208,
|
| 293 |
+
1.8320508003234863
|
| 294 |
+
],
|
| 295 |
+
[
|
| 296 |
+
1.3898975849151611,
|
| 297 |
+
1.61630380153656,
|
| 298 |
+
1.888205885887146
|
| 299 |
+
]
|
| 300 |
+
],
|
| 301 |
+
"model_type": "hev",
|
| 302 |
+
"n_heads": 3,
|
| 303 |
+
"prompt_format_version": 1,
|
| 304 |
+
"seed_mode": "pad",
|
| 305 |
+
"slot_token_id": 0,
|
| 306 |
+
"temperatures": [
|
| 307 |
+
1.338762879371643,
|
| 308 |
+
1.776159405708313,
|
| 309 |
+
1.816263198852539
|
| 310 |
+
],
|
| 311 |
+
"transformers_version": "5.17.0"
|
| 312 |
+
}
|
configuration_hev.py
CHANGED
|
@@ -1,47 +1,47 @@
|
|
| 1 |
-
"""HevConfig: everything needed to rebuild the frozen backbone, the head, and the prompt protocol."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
try:
|
| 5 |
-
from transformers import PreTrainedConfig
|
| 6 |
-
except ImportError: # transformers < 5
|
| 7 |
-
from transformers import PretrainedConfig as PreTrainedConfig
|
| 8 |
-
|
| 9 |
-
from .head import HevHeadConfig
|
| 10 |
-
from .schema import ALIASES
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class HevConfig(PreTrainedConfig):
|
| 14 |
-
model_type = "hev"
|
| 15 |
-
|
| 16 |
-
def __init__(self, backbone_id: str = "google/diffusiongemma-26B-A4B-it", backbone_config: dict | None = None,
|
| 17 |
-
head_config: dict | None = None, canvas_length: int = 64, seed_mode: str = "pad",
|
| 18 |
-
slot_token_id: int = 0, encoder_norm: str = "", decoder_norm: str = "",
|
| 19 |
-
max_prompt_tokens: int = 1024, temperatures: list[float] | None = None,
|
| 20 |
-
n_heads: int = 1, member_temperatures: list[list[float]] | None = None,
|
| 21 |
-
aliases: list[str] | None = None, prompt_format_version: int = 1,
|
| 22 |
-
extra_layers: dict | None = None, **kwargs):
|
| 23 |
-
self.backbone_id = backbone_id
|
| 24 |
-
# Module names of the intermediate encoder/decoder layers whose unit-RMS outputs the features append to
|
| 25 |
-
# the final-norm block (hev.features, --layers). Empty for a head trained on the final norms alone.
|
| 26 |
-
extra_layers = extra_layers or {}
|
| 27 |
-
self.extra_layers = {"encoder": list(extra_layers.get("encoder", [])),
|
| 28 |
-
"decoder": list(extra_layers.get("decoder", []))}
|
| 29 |
-
self.backbone_config = backbone_config or {}
|
| 30 |
-
self.head_config = head_config or HevHeadConfig().to_dict()
|
| 31 |
-
self.canvas_length = canvas_length
|
| 32 |
-
self.seed_mode = seed_mode
|
| 33 |
-
self.slot_token_id = slot_token_id
|
| 34 |
-
self.encoder_norm = encoder_norm
|
| 35 |
-
self.decoder_norm = decoder_norm
|
| 36 |
-
self.max_prompt_tokens = max_prompt_tokens
|
| 37 |
-
self.temperatures = temperatures or [1.0, 1.0, 1.0]
|
| 38 |
-
self.n_heads = int(n_heads)
|
| 39 |
-
# One temperature triple per member; a single-head repo written before this field existed has none, so
|
| 40 |
-
# its `temperatures` triple serves member 0.
|
| 41 |
-
self.member_temperatures = member_temperatures or [list(self.temperatures)]
|
| 42 |
-
self.aliases = aliases or list(ALIASES)
|
| 43 |
-
# The hev.prompting format the head was trained with. Configs written before this field existed used 1.
|
| 44 |
-
self.prompt_format_version = prompt_format_version
|
| 45 |
-
kwargs.setdefault("auto_map", {"AutoConfig": "configuration_hev.HevConfig",
|
| 46 |
-
"AutoModel": "modeling_hev.HevForDecisions"})
|
| 47 |
-
super().__init__(**kwargs)
|
|
|
|
| 1 |
+
"""HevConfig: everything needed to rebuild the frozen backbone, the head, and the prompt protocol."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
try:
|
| 5 |
+
from transformers import PreTrainedConfig
|
| 6 |
+
except ImportError: # transformers < 5
|
| 7 |
+
from transformers import PretrainedConfig as PreTrainedConfig
|
| 8 |
+
|
| 9 |
+
from .head import HevHeadConfig
|
| 10 |
+
from .schema import ALIASES
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class HevConfig(PreTrainedConfig):
|
| 14 |
+
model_type = "hev"
|
| 15 |
+
|
| 16 |
+
def __init__(self, backbone_id: str = "google/diffusiongemma-26B-A4B-it", backbone_config: dict | None = None,
|
| 17 |
+
head_config: dict | None = None, canvas_length: int = 64, seed_mode: str = "pad",
|
| 18 |
+
slot_token_id: int = 0, encoder_norm: str = "", decoder_norm: str = "",
|
| 19 |
+
max_prompt_tokens: int = 1024, temperatures: list[float] | None = None,
|
| 20 |
+
n_heads: int = 1, member_temperatures: list[list[float]] | None = None,
|
| 21 |
+
aliases: list[str] | None = None, prompt_format_version: int = 1,
|
| 22 |
+
extra_layers: dict | None = None, **kwargs):
|
| 23 |
+
self.backbone_id = backbone_id
|
| 24 |
+
# Module names of the intermediate encoder/decoder layers whose unit-RMS outputs the features append to
|
| 25 |
+
# the final-norm block (hev.features, --layers). Empty for a head trained on the final norms alone.
|
| 26 |
+
extra_layers = extra_layers or {}
|
| 27 |
+
self.extra_layers = {"encoder": list(extra_layers.get("encoder", [])),
|
| 28 |
+
"decoder": list(extra_layers.get("decoder", []))}
|
| 29 |
+
self.backbone_config = backbone_config or {}
|
| 30 |
+
self.head_config = head_config or HevHeadConfig().to_dict()
|
| 31 |
+
self.canvas_length = canvas_length
|
| 32 |
+
self.seed_mode = seed_mode
|
| 33 |
+
self.slot_token_id = slot_token_id
|
| 34 |
+
self.encoder_norm = encoder_norm
|
| 35 |
+
self.decoder_norm = decoder_norm
|
| 36 |
+
self.max_prompt_tokens = max_prompt_tokens
|
| 37 |
+
self.temperatures = temperatures or [1.0, 1.0, 1.0]
|
| 38 |
+
self.n_heads = int(n_heads)
|
| 39 |
+
# One temperature triple per member; a single-head repo written before this field existed has none, so
|
| 40 |
+
# its `temperatures` triple serves member 0.
|
| 41 |
+
self.member_temperatures = member_temperatures or [list(self.temperatures)]
|
| 42 |
+
self.aliases = aliases or list(ALIASES)
|
| 43 |
+
# The hev.prompting format the head was trained with. Configs written before this field existed used 1.
|
| 44 |
+
self.prompt_format_version = prompt_format_version
|
| 45 |
+
kwargs.setdefault("auto_map", {"AutoConfig": "configuration_hev.HevConfig",
|
| 46 |
+
"AutoModel": "modeling_hev.HevForDecisions"})
|
| 47 |
+
super().__init__(**kwargs)
|
data.py
CHANGED
|
@@ -1,216 +1,216 @@
|
|
| 1 |
-
"""Loaders for LocalLLaMA/typed-decisions and Praveenrajus/jev-bench, plus deterministic manifests."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import hashlib
|
| 5 |
-
import json
|
| 6 |
-
import math
|
| 7 |
-
import random
|
| 8 |
-
import re
|
| 9 |
-
from collections import defaultdict
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
from .schema import Example, Question, align_distribution
|
| 13 |
-
|
| 14 |
-
TYPED_DECISIONS = "LocalLLaMA/typed-decisions"
|
| 15 |
-
JEV_BENCH = "Praveenrajus/jev-bench"
|
| 16 |
-
JEV_BENCH_CONFIGS = [
|
| 17 |
-
"arc_challenge", "boolq", "chaosnli", "civil_comments", "fever_evidence", "go_emotions",
|
| 18 |
-
"helpsteer2_helpfulness", "helpsteer2_verbosity", "massive", "measuring_hate_speech", "mmlu",
|
| 19 |
-
"mnli", "paws", "sms_spam", "sst5", "strategyqa_closed", "strategyqa_grounded", "stsb", "yelp5",
|
| 20 |
-
]
|
| 21 |
-
TRUE_LABELS = {"1", "true", "yes"}
|
| 22 |
-
FALSE_LABELS = {"0", "false", "no"}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def _questions_from_map(qmap: dict) -> list[Question]:
|
| 26 |
-
return [Question.from_jev(qid, spec) for qid, spec in qmap.items()]
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _label_option(label, q: Question) -> str | None:
|
| 30 |
-
"""The row's discrete gold label as one of q's option names, or None when it is missing or not an option.
|
| 31 |
-
|
| 32 |
-
A boolean names the noul option "true" or "false", and a number its string form (a score level such as "2").
|
| 33 |
-
"""
|
| 34 |
-
if isinstance(label, bool):
|
| 35 |
-
label = "true" if label else "false"
|
| 36 |
-
if label is None or str(label) not in q.options:
|
| 37 |
-
return None
|
| 38 |
-
return str(label)
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def typed_decisions_example(row: dict) -> Example:
|
| 42 |
-
questions = _questions_from_map(json.loads(row["questions"]))
|
| 43 |
-
gold_raw = json.loads(row["gold"])
|
| 44 |
-
gold, labels = {}, {}
|
| 45 |
-
for q in questions:
|
| 46 |
-
entry = gold_raw[q.qid]
|
| 47 |
-
gold[q.qid] = align_distribution(entry["probabilities"], q.options)
|
| 48 |
-
# The card's discrete label breaks ties in the distribution; hev.evaluate scores accuracy against it.
|
| 49 |
-
label = _label_option(entry.get("label"), q)
|
| 50 |
-
if label is not None:
|
| 51 |
-
labels[q.qid] = label
|
| 52 |
-
meta = {"workflow": row["workflow"]}
|
| 53 |
-
if labels:
|
| 54 |
-
meta["labels"] = labels
|
| 55 |
-
return Example(id=row["id"], source=f"typed-decisions/{row['workflow']}",
|
| 56 |
-
state=row["state"], questions=questions, gold=gold, meta=meta)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def _positional_distribution(soft, k: int) -> list[float] | None:
|
| 60 |
-
if len(soft) != k:
|
| 61 |
-
return None
|
| 62 |
-
if not all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in soft):
|
| 63 |
-
return None
|
| 64 |
-
vals = [float(v) for v in soft]
|
| 65 |
-
if any(v < 0 or not math.isfinite(v) for v in vals):
|
| 66 |
-
return None
|
| 67 |
-
total = sum(vals)
|
| 68 |
-
if total <= 0:
|
| 69 |
-
return None
|
| 70 |
-
return [v / total for v in vals]
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def jev_bench_example(row: dict, config: str) -> Example:
|
| 74 |
-
q = Question.from_jev("q", json.loads(row["question"]))
|
| 75 |
-
soft = row.get("soft_label")
|
| 76 |
-
if isinstance(soft, str):
|
| 77 |
-
soft = json.loads(soft)
|
| 78 |
-
dist = None
|
| 79 |
-
if isinstance(soft, dict) and set(soft) & set(q.options):
|
| 80 |
-
dist = align_distribution(soft, q.options)
|
| 81 |
-
elif (isinstance(soft, (int, float)) and not isinstance(soft, bool) and math.isfinite(soft)
|
| 82 |
-
and q.type == "noul"):
|
| 83 |
-
p = min(1.0, max(0.0, float(soft)))
|
| 84 |
-
dist = [p if o == "true" else 1.0 - p for o in q.options]
|
| 85 |
-
elif isinstance(soft, (list, tuple)) and q.type == "score":
|
| 86 |
-
dist = _positional_distribution(soft, q.k)
|
| 87 |
-
if dist is None:
|
| 88 |
-
label = str(row["label"])
|
| 89 |
-
if q.type == "noul":
|
| 90 |
-
norm = label.strip().lower()
|
| 91 |
-
if norm in TRUE_LABELS:
|
| 92 |
-
label = "true"
|
| 93 |
-
elif norm in FALSE_LABELS:
|
| 94 |
-
label = "false"
|
| 95 |
-
else:
|
| 96 |
-
raise ValueError(f"{row['id']}: unrecognised noul label {row['label']!r}")
|
| 97 |
-
if label not in q.options:
|
| 98 |
-
raise ValueError(f"{row['id']}: label {label!r} not in options {q.options}")
|
| 99 |
-
dist = [1.0 if o == label else 0.0 for o in q.options]
|
| 100 |
-
meta = row.get("meta") or "{}"
|
| 101 |
-
meta = json.loads(meta) if isinstance(meta, str) else dict(meta)
|
| 102 |
-
meta["config"] = config
|
| 103 |
-
return Example(id=row["id"], source=f"jev-bench/{config}", state=row["state"],
|
| 104 |
-
questions=[q], gold={"q": dist}, meta=meta)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def split_validation(examples: list[Example], frac: float = 0.1, seed: int = 42):
|
| 108 |
-
by_source = defaultdict(list)
|
| 109 |
-
for ex in examples:
|
| 110 |
-
by_source[ex.source].append(ex)
|
| 111 |
-
train, val = [], []
|
| 112 |
-
for source in sorted(by_source):
|
| 113 |
-
group = sorted(by_source[source], key=lambda e: e.id)
|
| 114 |
-
rng = random.Random(f"{seed}:{source}")
|
| 115 |
-
rng.shuffle(group)
|
| 116 |
-
n_val = max(1, round(len(group) * frac))
|
| 117 |
-
val.extend(group[:n_val])
|
| 118 |
-
train.extend(group[n_val:])
|
| 119 |
-
return train, val
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def write_manifest(examples: list[Example], path) -> None:
|
| 123 |
-
path = Path(path)
|
| 124 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 125 |
-
with path.open("w", encoding="utf-8") as f:
|
| 126 |
-
for ex in examples:
|
| 127 |
-
f.write(json.dumps(ex.to_dict(), ensure_ascii=False) + "\n")
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def read_manifest(path) -> list[Example]:
|
| 131 |
-
with Path(path).open("r", encoding="utf-8") as f:
|
| 132 |
-
return [Example.from_dict(json.loads(line)) for line in f if line.strip()]
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def manifest_checksum(path) -> str:
|
| 136 |
-
return hashlib.sha256(Path(path).read_bytes()).hexdigest()[:16]
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def _is_missing_split(err: Exception, split: str) -> bool:
|
| 140 |
-
"""True for the error `datasets` raises when a config has no such split (the only load error that is skipped)."""
|
| 141 |
-
msg = str(err)
|
| 142 |
-
return ("Unknown split" in msg or "Bad split" in msg) and split in msg
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def _dataset_revision(ds) -> str | None:
|
| 146 |
-
"""The Hub commit a loaded split came from, read off its cache path (.../<config>/<version>/<sha>/...)."""
|
| 147 |
-
for f in getattr(ds, "cache_files", None) or []:
|
| 148 |
-
name = Path(str(f.get("filename", ""))).parent.name
|
| 149 |
-
if re.fullmatch(r"[0-9a-f]{40}", name):
|
| 150 |
-
return name
|
| 151 |
-
return None
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def _sample(rows, k: int, seed: int, key: str):
|
| 155 |
-
idx = list(range(len(rows)))
|
| 156 |
-
random.Random(f"{seed}:{key}").shuffle(idx)
|
| 157 |
-
return [rows[i] for i in sorted(idx[:k])]
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def build_manifests(out_dir, seed: int = 42, per_config: dict | None = None) -> dict:
|
| 161 |
-
# Imported here, inside try/except, so neither the tests nor Hub users of the flattened data.py need `datasets`:
|
| 162 |
-
# transformers' check_imports skips imports inside try blocks.
|
| 163 |
-
try:
|
| 164 |
-
from datasets import load_dataset
|
| 165 |
-
except ImportError as err:
|
| 166 |
-
raise ImportError('building the manifests needs the `datasets` package: pip install datasets '
|
| 167 |
-
'(or pip install -e ".[data]")') from err
|
| 168 |
-
|
| 169 |
-
per_config = per_config or {"train": 300, "validation": 100, "test": 200}
|
| 170 |
-
out = Path(out_dir)
|
| 171 |
-
revisions = defaultdict(set)
|
| 172 |
-
|
| 173 |
-
def load(repo, config, split):
|
| 174 |
-
ds = load_dataset(repo, config, split=split)
|
| 175 |
-
sha = _dataset_revision(ds)
|
| 176 |
-
if sha:
|
| 177 |
-
revisions[repo].add(sha)
|
| 178 |
-
return ds
|
| 179 |
-
|
| 180 |
-
td_train = [typed_decisions_example(r) for r in load(TYPED_DECISIONS, "all", "train")]
|
| 181 |
-
td_test = [typed_decisions_example(r) for r in load(TYPED_DECISIONS, "all", "test")]
|
| 182 |
-
train, val = split_validation(td_train, 0.1, seed)
|
| 183 |
-
jb_test = []
|
| 184 |
-
for config in JEV_BENCH_CONFIGS:
|
| 185 |
-
for split, target in (("train", train), ("validation", val), ("test", jb_test)):
|
| 186 |
-
try:
|
| 187 |
-
rows = load(JEV_BENCH, config, split)
|
| 188 |
-
except ValueError as err:
|
| 189 |
-
if not _is_missing_split(err, split):
|
| 190 |
-
raise
|
| 191 |
-
print(f"skip {config}/{split}: {err}", flush=True)
|
| 192 |
-
continue
|
| 193 |
-
rows = [rows[i] for i in range(len(rows))]
|
| 194 |
-
for r in _sample(rows, per_config[split], seed, f"{config}/{split}"):
|
| 195 |
-
target.append(jev_bench_example(r, config))
|
| 196 |
-
write_manifest(train, out / "train.jsonl")
|
| 197 |
-
write_manifest(val, out / "validation.jsonl")
|
| 198 |
-
write_manifest(td_test, out / "test_td.jsonl")
|
| 199 |
-
write_manifest(jb_test, out / "test_jb.jsonl")
|
| 200 |
-
counts = {"train": len(train), "validation": len(val), "test_td": len(td_test), "test_jb": len(jb_test)}
|
| 201 |
-
for name in ("train", "validation", "test_td", "test_jb"):
|
| 202 |
-
counts[f"{name}_sha"] = manifest_checksum(out / f"{name}.jsonl")
|
| 203 |
-
if revisions: # the dataset commit each repo was loaded from (a sorted list if the cache mixed several)
|
| 204 |
-
counts["revisions"] = {repo: next(iter(shas)) if len(shas) == 1 else sorted(shas)
|
| 205 |
-
for repo, shas in sorted(revisions.items())}
|
| 206 |
-
(out / "counts.json").write_text(json.dumps(counts, indent=2))
|
| 207 |
-
return counts
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
if __name__ == "__main__":
|
| 211 |
-
import argparse
|
| 212 |
-
ap = argparse.ArgumentParser()
|
| 213 |
-
ap.add_argument("--out", default="data")
|
| 214 |
-
ap.add_argument("--seed", type=int, default=42)
|
| 215 |
-
args = ap.parse_args()
|
| 216 |
-
print(json.dumps(build_manifests(args.out, args.seed), indent=2))
|
|
|
|
| 1 |
+
"""Loaders for LocalLLaMA/typed-decisions and Praveenrajus/jev-bench, plus deterministic manifests."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import random
|
| 8 |
+
import re
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from .schema import Example, Question, align_distribution
|
| 13 |
+
|
| 14 |
+
TYPED_DECISIONS = "LocalLLaMA/typed-decisions"
|
| 15 |
+
JEV_BENCH = "Praveenrajus/jev-bench"
|
| 16 |
+
JEV_BENCH_CONFIGS = [
|
| 17 |
+
"arc_challenge", "boolq", "chaosnli", "civil_comments", "fever_evidence", "go_emotions",
|
| 18 |
+
"helpsteer2_helpfulness", "helpsteer2_verbosity", "massive", "measuring_hate_speech", "mmlu",
|
| 19 |
+
"mnli", "paws", "sms_spam", "sst5", "strategyqa_closed", "strategyqa_grounded", "stsb", "yelp5",
|
| 20 |
+
]
|
| 21 |
+
TRUE_LABELS = {"1", "true", "yes"}
|
| 22 |
+
FALSE_LABELS = {"0", "false", "no"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _questions_from_map(qmap: dict) -> list[Question]:
|
| 26 |
+
return [Question.from_jev(qid, spec) for qid, spec in qmap.items()]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _label_option(label, q: Question) -> str | None:
|
| 30 |
+
"""The row's discrete gold label as one of q's option names, or None when it is missing or not an option.
|
| 31 |
+
|
| 32 |
+
A boolean names the noul option "true" or "false", and a number its string form (a score level such as "2").
|
| 33 |
+
"""
|
| 34 |
+
if isinstance(label, bool):
|
| 35 |
+
label = "true" if label else "false"
|
| 36 |
+
if label is None or str(label) not in q.options:
|
| 37 |
+
return None
|
| 38 |
+
return str(label)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def typed_decisions_example(row: dict) -> Example:
|
| 42 |
+
questions = _questions_from_map(json.loads(row["questions"]))
|
| 43 |
+
gold_raw = json.loads(row["gold"])
|
| 44 |
+
gold, labels = {}, {}
|
| 45 |
+
for q in questions:
|
| 46 |
+
entry = gold_raw[q.qid]
|
| 47 |
+
gold[q.qid] = align_distribution(entry["probabilities"], q.options)
|
| 48 |
+
# The card's discrete label breaks ties in the distribution; hev.evaluate scores accuracy against it.
|
| 49 |
+
label = _label_option(entry.get("label"), q)
|
| 50 |
+
if label is not None:
|
| 51 |
+
labels[q.qid] = label
|
| 52 |
+
meta = {"workflow": row["workflow"]}
|
| 53 |
+
if labels:
|
| 54 |
+
meta["labels"] = labels
|
| 55 |
+
return Example(id=row["id"], source=f"typed-decisions/{row['workflow']}",
|
| 56 |
+
state=row["state"], questions=questions, gold=gold, meta=meta)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _positional_distribution(soft, k: int) -> list[float] | None:
|
| 60 |
+
if len(soft) != k:
|
| 61 |
+
return None
|
| 62 |
+
if not all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in soft):
|
| 63 |
+
return None
|
| 64 |
+
vals = [float(v) for v in soft]
|
| 65 |
+
if any(v < 0 or not math.isfinite(v) for v in vals):
|
| 66 |
+
return None
|
| 67 |
+
total = sum(vals)
|
| 68 |
+
if total <= 0:
|
| 69 |
+
return None
|
| 70 |
+
return [v / total for v in vals]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def jev_bench_example(row: dict, config: str) -> Example:
|
| 74 |
+
q = Question.from_jev("q", json.loads(row["question"]))
|
| 75 |
+
soft = row.get("soft_label")
|
| 76 |
+
if isinstance(soft, str):
|
| 77 |
+
soft = json.loads(soft)
|
| 78 |
+
dist = None
|
| 79 |
+
if isinstance(soft, dict) and set(soft) & set(q.options):
|
| 80 |
+
dist = align_distribution(soft, q.options)
|
| 81 |
+
elif (isinstance(soft, (int, float)) and not isinstance(soft, bool) and math.isfinite(soft)
|
| 82 |
+
and q.type == "noul"):
|
| 83 |
+
p = min(1.0, max(0.0, float(soft)))
|
| 84 |
+
dist = [p if o == "true" else 1.0 - p for o in q.options]
|
| 85 |
+
elif isinstance(soft, (list, tuple)) and q.type == "score":
|
| 86 |
+
dist = _positional_distribution(soft, q.k)
|
| 87 |
+
if dist is None:
|
| 88 |
+
label = str(row["label"])
|
| 89 |
+
if q.type == "noul":
|
| 90 |
+
norm = label.strip().lower()
|
| 91 |
+
if norm in TRUE_LABELS:
|
| 92 |
+
label = "true"
|
| 93 |
+
elif norm in FALSE_LABELS:
|
| 94 |
+
label = "false"
|
| 95 |
+
else:
|
| 96 |
+
raise ValueError(f"{row['id']}: unrecognised noul label {row['label']!r}")
|
| 97 |
+
if label not in q.options:
|
| 98 |
+
raise ValueError(f"{row['id']}: label {label!r} not in options {q.options}")
|
| 99 |
+
dist = [1.0 if o == label else 0.0 for o in q.options]
|
| 100 |
+
meta = row.get("meta") or "{}"
|
| 101 |
+
meta = json.loads(meta) if isinstance(meta, str) else dict(meta)
|
| 102 |
+
meta["config"] = config
|
| 103 |
+
return Example(id=row["id"], source=f"jev-bench/{config}", state=row["state"],
|
| 104 |
+
questions=[q], gold={"q": dist}, meta=meta)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def split_validation(examples: list[Example], frac: float = 0.1, seed: int = 42):
|
| 108 |
+
by_source = defaultdict(list)
|
| 109 |
+
for ex in examples:
|
| 110 |
+
by_source[ex.source].append(ex)
|
| 111 |
+
train, val = [], []
|
| 112 |
+
for source in sorted(by_source):
|
| 113 |
+
group = sorted(by_source[source], key=lambda e: e.id)
|
| 114 |
+
rng = random.Random(f"{seed}:{source}")
|
| 115 |
+
rng.shuffle(group)
|
| 116 |
+
n_val = max(1, round(len(group) * frac))
|
| 117 |
+
val.extend(group[:n_val])
|
| 118 |
+
train.extend(group[n_val:])
|
| 119 |
+
return train, val
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def write_manifest(examples: list[Example], path) -> None:
|
| 123 |
+
path = Path(path)
|
| 124 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 125 |
+
with path.open("w", encoding="utf-8") as f:
|
| 126 |
+
for ex in examples:
|
| 127 |
+
f.write(json.dumps(ex.to_dict(), ensure_ascii=False) + "\n")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def read_manifest(path) -> list[Example]:
|
| 131 |
+
with Path(path).open("r", encoding="utf-8") as f:
|
| 132 |
+
return [Example.from_dict(json.loads(line)) for line in f if line.strip()]
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def manifest_checksum(path) -> str:
|
| 136 |
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()[:16]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _is_missing_split(err: Exception, split: str) -> bool:
|
| 140 |
+
"""True for the error `datasets` raises when a config has no such split (the only load error that is skipped)."""
|
| 141 |
+
msg = str(err)
|
| 142 |
+
return ("Unknown split" in msg or "Bad split" in msg) and split in msg
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _dataset_revision(ds) -> str | None:
|
| 146 |
+
"""The Hub commit a loaded split came from, read off its cache path (.../<config>/<version>/<sha>/...)."""
|
| 147 |
+
for f in getattr(ds, "cache_files", None) or []:
|
| 148 |
+
name = Path(str(f.get("filename", ""))).parent.name
|
| 149 |
+
if re.fullmatch(r"[0-9a-f]{40}", name):
|
| 150 |
+
return name
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _sample(rows, k: int, seed: int, key: str):
|
| 155 |
+
idx = list(range(len(rows)))
|
| 156 |
+
random.Random(f"{seed}:{key}").shuffle(idx)
|
| 157 |
+
return [rows[i] for i in sorted(idx[:k])]
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def build_manifests(out_dir, seed: int = 42, per_config: dict | None = None) -> dict:
|
| 161 |
+
# Imported here, inside try/except, so neither the tests nor Hub users of the flattened data.py need `datasets`:
|
| 162 |
+
# transformers' check_imports skips imports inside try blocks.
|
| 163 |
+
try:
|
| 164 |
+
from datasets import load_dataset
|
| 165 |
+
except ImportError as err:
|
| 166 |
+
raise ImportError('building the manifests needs the `datasets` package: pip install datasets '
|
| 167 |
+
'(or pip install -e ".[data]")') from err
|
| 168 |
+
|
| 169 |
+
per_config = per_config or {"train": 300, "validation": 100, "test": 200}
|
| 170 |
+
out = Path(out_dir)
|
| 171 |
+
revisions = defaultdict(set)
|
| 172 |
+
|
| 173 |
+
def load(repo, config, split):
|
| 174 |
+
ds = load_dataset(repo, config, split=split)
|
| 175 |
+
sha = _dataset_revision(ds)
|
| 176 |
+
if sha:
|
| 177 |
+
revisions[repo].add(sha)
|
| 178 |
+
return ds
|
| 179 |
+
|
| 180 |
+
td_train = [typed_decisions_example(r) for r in load(TYPED_DECISIONS, "all", "train")]
|
| 181 |
+
td_test = [typed_decisions_example(r) for r in load(TYPED_DECISIONS, "all", "test")]
|
| 182 |
+
train, val = split_validation(td_train, 0.1, seed)
|
| 183 |
+
jb_test = []
|
| 184 |
+
for config in JEV_BENCH_CONFIGS:
|
| 185 |
+
for split, target in (("train", train), ("validation", val), ("test", jb_test)):
|
| 186 |
+
try:
|
| 187 |
+
rows = load(JEV_BENCH, config, split)
|
| 188 |
+
except ValueError as err:
|
| 189 |
+
if not _is_missing_split(err, split):
|
| 190 |
+
raise
|
| 191 |
+
print(f"skip {config}/{split}: {err}", flush=True)
|
| 192 |
+
continue
|
| 193 |
+
rows = [rows[i] for i in range(len(rows))]
|
| 194 |
+
for r in _sample(rows, per_config[split], seed, f"{config}/{split}"):
|
| 195 |
+
target.append(jev_bench_example(r, config))
|
| 196 |
+
write_manifest(train, out / "train.jsonl")
|
| 197 |
+
write_manifest(val, out / "validation.jsonl")
|
| 198 |
+
write_manifest(td_test, out / "test_td.jsonl")
|
| 199 |
+
write_manifest(jb_test, out / "test_jb.jsonl")
|
| 200 |
+
counts = {"train": len(train), "validation": len(val), "test_td": len(td_test), "test_jb": len(jb_test)}
|
| 201 |
+
for name in ("train", "validation", "test_td", "test_jb"):
|
| 202 |
+
counts[f"{name}_sha"] = manifest_checksum(out / f"{name}.jsonl")
|
| 203 |
+
if revisions: # the dataset commit each repo was loaded from (a sorted list if the cache mixed several)
|
| 204 |
+
counts["revisions"] = {repo: next(iter(shas)) if len(shas) == 1 else sorted(shas)
|
| 205 |
+
for repo, shas in sorted(revisions.items())}
|
| 206 |
+
(out / "counts.json").write_text(json.dumps(counts, indent=2))
|
| 207 |
+
return counts
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
import argparse
|
| 212 |
+
ap = argparse.ArgumentParser()
|
| 213 |
+
ap.add_argument("--out", default="data")
|
| 214 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 215 |
+
args = ap.parse_args()
|
| 216 |
+
print(json.dumps(build_manifests(args.out, args.seed), indent=2))
|
eval/results.json
CHANGED
|
@@ -1,223 +1,223 @@
|
|
| 1 |
-
{
|
| 2 |
-
"preds": "pod_artifacts/yl3x22ceoi4s80/unpacked/eval/test_td.preds.json",
|
| 3 |
-
"manifest": "data/test_td.jsonl",
|
| 4 |
-
"labelled": 2000,
|
| 5 |
-
"rows": {
|
| 6 |
-
"DiffusionGemma zero-shot canvas read": {
|
| 7 |
-
"n": 2000,
|
| 8 |
-
"acc": 0.7075,
|
| 9 |
-
"soft_acc": 0.5731857140189666,
|
| 10 |
-
"macro_f1": 0.550702638008923,
|
| 11 |
-
"kl": 1.3327719231855613,
|
| 12 |
-
"tv": 0.3621118097383339,
|
| 13 |
-
"brier": 0.30395114563065456,
|
| 14 |
-
"nll": 2.100130478809889,
|
| 15 |
-
"ece": 0.21345150114595893,
|
| 16 |
-
"score_mae": 0.41135865707582425,
|
| 17 |
-
"within_1": 0.93375,
|
| 18 |
-
"by_type": {
|
| 19 |
-
"choice": {
|
| 20 |
-
"n": 600,
|
| 21 |
-
"acc": 0.6583333333333333,
|
| 22 |
-
"soft_acc": 0.529550382418425,
|
| 23 |
-
"macro_f1": 0.4580138346822841,
|
| 24 |
-
"kl": 1.8344121477390998,
|
| 25 |
-
"tv": 0.4134490126261477,
|
| 26 |
-
"brier": 0.3653080227268783,
|
| 27 |
-
"nll": 2.698206744585924,
|
| 28 |
-
"ece": 0.27046276137232783,
|
| 29 |
-
"score_mae": null,
|
| 30 |
-
"within_1": null
|
| 31 |
-
},
|
| 32 |
-
"noul": {
|
| 33 |
-
"n": 600,
|
| 34 |
-
"acc": 0.7716666666666666,
|
| 35 |
-
"soft_acc": 0.6983805466666665,
|
| 36 |
-
"macro_f1": 0.6222345422638039,
|
| 37 |
-
"kl": 1.0794531386915138,
|
| 38 |
-
"tv": 0.27516267522582993,
|
| 39 |
-
"brier": 0.25676828758281806,
|
| 40 |
-
"nll": 1.5331753820440792,
|
| 41 |
-
"ece": 0.19590681572755178,
|
| 42 |
-
"score_mae": null,
|
| 43 |
-
"within_1": null
|
| 44 |
-
},
|
| 45 |
-
"score": {
|
| 46 |
-
"n": 800,
|
| 47 |
-
"acc": 0.69625,
|
| 48 |
-
"soft_acc": 0.5120160882335887,
|
| 49 |
-
"macro_f1": 0.5665703123127417,
|
| 50 |
-
"kl": 1.1465308431409578,
|
| 51 |
-
"tv": 0.38882075845685227,
|
| 52 |
-
"brier": 0.2933206313443617,
|
| 53 |
-
"nll": 2.076789602052242,
|
| 54 |
-
"ece": 0.1857137256860733,
|
| 55 |
-
"score_mae": 0.41135865707582425,
|
| 56 |
-
"within_1": 0.93375
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"by_source": {
|
| 60 |
-
"typed-decisions/agent_trace_observability": {
|
| 61 |
-
"n": 500,
|
| 62 |
-
"acc": 0.604,
|
| 63 |
-
"soft_acc": 0.4776453329467184,
|
| 64 |
-
"macro_f1": 0.5448100563542999,
|
| 65 |
-
"kl": 1.3346726647537777,
|
| 66 |
-
"tv": 0.4248764365872663,
|
| 67 |
-
"brier": 0.36639518814697086,
|
| 68 |
-
"nll": 2.2131429376990006,
|
| 69 |
-
"ece": 0.2710903854966163,
|
| 70 |
-
"score_mae": 0.41654339421197734,
|
| 71 |
-
"within_1": 0.965
|
| 72 |
-
},
|
| 73 |
-
"typed-decisions/customer_service": {
|
| 74 |
-
"n": 500,
|
| 75 |
-
"acc": 0.772,
|
| 76 |
-
"soft_acc": 0.6172040574027722,
|
| 77 |
-
"macro_f1": 0.6487457943630335,
|
| 78 |
-
"kl": 1.5586844768592238,
|
| 79 |
-
"tv": 0.34467774901520876,
|
| 80 |
-
"brier": 0.28435340437718454,
|
| 81 |
-
"nll": 2.2958391148687096,
|
| 82 |
-
"ece": 0.1874517269730567,
|
| 83 |
-
"score_mae": 0.3006309953052926,
|
| 84 |
-
"within_1": 0.99
|
| 85 |
-
},
|
| 86 |
-
"typed-decisions/invoice_processing": {
|
| 87 |
-
"n": 500,
|
| 88 |
-
"acc": 0.758,
|
| 89 |
-
"soft_acc": 0.6795901281829838,
|
| 90 |
-
"macro_f1": 0.571010468782984,
|
| 91 |
-
"kl": 0.7263074853218779,
|
| 92 |
-
"tv": 0.24453685503895994,
|
| 93 |
-
"brier": 0.20045868313962484,
|
| 94 |
-
"nll": 1.301254960986774,
|
| 95 |
-
"ece": 0.1528367287516594,
|
| 96 |
-
"score_mae": 0.3736864058152724,
|
| 97 |
-
"within_1": 0.91
|
| 98 |
-
},
|
| 99 |
-
"typed-decisions/security_incidents": {
|
| 100 |
-
"n": 500,
|
| 101 |
-
"acc": 0.696,
|
| 102 |
-
"soft_acc": 0.51830333754338,
|
| 103 |
-
"macro_f1": 0.43824423253537487,
|
| 104 |
-
"kl": 1.7114230658073863,
|
| 105 |
-
"tv": 0.4343561983119038,
|
| 106 |
-
"brier": 0.36459730685883546,
|
| 107 |
-
"nll": 2.590284901685105,
|
| 108 |
-
"ece": 0.24981171822547912,
|
| 109 |
-
"score_mae": 0.5545738329707549,
|
| 110 |
-
"within_1": 0.87
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
},
|
| 114 |
-
"Hev": {
|
| 115 |
-
"n": 2000,
|
| 116 |
-
"acc": 0.731,
|
| 117 |
-
"soft_acc": 0.5841690466990694,
|
| 118 |
-
"macro_f1": 0.6263593100482722,
|
| 119 |
-
"kl": 0.18476683805325994,
|
| 120 |
-
"tv": 0.21320750403794955,
|
| 121 |
-
"brier": 0.09350640922665171,
|
| 122 |
-
"nll": 0.9521253933876698,
|
| 123 |
-
"ece": 0.16195184084028008,
|
| 124 |
-
"score_mae": 0.30327667244408074,
|
| 125 |
-
"within_1": 0.97625,
|
| 126 |
-
"by_type": {
|
| 127 |
-
"choice": {
|
| 128 |
-
"n": 600,
|
| 129 |
-
"acc": 0.6883333333333334,
|
| 130 |
-
"soft_acc": 0.5400114920828492,
|
| 131 |
-
"macro_f1": 0.5819579445698401,
|
| 132 |
-
"kl": 0.2682545787140505,
|
| 133 |
-
"tv": 0.2583168463366746,
|
| 134 |
-
"brier": 0.12431620966551991,
|
| 135 |
-
"nll": 1.1320491751871322,
|
| 136 |
-
"ece": 0.15876342798272766,
|
| 137 |
-
"score_mae": null,
|
| 138 |
-
"within_1": null
|
| 139 |
-
},
|
| 140 |
-
"noul": {
|
| 141 |
-
"n": 600,
|
| 142 |
-
"acc": 0.8133333333333334,
|
| 143 |
-
"soft_acc": 0.7151805511737863,
|
| 144 |
-
"macro_f1": 0.7030793844723765,
|
| 145 |
-
"kl": 0.10688763325700651,
|
| 146 |
-
"tv": 0.1591590390598867,
|
| 147 |
-
"brier": 0.07606820441416512,
|
| 148 |
-
"nll": 0.5606098758757383,
|
| 149 |
-
"ece": 0.08742988765239716,
|
| 150 |
-
"score_mae": null,
|
| 151 |
-
"within_1": null
|
| 152 |
-
},
|
| 153 |
-
"score": {
|
| 154 |
-
"n": 800,
|
| 155 |
-
"acc": 0.70125,
|
| 156 |
-
"soft_acc": 0.519028584305197,
|
| 157 |
-
"macro_f1": 0.602120278339018,
|
| 158 |
-
"kl": 0.1805604361548552,
|
| 159 |
-
"tv": 0.21991184604745284,
|
| 160 |
-
"brier": 0.08347771250686478,
|
| 161 |
-
"nll": 1.1108191951720112,
|
| 162 |
-
"ece": 0.22084334811195733,
|
| 163 |
-
"score_mae": 0.30327667244408074,
|
| 164 |
-
"within_1": 0.97625
|
| 165 |
-
}
|
| 166 |
-
},
|
| 167 |
-
"by_source": {
|
| 168 |
-
"typed-decisions/agent_trace_observability": {
|
| 169 |
-
"n": 500,
|
| 170 |
-
"acc": 0.654,
|
| 171 |
-
"soft_acc": 0.49528532839939,
|
| 172 |
-
"macro_f1": 0.6164904940650149,
|
| 173 |
-
"kl": 0.19085320215634394,
|
| 174 |
-
"tv": 0.21603136127034667,
|
| 175 |
-
"brier": 0.09737799980224329,
|
| 176 |
-
"nll": 1.0693234745297415,
|
| 177 |
-
"ece": 0.15192090582847595,
|
| 178 |
-
"score_mae": 0.2841385978687322,
|
| 179 |
-
"within_1": 0.99
|
| 180 |
-
},
|
| 181 |
-
"typed-decisions/customer_service": {
|
| 182 |
-
"n": 500,
|
| 183 |
-
"acc": 0.76,
|
| 184 |
-
"soft_acc": 0.6186040637716651,
|
| 185 |
-
"macro_f1": 0.696363079487989,
|
| 186 |
-
"kl": 0.18797709494129541,
|
| 187 |
-
"tv": 0.2113354651791742,
|
| 188 |
-
"brier": 0.09376893105456086,
|
| 189 |
-
"nll": 0.9251317321598946,
|
| 190 |
-
"ece": 0.14841172212362289,
|
| 191 |
-
"score_mae": 0.24546261819079518,
|
| 192 |
-
"within_1": 1.0
|
| 193 |
-
},
|
| 194 |
-
"typed-decisions/invoice_processing": {
|
| 195 |
-
"n": 500,
|
| 196 |
-
"acc": 0.786,
|
| 197 |
-
"soft_acc": 0.6944667954742908,
|
| 198 |
-
"macro_f1": 0.5980108397703063,
|
| 199 |
-
"kl": 0.22106904117845244,
|
| 200 |
-
"tv": 0.23180358876640095,
|
| 201 |
-
"brier": 0.11166511418841754,
|
| 202 |
-
"nll": 0.7960165171770665,
|
| 203 |
-
"ece": 0.1501800780892372,
|
| 204 |
-
"score_mae": 0.39022797410623755,
|
| 205 |
-
"within_1": 0.92
|
| 206 |
-
},
|
| 207 |
-
"typed-decisions/security_incidents": {
|
| 208 |
-
"n": 500,
|
| 209 |
-
"acc": 0.724,
|
| 210 |
-
"soft_acc": 0.5283199991509319,
|
| 211 |
-
"macro_f1": 0.5945728268697784,
|
| 212 |
-
"kl": 0.13916801393694356,
|
| 213 |
-
"tv": 0.19365960093587636,
|
| 214 |
-
"brier": 0.07121359186138425,
|
| 215 |
-
"nll": 1.0180298496839644,
|
| 216 |
-
"ece": 0.2086083108484745,
|
| 217 |
-
"score_mae": 0.29327749961055816,
|
| 218 |
-
"within_1": 0.995
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
}
|
| 222 |
-
}
|
| 223 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"preds": "pod_artifacts/yl3x22ceoi4s80/unpacked/eval/test_td.preds.json",
|
| 3 |
+
"manifest": "data/test_td.jsonl",
|
| 4 |
+
"labelled": 2000,
|
| 5 |
+
"rows": {
|
| 6 |
+
"DiffusionGemma zero-shot canvas read": {
|
| 7 |
+
"n": 2000,
|
| 8 |
+
"acc": 0.7075,
|
| 9 |
+
"soft_acc": 0.5731857140189666,
|
| 10 |
+
"macro_f1": 0.550702638008923,
|
| 11 |
+
"kl": 1.3327719231855613,
|
| 12 |
+
"tv": 0.3621118097383339,
|
| 13 |
+
"brier": 0.30395114563065456,
|
| 14 |
+
"nll": 2.100130478809889,
|
| 15 |
+
"ece": 0.21345150114595893,
|
| 16 |
+
"score_mae": 0.41135865707582425,
|
| 17 |
+
"within_1": 0.93375,
|
| 18 |
+
"by_type": {
|
| 19 |
+
"choice": {
|
| 20 |
+
"n": 600,
|
| 21 |
+
"acc": 0.6583333333333333,
|
| 22 |
+
"soft_acc": 0.529550382418425,
|
| 23 |
+
"macro_f1": 0.4580138346822841,
|
| 24 |
+
"kl": 1.8344121477390998,
|
| 25 |
+
"tv": 0.4134490126261477,
|
| 26 |
+
"brier": 0.3653080227268783,
|
| 27 |
+
"nll": 2.698206744585924,
|
| 28 |
+
"ece": 0.27046276137232783,
|
| 29 |
+
"score_mae": null,
|
| 30 |
+
"within_1": null
|
| 31 |
+
},
|
| 32 |
+
"noul": {
|
| 33 |
+
"n": 600,
|
| 34 |
+
"acc": 0.7716666666666666,
|
| 35 |
+
"soft_acc": 0.6983805466666665,
|
| 36 |
+
"macro_f1": 0.6222345422638039,
|
| 37 |
+
"kl": 1.0794531386915138,
|
| 38 |
+
"tv": 0.27516267522582993,
|
| 39 |
+
"brier": 0.25676828758281806,
|
| 40 |
+
"nll": 1.5331753820440792,
|
| 41 |
+
"ece": 0.19590681572755178,
|
| 42 |
+
"score_mae": null,
|
| 43 |
+
"within_1": null
|
| 44 |
+
},
|
| 45 |
+
"score": {
|
| 46 |
+
"n": 800,
|
| 47 |
+
"acc": 0.69625,
|
| 48 |
+
"soft_acc": 0.5120160882335887,
|
| 49 |
+
"macro_f1": 0.5665703123127417,
|
| 50 |
+
"kl": 1.1465308431409578,
|
| 51 |
+
"tv": 0.38882075845685227,
|
| 52 |
+
"brier": 0.2933206313443617,
|
| 53 |
+
"nll": 2.076789602052242,
|
| 54 |
+
"ece": 0.1857137256860733,
|
| 55 |
+
"score_mae": 0.41135865707582425,
|
| 56 |
+
"within_1": 0.93375
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"by_source": {
|
| 60 |
+
"typed-decisions/agent_trace_observability": {
|
| 61 |
+
"n": 500,
|
| 62 |
+
"acc": 0.604,
|
| 63 |
+
"soft_acc": 0.4776453329467184,
|
| 64 |
+
"macro_f1": 0.5448100563542999,
|
| 65 |
+
"kl": 1.3346726647537777,
|
| 66 |
+
"tv": 0.4248764365872663,
|
| 67 |
+
"brier": 0.36639518814697086,
|
| 68 |
+
"nll": 2.2131429376990006,
|
| 69 |
+
"ece": 0.2710903854966163,
|
| 70 |
+
"score_mae": 0.41654339421197734,
|
| 71 |
+
"within_1": 0.965
|
| 72 |
+
},
|
| 73 |
+
"typed-decisions/customer_service": {
|
| 74 |
+
"n": 500,
|
| 75 |
+
"acc": 0.772,
|
| 76 |
+
"soft_acc": 0.6172040574027722,
|
| 77 |
+
"macro_f1": 0.6487457943630335,
|
| 78 |
+
"kl": 1.5586844768592238,
|
| 79 |
+
"tv": 0.34467774901520876,
|
| 80 |
+
"brier": 0.28435340437718454,
|
| 81 |
+
"nll": 2.2958391148687096,
|
| 82 |
+
"ece": 0.1874517269730567,
|
| 83 |
+
"score_mae": 0.3006309953052926,
|
| 84 |
+
"within_1": 0.99
|
| 85 |
+
},
|
| 86 |
+
"typed-decisions/invoice_processing": {
|
| 87 |
+
"n": 500,
|
| 88 |
+
"acc": 0.758,
|
| 89 |
+
"soft_acc": 0.6795901281829838,
|
| 90 |
+
"macro_f1": 0.571010468782984,
|
| 91 |
+
"kl": 0.7263074853218779,
|
| 92 |
+
"tv": 0.24453685503895994,
|
| 93 |
+
"brier": 0.20045868313962484,
|
| 94 |
+
"nll": 1.301254960986774,
|
| 95 |
+
"ece": 0.1528367287516594,
|
| 96 |
+
"score_mae": 0.3736864058152724,
|
| 97 |
+
"within_1": 0.91
|
| 98 |
+
},
|
| 99 |
+
"typed-decisions/security_incidents": {
|
| 100 |
+
"n": 500,
|
| 101 |
+
"acc": 0.696,
|
| 102 |
+
"soft_acc": 0.51830333754338,
|
| 103 |
+
"macro_f1": 0.43824423253537487,
|
| 104 |
+
"kl": 1.7114230658073863,
|
| 105 |
+
"tv": 0.4343561983119038,
|
| 106 |
+
"brier": 0.36459730685883546,
|
| 107 |
+
"nll": 2.590284901685105,
|
| 108 |
+
"ece": 0.24981171822547912,
|
| 109 |
+
"score_mae": 0.5545738329707549,
|
| 110 |
+
"within_1": 0.87
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"Hev": {
|
| 115 |
+
"n": 2000,
|
| 116 |
+
"acc": 0.731,
|
| 117 |
+
"soft_acc": 0.5841690466990694,
|
| 118 |
+
"macro_f1": 0.6263593100482722,
|
| 119 |
+
"kl": 0.18476683805325994,
|
| 120 |
+
"tv": 0.21320750403794955,
|
| 121 |
+
"brier": 0.09350640922665171,
|
| 122 |
+
"nll": 0.9521253933876698,
|
| 123 |
+
"ece": 0.16195184084028008,
|
| 124 |
+
"score_mae": 0.30327667244408074,
|
| 125 |
+
"within_1": 0.97625,
|
| 126 |
+
"by_type": {
|
| 127 |
+
"choice": {
|
| 128 |
+
"n": 600,
|
| 129 |
+
"acc": 0.6883333333333334,
|
| 130 |
+
"soft_acc": 0.5400114920828492,
|
| 131 |
+
"macro_f1": 0.5819579445698401,
|
| 132 |
+
"kl": 0.2682545787140505,
|
| 133 |
+
"tv": 0.2583168463366746,
|
| 134 |
+
"brier": 0.12431620966551991,
|
| 135 |
+
"nll": 1.1320491751871322,
|
| 136 |
+
"ece": 0.15876342798272766,
|
| 137 |
+
"score_mae": null,
|
| 138 |
+
"within_1": null
|
| 139 |
+
},
|
| 140 |
+
"noul": {
|
| 141 |
+
"n": 600,
|
| 142 |
+
"acc": 0.8133333333333334,
|
| 143 |
+
"soft_acc": 0.7151805511737863,
|
| 144 |
+
"macro_f1": 0.7030793844723765,
|
| 145 |
+
"kl": 0.10688763325700651,
|
| 146 |
+
"tv": 0.1591590390598867,
|
| 147 |
+
"brier": 0.07606820441416512,
|
| 148 |
+
"nll": 0.5606098758757383,
|
| 149 |
+
"ece": 0.08742988765239716,
|
| 150 |
+
"score_mae": null,
|
| 151 |
+
"within_1": null
|
| 152 |
+
},
|
| 153 |
+
"score": {
|
| 154 |
+
"n": 800,
|
| 155 |
+
"acc": 0.70125,
|
| 156 |
+
"soft_acc": 0.519028584305197,
|
| 157 |
+
"macro_f1": 0.602120278339018,
|
| 158 |
+
"kl": 0.1805604361548552,
|
| 159 |
+
"tv": 0.21991184604745284,
|
| 160 |
+
"brier": 0.08347771250686478,
|
| 161 |
+
"nll": 1.1108191951720112,
|
| 162 |
+
"ece": 0.22084334811195733,
|
| 163 |
+
"score_mae": 0.30327667244408074,
|
| 164 |
+
"within_1": 0.97625
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"by_source": {
|
| 168 |
+
"typed-decisions/agent_trace_observability": {
|
| 169 |
+
"n": 500,
|
| 170 |
+
"acc": 0.654,
|
| 171 |
+
"soft_acc": 0.49528532839939,
|
| 172 |
+
"macro_f1": 0.6164904940650149,
|
| 173 |
+
"kl": 0.19085320215634394,
|
| 174 |
+
"tv": 0.21603136127034667,
|
| 175 |
+
"brier": 0.09737799980224329,
|
| 176 |
+
"nll": 1.0693234745297415,
|
| 177 |
+
"ece": 0.15192090582847595,
|
| 178 |
+
"score_mae": 0.2841385978687322,
|
| 179 |
+
"within_1": 0.99
|
| 180 |
+
},
|
| 181 |
+
"typed-decisions/customer_service": {
|
| 182 |
+
"n": 500,
|
| 183 |
+
"acc": 0.76,
|
| 184 |
+
"soft_acc": 0.6186040637716651,
|
| 185 |
+
"macro_f1": 0.696363079487989,
|
| 186 |
+
"kl": 0.18797709494129541,
|
| 187 |
+
"tv": 0.2113354651791742,
|
| 188 |
+
"brier": 0.09376893105456086,
|
| 189 |
+
"nll": 0.9251317321598946,
|
| 190 |
+
"ece": 0.14841172212362289,
|
| 191 |
+
"score_mae": 0.24546261819079518,
|
| 192 |
+
"within_1": 1.0
|
| 193 |
+
},
|
| 194 |
+
"typed-decisions/invoice_processing": {
|
| 195 |
+
"n": 500,
|
| 196 |
+
"acc": 0.786,
|
| 197 |
+
"soft_acc": 0.6944667954742908,
|
| 198 |
+
"macro_f1": 0.5980108397703063,
|
| 199 |
+
"kl": 0.22106904117845244,
|
| 200 |
+
"tv": 0.23180358876640095,
|
| 201 |
+
"brier": 0.11166511418841754,
|
| 202 |
+
"nll": 0.7960165171770665,
|
| 203 |
+
"ece": 0.1501800780892372,
|
| 204 |
+
"score_mae": 0.39022797410623755,
|
| 205 |
+
"within_1": 0.92
|
| 206 |
+
},
|
| 207 |
+
"typed-decisions/security_incidents": {
|
| 208 |
+
"n": 500,
|
| 209 |
+
"acc": 0.724,
|
| 210 |
+
"soft_acc": 0.5283199991509319,
|
| 211 |
+
"macro_f1": 0.5945728268697784,
|
| 212 |
+
"kl": 0.13916801393694356,
|
| 213 |
+
"tv": 0.19365960093587636,
|
| 214 |
+
"brier": 0.07121359186138425,
|
| 215 |
+
"nll": 1.0180298496839644,
|
| 216 |
+
"ece": 0.2086083108484745,
|
| 217 |
+
"score_mae": 0.29327749961055816,
|
| 218 |
+
"within_1": 0.995
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
}
|
features.py
CHANGED
|
@@ -1,345 +1,345 @@
|
|
| 1 |
-
"""Backbone feature extraction: one forward pass per example, hidden states captured by hooks."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
import math
|
| 7 |
-
import random
|
| 8 |
-
import re
|
| 9 |
-
import time
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
import torch
|
| 13 |
-
|
| 14 |
-
from .cache import CacheWriter, Decision, FeatureCache, Record
|
| 15 |
-
from .data import read_manifest
|
| 16 |
-
from .prompting import alias_token_ids, canvas_ids, shuffled_order, tokenize_prompt
|
| 17 |
-
from .schema import Example, argmax
|
| 18 |
-
|
| 19 |
-
MODEL_ID = "google/diffusiongemma-26B-A4B-it"
|
| 20 |
-
PROBE_STATE = "A short probe state."
|
| 21 |
-
RANDOM_DRAWS = 4
|
| 22 |
-
SEED_MODES = ("pad", "alias", "random")
|
| 23 |
-
ZERO_SHOT_LOG = "configs/session1_zero_shot.jsonl"
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class NormCapture:
|
| 27 |
-
"""Registers forward hooks on final-norm modules (and any modules named in `extra`) and keeps their latest
|
| 28 |
-
outputs. With no names given every norm outside the layer stacks is hooked (discovery, see probe)."""
|
| 29 |
-
|
| 30 |
-
def __init__(self, model, encoder_name: str | None = None, decoder_name: str | None = None,
|
| 31 |
-
extra: tuple[str, ...] | list[str] = ()):
|
| 32 |
-
self.outputs: dict[str, torch.Tensor] = {}
|
| 33 |
-
self.handles = []
|
| 34 |
-
extra = set(extra)
|
| 35 |
-
for name, mod in model.named_modules():
|
| 36 |
-
if name in extra:
|
| 37 |
-
self.handles.append(mod.register_forward_hook(self._hook(name)))
|
| 38 |
-
continue
|
| 39 |
-
if ".layers." in f".{name}." or "Norm" not in type(mod).__name__:
|
| 40 |
-
continue
|
| 41 |
-
if encoder_name is not None and name not in (encoder_name, decoder_name):
|
| 42 |
-
continue
|
| 43 |
-
self.handles.append(mod.register_forward_hook(self._hook(name)))
|
| 44 |
-
|
| 45 |
-
def _hook(self, name):
|
| 46 |
-
def fn(_m, _i, out):
|
| 47 |
-
self.outputs[name] = out[0] if isinstance(out, tuple) else out
|
| 48 |
-
return fn
|
| 49 |
-
|
| 50 |
-
def remove(self):
|
| 51 |
-
for h in self.handles:
|
| 52 |
-
h.remove()
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def unit_rms(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
|
| 56 |
-
"""Scale every row to unit RMS (a parameter-free RMSNorm). Intermediate layers are residual-stream states
|
| 57 |
-
of arbitrary magnitude; this keeps them on the final norm's scale and inside float16 for the cache."""
|
| 58 |
-
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
_LAYER_RE = re.compile(r"(.*\blayers)\.(\d+)")
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def layer_stacks(model) -> dict[str, int]:
|
| 65 |
-
"""Layer-stack prefixes and their depths, from module names of the form `<prefix>.layers.<n>`."""
|
| 66 |
-
stacks: dict[str, int] = {}
|
| 67 |
-
for name, _ in model.named_modules():
|
| 68 |
-
m = _LAYER_RE.fullmatch(name)
|
| 69 |
-
if m:
|
| 70 |
-
stacks[m.group(1)] = max(stacks.get(m.group(1), 0), int(m.group(2)) + 1)
|
| 71 |
-
return stacks
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def select_layers(n_layers: int, fracs) -> list[int]:
|
| 75 |
-
"""Zero-based layer indices at the given depth fractions: 0.5 of 30 layers is layer 14, 0.75 is 22."""
|
| 76 |
-
out = set()
|
| 77 |
-
for f in fracs:
|
| 78 |
-
if not 0.0 < float(f) <= 1.0:
|
| 79 |
-
raise ValueError(f"layer fraction must be in (0, 1], got {f}")
|
| 80 |
-
out.add(max(0, min(n_layers - 1, math.ceil(float(f) * n_layers) - 1)))
|
| 81 |
-
return sorted(out)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def resolve_extra_layers(model, encoder_norm: str, decoder_norm: str, fracs) -> dict[str, list[str]]:
|
| 85 |
-
"""Module names of the extra layers to capture: for each final norm, the layer stack beside it (same parent,
|
| 86 |
-
else the shallowest stack below that parent) at `fracs` of its depth."""
|
| 87 |
-
stacks = layer_stacks(model)
|
| 88 |
-
|
| 89 |
-
def parent(name: str) -> str:
|
| 90 |
-
return name.rsplit(".", 1)[0] if "." in name else ""
|
| 91 |
-
|
| 92 |
-
def stack_for(norm: str) -> str:
|
| 93 |
-
p = parent(norm)
|
| 94 |
-
beside = [s for s in stacks if parent(s) == p]
|
| 95 |
-
below = [s for s in stacks if p and s.startswith(p + ".")]
|
| 96 |
-
cands = beside or below
|
| 97 |
-
if not cands:
|
| 98 |
-
raise RuntimeError(f"no layer stack found beside {norm!r}; stacks: {sorted(stacks)}")
|
| 99 |
-
return min(cands, key=len)
|
| 100 |
-
|
| 101 |
-
enc, dec = stack_for(encoder_norm), stack_for(decoder_norm)
|
| 102 |
-
if enc == dec:
|
| 103 |
-
raise RuntimeError(f"encoder and decoder norms resolve to the same layer stack {enc!r}")
|
| 104 |
-
return {"encoder": [f"{enc}.{i}" for i in select_layers(stacks[enc], fracs)],
|
| 105 |
-
"decoder": [f"{dec}.{i}" for i in select_layers(stacks[dec], fracs)]}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def extra_layer_names(info: dict) -> list[str]:
|
| 109 |
-
extra = info.get("extra_layers") or {}
|
| 110 |
-
return list(extra.get("encoder", [])) + list(extra.get("decoder", []))
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def _probe_example() -> Example:
|
| 114 |
-
from .schema import Question
|
| 115 |
-
q = Question.from_jev("ok", {"type": "noul", "instructions": "Is the probe fine?"})
|
| 116 |
-
return Example(id="probe", source="probe", state=PROBE_STATE, questions=[q], gold={"ok": None})
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
@torch.no_grad()
|
| 120 |
-
def _forward(model, prompt_ids: list[int], canvas: list[int]):
|
| 121 |
-
dev = model.device
|
| 122 |
-
input_ids = torch.tensor([prompt_ids], dtype=torch.long, device=dev)
|
| 123 |
-
return model(input_ids=input_ids, attention_mask=torch.ones_like(input_ids),
|
| 124 |
-
decoder_input_ids=torch.tensor([canvas], dtype=torch.long, device=dev))
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def probe(model, tok, hidden_size: int) -> dict:
|
| 128 |
-
ex = _probe_example()
|
| 129 |
-
tp = tokenize_prompt(ex, tok)
|
| 130 |
-
for canvas_length in (64, 256):
|
| 131 |
-
canvas, _ = canvas_ids(ex, tok, slot_token_id=0, canvas_length=canvas_length)
|
| 132 |
-
cap = NormCapture(model)
|
| 133 |
-
try:
|
| 134 |
-
_forward(model, tp.ids, canvas)
|
| 135 |
-
except Exception as err: # noqa: BLE001 - the whole point is to learn which length works
|
| 136 |
-
cap.remove()
|
| 137 |
-
print(f"canvas_length {canvas_length} failed: {type(err).__name__}: {err}")
|
| 138 |
-
continue
|
| 139 |
-
cap.remove()
|
| 140 |
-
enc = [n for n, o in cap.outputs.items() if o.shape[-2] == len(tp.ids) and o.shape[-1] == hidden_size]
|
| 141 |
-
dec = [n for n, o in cap.outputs.items() if o.shape[-2] == canvas_length and o.shape[-1] == hidden_size]
|
| 142 |
-
if not enc or not dec:
|
| 143 |
-
shapes = {k: tuple(v.shape) for k, v in cap.outputs.items()}
|
| 144 |
-
raise RuntimeError(f"could not find final norms; captured {shapes}")
|
| 145 |
-
if enc[-1] == dec[-1]:
|
| 146 |
-
raise RuntimeError(f"encoder and decoder norms resolved to the same module: {enc[-1]}")
|
| 147 |
-
if len(enc) > 1 or len(dec) > 1:
|
| 148 |
-
print(f"warning: several final-norm candidates, using the last; encoder {enc}, decoder {dec}")
|
| 149 |
-
return {"canvas_length": canvas_length, "encoder_norm": enc[-1], "decoder_norm": dec[-1],
|
| 150 |
-
"hidden_size": hidden_size}
|
| 151 |
-
raise RuntimeError("neither canvas length 64 nor 256 ran")
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def _slot_token(seed_mode: str, alias_ids: list[int], rng: random.Random, vocab: int) -> list[int]:
|
| 155 |
-
if seed_mode == "pad":
|
| 156 |
-
return [0]
|
| 157 |
-
if seed_mode == "alias":
|
| 158 |
-
return [alias_ids[0]]
|
| 159 |
-
if seed_mode == "random":
|
| 160 |
-
return [rng.randrange(10, vocab) for _ in range(RANDOM_DRAWS)]
|
| 161 |
-
raise ValueError(f"unknown seed_mode {seed_mode}")
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def _rows(cap: "NormCapture", name: str, n_rows: int, what: str) -> torch.Tensor:
|
| 165 |
-
"""A captured layer's rows for this forward (batch element 0), checked against the expected row count."""
|
| 166 |
-
out = cap.outputs[name][0].float()
|
| 167 |
-
if out.shape[0] != n_rows:
|
| 168 |
-
raise RuntimeError(f"{name} output has shape {tuple(out.shape)}, expected {n_rows} {what} rows")
|
| 169 |
-
return out
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
@torch.no_grad()
|
| 173 |
-
def encode_example(model, tok, ex: Example, cap: "NormCapture", info: dict, *, slot_tokens: list[int],
|
| 174 |
-
alias_ids: list[int], max_tokens: int = 1024, option_order=None) -> tuple[torch.Tensor, Record]:
|
| 175 |
-
"""One example through the backbone: features averaged over slot tokens, plus a Record with priors.
|
| 176 |
-
|
| 177 |
-
The Record's path is left empty; the caller names the file.
|
| 178 |
-
"""
|
| 179 |
-
canvas_length = info["canvas_length"]
|
| 180 |
-
tp = tokenize_prompt(ex, tok, max_tokens=max_tokens, option_order=option_order)
|
| 181 |
-
feats_sum, prob_sum, slots = None, None, None
|
| 182 |
-
for slot_tok in slot_tokens:
|
| 183 |
-
canvas, slots = canvas_ids(ex, tok, slot_token_id=slot_tok, canvas_length=canvas_length)
|
| 184 |
-
cap.outputs.clear()
|
| 185 |
-
out = _forward(model, tp.ids, canvas)
|
| 186 |
-
if out.logits.shape[1] != canvas_length:
|
| 187 |
-
raise RuntimeError(f"logits cover {out.logits.shape[1]} positions, expected canvas_length {canvas_length}")
|
| 188 |
-
extra = info.get("extra_layers") or {}
|
| 189 |
-
wanted = [info["encoder_norm"], info["decoder_norm"], *extra_layer_names(info)]
|
| 190 |
-
missing = [n for n in wanted if n not in cap.outputs]
|
| 191 |
-
if missing:
|
| 192 |
-
raise RuntimeError(f"norm outputs missing after forward: {missing}; captured {sorted(cap.outputs)}")
|
| 193 |
-
hp = cap.outputs[info["encoder_norm"]][0].float()
|
| 194 |
-
hc = cap.outputs[info["decoder_norm"]][0].float()
|
| 195 |
-
if hp.shape[0] != len(tp.ids):
|
| 196 |
-
raise RuntimeError(f"encoder norm output has shape {tuple(hp.shape)}, expected {len(tp.ids)} prompt rows")
|
| 197 |
-
if hc.shape[0] != canvas_length:
|
| 198 |
-
raise RuntimeError(f"decoder norm output has shape {tuple(hc.shape)}, expected canvas_length {canvas_length}")
|
| 199 |
-
enc_extra = [unit_rms(_rows(cap, name, len(tp.ids), "prompt")) for name in extra.get("encoder", [])]
|
| 200 |
-
dec_extra = [unit_rms(_rows(cap, name, canvas_length, "canvas")) for name in extra.get("decoder", [])]
|
| 201 |
-
feats = torch.cat([torch.cat([hp, *enc_extra], dim=1), torch.cat([hc, *dec_extra], dim=1)], dim=0)
|
| 202 |
-
probs = torch.softmax(out.logits[0].float(), dim=-1)[slots] # (n_q, vocab)
|
| 203 |
-
feats_sum = feats if feats_sum is None else feats_sum + feats
|
| 204 |
-
prob_sum = probs if prob_sum is None else prob_sum + probs
|
| 205 |
-
feats = feats_sum / len(slot_tokens)
|
| 206 |
-
logp = (prob_sum / len(slot_tokens)).clamp_min(1e-12).log()
|
| 207 |
-
decisions = []
|
| 208 |
-
for qi, q in enumerate(ex.questions):
|
| 209 |
-
ids = torch.tensor([alias_ids[a] for a in tp.option_alias[qi]], device=logp.device)
|
| 210 |
-
prior = logp[qi, ids]
|
| 211 |
-
prior = (prior - torch.logsumexp(prior, dim=0)).tolist()
|
| 212 |
-
decisions.append(Decision(qid=q.qid, type=q.type, slot=len(tp.ids) + slots[qi],
|
| 213 |
-
spans=[list(s) for s in tp.option_spans[qi]],
|
| 214 |
-
alias=list(tp.option_alias[qi]), prior=prior, gold=ex.gold.get(q.qid)))
|
| 215 |
-
rec = Record(id=ex.id, source=ex.source, path="", n_prompt=len(tp.ids),
|
| 216 |
-
n_canvas=canvas_length, decisions=decisions)
|
| 217 |
-
return feats, rec
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def extract(model, tok, examples: list[Example], out_dir, *, seed_mode: str, canvas_length: int,
|
| 221 |
-
alias_ids: list[int], hidden_size: int, max_tokens: int = 1024, shuffle_rng=None,
|
| 222 |
-
limit: int | None = None, vocab_size: int = 262144, log_every: int = 50,
|
| 223 |
-
info: dict | None = None, layer_fracs=()) -> dict:
|
| 224 |
-
"""Write a feature cache. With `layer_fracs` (depth fractions, e.g. (0.5, 0.75)) every row also carries the
|
| 225 |
-
unit-RMS outputs of the encoder (prompt rows) or decoder (canvas rows) layers at those depths, appended to the
|
| 226 |
-
final-norm block, so the feature dim is hidden_size * (1 + len(layers))."""
|
| 227 |
-
info = dict(probe(model, tok, hidden_size) if info is None else info)
|
| 228 |
-
if info["canvas_length"] != canvas_length:
|
| 229 |
-
raise ValueError(f"requested canvas {canvas_length} but probe says {info['canvas_length']}")
|
| 230 |
-
if layer_fracs:
|
| 231 |
-
info["extra_layers"] = resolve_extra_layers(model, info["encoder_norm"], info["decoder_norm"], layer_fracs)
|
| 232 |
-
writer = CacheWriter(out_dir) # opened before the hooks go on, so a folder that cannot be written leaks nothing
|
| 233 |
-
cap = NormCapture(model, info["encoder_norm"], info["decoder_norm"], extra=extra_layer_names(info))
|
| 234 |
-
rng = random.Random(1234)
|
| 235 |
-
n_dec, t0 = 0, time.time()
|
| 236 |
-
examples = examples[:limit] if limit else examples
|
| 237 |
-
try:
|
| 238 |
-
for i, ex in enumerate(examples):
|
| 239 |
-
order = shuffled_order(ex, shuffle_rng) if shuffle_rng is not None else None
|
| 240 |
-
draws = _slot_token(seed_mode, alias_ids, rng, vocab_size)
|
| 241 |
-
feats, rec = encode_example(model, tok, ex, cap, info, slot_tokens=draws, alias_ids=alias_ids,
|
| 242 |
-
max_tokens=max_tokens, option_order=order)
|
| 243 |
-
rec.path = f"{ex.id.replace('/', '_')}.safetensors"
|
| 244 |
-
writer.write(rec, feats)
|
| 245 |
-
n_dec += len(rec.decisions)
|
| 246 |
-
if (i + 1) % log_every == 0:
|
| 247 |
-
print(f"{i + 1}/{len(examples)} examples, {time.time() - t0:.0f}s", flush=True)
|
| 248 |
-
finally: # close the manifest and drop the hooks even when an example raises
|
| 249 |
-
writer.close()
|
| 250 |
-
cap.remove()
|
| 251 |
-
n_extra = len((info.get("extra_layers") or {}).get("encoder", []))
|
| 252 |
-
summary = {"examples": len(examples), "decisions": n_dec, "seed_mode": seed_mode,
|
| 253 |
-
"canvas_length": canvas_length, "seconds": round(time.time() - t0, 1),
|
| 254 |
-
"feature_dim": hidden_size * (1 + n_extra), **info}
|
| 255 |
-
Path(out_dir, "summary.json").write_text(json.dumps(summary, indent=2))
|
| 256 |
-
return summary
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
def zero_shot_accuracy(cache_dir) -> float:
|
| 260 |
-
cache = FeatureCache(cache_dir)
|
| 261 |
-
hits = total = 0
|
| 262 |
-
for rec in cache.records:
|
| 263 |
-
for d in rec.decisions:
|
| 264 |
-
if d.gold is None:
|
| 265 |
-
continue
|
| 266 |
-
total += 1
|
| 267 |
-
hits += int(argmax(d.prior) == argmax(d.gold))
|
| 268 |
-
return hits / max(total, 1)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
def load_backbone(model_id: str = MODEL_ID, load_in_4bit: bool = False):
|
| 272 |
-
from transformers import AutoTokenizer, DiffusionGemmaForBlockDiffusion
|
| 273 |
-
kwargs = {"device_map": "auto", "dtype": torch.bfloat16}
|
| 274 |
-
if load_in_4bit:
|
| 275 |
-
from transformers import BitsAndBytesConfig
|
| 276 |
-
kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)
|
| 277 |
-
model = DiffusionGemmaForBlockDiffusion.from_pretrained(model_id, **kwargs).eval()
|
| 278 |
-
return model, AutoTokenizer.from_pretrained(model_id)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
def main():
|
| 282 |
-
ap = argparse.ArgumentParser()
|
| 283 |
-
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 284 |
-
p = sub.add_parser("probe")
|
| 285 |
-
p.add_argument("--model", default=MODEL_ID)
|
| 286 |
-
e = sub.add_parser("extract")
|
| 287 |
-
e.add_argument("--model", default=MODEL_ID)
|
| 288 |
-
e.add_argument("--manifest", required=True)
|
| 289 |
-
e.add_argument("--out", required=True)
|
| 290 |
-
e.add_argument("--seed-mode", default="pad",
|
| 291 |
-
help="pad, alias or random, or a comma-separated list of them; a list writes <out>_<mode> per mode")
|
| 292 |
-
e.add_argument("--canvas", type=int, default=64)
|
| 293 |
-
e.add_argument("--limit", type=int, default=None)
|
| 294 |
-
e.add_argument("--shuffle-seed", type=int, default=None, help="shuffle choice options (training augmentation)")
|
| 295 |
-
e.add_argument("--load-in-4bit", action="store_true")
|
| 296 |
-
e.add_argument("--zero-shot-log", action="store_true", help=f"append each cache's zero-shot accuracy to {ZERO_SHOT_LOG}")
|
| 297 |
-
for sp in (p, e):
|
| 298 |
-
sp.add_argument("--layers", default=None,
|
| 299 |
-
help="depth fractions of extra encoder/decoder layers to append to the features, e.g. 0.5,0.75 "
|
| 300 |
-
"(feature dim becomes hidden_size * (1 + count)); probe records their module names")
|
| 301 |
-
z = sub.add_parser("zero-shot")
|
| 302 |
-
z.add_argument("--cache", required=True)
|
| 303 |
-
args = ap.parse_args()
|
| 304 |
-
fracs = tuple(float(x) for x in args.layers.split(",")) if getattr(args, "layers", None) else ()
|
| 305 |
-
if args.cmd == "zero-shot":
|
| 306 |
-
print(json.dumps({"cache": args.cache, "zero_shot_accuracy": zero_shot_accuracy(args.cache)}))
|
| 307 |
-
return
|
| 308 |
-
if args.cmd == "extract":
|
| 309 |
-
modes = args.seed_mode.split(",")
|
| 310 |
-
if any(m not in SEED_MODES for m in modes) or len(set(modes)) != len(modes):
|
| 311 |
-
e.error(f"--seed-mode takes distinct values from {', '.join(SEED_MODES)}; got {args.seed_mode!r}")
|
| 312 |
-
model, tok = load_backbone(args.model, getattr(args, "load_in_4bit", False))
|
| 313 |
-
hidden = model.config.text_config.hidden_size
|
| 314 |
-
if args.cmd == "probe":
|
| 315 |
-
info = probe(model, tok, hidden)
|
| 316 |
-
if fracs:
|
| 317 |
-
info["extra_layers"] = resolve_extra_layers(model, info["encoder_norm"], info["decoder_norm"], fracs)
|
| 318 |
-
info["feature_dim"] = hidden * (1 + len(info["extra_layers"]["encoder"]))
|
| 319 |
-
Path("configs").mkdir(exist_ok=True)
|
| 320 |
-
Path("configs/backbone.json").write_text(json.dumps({**info, "model_id": args.model}, indent=2))
|
| 321 |
-
print(json.dumps(info, indent=2))
|
| 322 |
-
return
|
| 323 |
-
alias = alias_token_ids(tok, 64)
|
| 324 |
-
vocab = getattr(model.config.text_config, "vocab_size", 262144)
|
| 325 |
-
examples = read_manifest(args.manifest)
|
| 326 |
-
info = probe(model, tok, hidden) if len(modes) > 1 else None # one probe shared by every mode
|
| 327 |
-
for mode in modes:
|
| 328 |
-
out = f"{args.out}_{mode}" if len(modes) > 1 else args.out
|
| 329 |
-
# a fresh rng per mode, so each cache matches what a separate run of that mode would write
|
| 330 |
-
rng = random.Random(args.shuffle_seed) if args.shuffle_seed is not None else None
|
| 331 |
-
summary = extract(model, tok, examples, out, seed_mode=mode, canvas_length=args.canvas, alias_ids=alias,
|
| 332 |
-
hidden_size=hidden, shuffle_rng=rng, limit=args.limit, vocab_size=vocab, info=info,
|
| 333 |
-
layer_fracs=fracs)
|
| 334 |
-
print(json.dumps(summary, indent=2))
|
| 335 |
-
acc = zero_shot_accuracy(out)
|
| 336 |
-
print(f"zero_shot {mode} {acc:.4f}", flush=True)
|
| 337 |
-
if args.zero_shot_log:
|
| 338 |
-
log = Path(ZERO_SHOT_LOG)
|
| 339 |
-
log.parent.mkdir(parents=True, exist_ok=True)
|
| 340 |
-
with log.open("a", encoding="utf-8") as f:
|
| 341 |
-
f.write(json.dumps({"cache": out, "seed_mode": mode, "zero_shot_accuracy": acc}) + "\n")
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
if __name__ == "__main__":
|
| 345 |
-
main()
|
|
|
|
| 1 |
+
"""Backbone feature extraction: one forward pass per example, hidden states captured by hooks."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import random
|
| 8 |
+
import re
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from .cache import CacheWriter, Decision, FeatureCache, Record
|
| 15 |
+
from .data import read_manifest
|
| 16 |
+
from .prompting import alias_token_ids, canvas_ids, shuffled_order, tokenize_prompt
|
| 17 |
+
from .schema import Example, argmax
|
| 18 |
+
|
| 19 |
+
MODEL_ID = "google/diffusiongemma-26B-A4B-it"
|
| 20 |
+
PROBE_STATE = "A short probe state."
|
| 21 |
+
RANDOM_DRAWS = 4
|
| 22 |
+
SEED_MODES = ("pad", "alias", "random")
|
| 23 |
+
ZERO_SHOT_LOG = "configs/session1_zero_shot.jsonl"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class NormCapture:
|
| 27 |
+
"""Registers forward hooks on final-norm modules (and any modules named in `extra`) and keeps their latest
|
| 28 |
+
outputs. With no names given every norm outside the layer stacks is hooked (discovery, see probe)."""
|
| 29 |
+
|
| 30 |
+
def __init__(self, model, encoder_name: str | None = None, decoder_name: str | None = None,
|
| 31 |
+
extra: tuple[str, ...] | list[str] = ()):
|
| 32 |
+
self.outputs: dict[str, torch.Tensor] = {}
|
| 33 |
+
self.handles = []
|
| 34 |
+
extra = set(extra)
|
| 35 |
+
for name, mod in model.named_modules():
|
| 36 |
+
if name in extra:
|
| 37 |
+
self.handles.append(mod.register_forward_hook(self._hook(name)))
|
| 38 |
+
continue
|
| 39 |
+
if ".layers." in f".{name}." or "Norm" not in type(mod).__name__:
|
| 40 |
+
continue
|
| 41 |
+
if encoder_name is not None and name not in (encoder_name, decoder_name):
|
| 42 |
+
continue
|
| 43 |
+
self.handles.append(mod.register_forward_hook(self._hook(name)))
|
| 44 |
+
|
| 45 |
+
def _hook(self, name):
|
| 46 |
+
def fn(_m, _i, out):
|
| 47 |
+
self.outputs[name] = out[0] if isinstance(out, tuple) else out
|
| 48 |
+
return fn
|
| 49 |
+
|
| 50 |
+
def remove(self):
|
| 51 |
+
for h in self.handles:
|
| 52 |
+
h.remove()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def unit_rms(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
|
| 56 |
+
"""Scale every row to unit RMS (a parameter-free RMSNorm). Intermediate layers are residual-stream states
|
| 57 |
+
of arbitrary magnitude; this keeps them on the final norm's scale and inside float16 for the cache."""
|
| 58 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
_LAYER_RE = re.compile(r"(.*\blayers)\.(\d+)")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def layer_stacks(model) -> dict[str, int]:
|
| 65 |
+
"""Layer-stack prefixes and their depths, from module names of the form `<prefix>.layers.<n>`."""
|
| 66 |
+
stacks: dict[str, int] = {}
|
| 67 |
+
for name, _ in model.named_modules():
|
| 68 |
+
m = _LAYER_RE.fullmatch(name)
|
| 69 |
+
if m:
|
| 70 |
+
stacks[m.group(1)] = max(stacks.get(m.group(1), 0), int(m.group(2)) + 1)
|
| 71 |
+
return stacks
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def select_layers(n_layers: int, fracs) -> list[int]:
|
| 75 |
+
"""Zero-based layer indices at the given depth fractions: 0.5 of 30 layers is layer 14, 0.75 is 22."""
|
| 76 |
+
out = set()
|
| 77 |
+
for f in fracs:
|
| 78 |
+
if not 0.0 < float(f) <= 1.0:
|
| 79 |
+
raise ValueError(f"layer fraction must be in (0, 1], got {f}")
|
| 80 |
+
out.add(max(0, min(n_layers - 1, math.ceil(float(f) * n_layers) - 1)))
|
| 81 |
+
return sorted(out)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def resolve_extra_layers(model, encoder_norm: str, decoder_norm: str, fracs) -> dict[str, list[str]]:
|
| 85 |
+
"""Module names of the extra layers to capture: for each final norm, the layer stack beside it (same parent,
|
| 86 |
+
else the shallowest stack below that parent) at `fracs` of its depth."""
|
| 87 |
+
stacks = layer_stacks(model)
|
| 88 |
+
|
| 89 |
+
def parent(name: str) -> str:
|
| 90 |
+
return name.rsplit(".", 1)[0] if "." in name else ""
|
| 91 |
+
|
| 92 |
+
def stack_for(norm: str) -> str:
|
| 93 |
+
p = parent(norm)
|
| 94 |
+
beside = [s for s in stacks if parent(s) == p]
|
| 95 |
+
below = [s for s in stacks if p and s.startswith(p + ".")]
|
| 96 |
+
cands = beside or below
|
| 97 |
+
if not cands:
|
| 98 |
+
raise RuntimeError(f"no layer stack found beside {norm!r}; stacks: {sorted(stacks)}")
|
| 99 |
+
return min(cands, key=len)
|
| 100 |
+
|
| 101 |
+
enc, dec = stack_for(encoder_norm), stack_for(decoder_norm)
|
| 102 |
+
if enc == dec:
|
| 103 |
+
raise RuntimeError(f"encoder and decoder norms resolve to the same layer stack {enc!r}")
|
| 104 |
+
return {"encoder": [f"{enc}.{i}" for i in select_layers(stacks[enc], fracs)],
|
| 105 |
+
"decoder": [f"{dec}.{i}" for i in select_layers(stacks[dec], fracs)]}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def extra_layer_names(info: dict) -> list[str]:
|
| 109 |
+
extra = info.get("extra_layers") or {}
|
| 110 |
+
return list(extra.get("encoder", [])) + list(extra.get("decoder", []))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _probe_example() -> Example:
|
| 114 |
+
from .schema import Question
|
| 115 |
+
q = Question.from_jev("ok", {"type": "noul", "instructions": "Is the probe fine?"})
|
| 116 |
+
return Example(id="probe", source="probe", state=PROBE_STATE, questions=[q], gold={"ok": None})
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@torch.no_grad()
|
| 120 |
+
def _forward(model, prompt_ids: list[int], canvas: list[int]):
|
| 121 |
+
dev = model.device
|
| 122 |
+
input_ids = torch.tensor([prompt_ids], dtype=torch.long, device=dev)
|
| 123 |
+
return model(input_ids=input_ids, attention_mask=torch.ones_like(input_ids),
|
| 124 |
+
decoder_input_ids=torch.tensor([canvas], dtype=torch.long, device=dev))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def probe(model, tok, hidden_size: int) -> dict:
|
| 128 |
+
ex = _probe_example()
|
| 129 |
+
tp = tokenize_prompt(ex, tok)
|
| 130 |
+
for canvas_length in (64, 256):
|
| 131 |
+
canvas, _ = canvas_ids(ex, tok, slot_token_id=0, canvas_length=canvas_length)
|
| 132 |
+
cap = NormCapture(model)
|
| 133 |
+
try:
|
| 134 |
+
_forward(model, tp.ids, canvas)
|
| 135 |
+
except Exception as err: # noqa: BLE001 - the whole point is to learn which length works
|
| 136 |
+
cap.remove()
|
| 137 |
+
print(f"canvas_length {canvas_length} failed: {type(err).__name__}: {err}")
|
| 138 |
+
continue
|
| 139 |
+
cap.remove()
|
| 140 |
+
enc = [n for n, o in cap.outputs.items() if o.shape[-2] == len(tp.ids) and o.shape[-1] == hidden_size]
|
| 141 |
+
dec = [n for n, o in cap.outputs.items() if o.shape[-2] == canvas_length and o.shape[-1] == hidden_size]
|
| 142 |
+
if not enc or not dec:
|
| 143 |
+
shapes = {k: tuple(v.shape) for k, v in cap.outputs.items()}
|
| 144 |
+
raise RuntimeError(f"could not find final norms; captured {shapes}")
|
| 145 |
+
if enc[-1] == dec[-1]:
|
| 146 |
+
raise RuntimeError(f"encoder and decoder norms resolved to the same module: {enc[-1]}")
|
| 147 |
+
if len(enc) > 1 or len(dec) > 1:
|
| 148 |
+
print(f"warning: several final-norm candidates, using the last; encoder {enc}, decoder {dec}")
|
| 149 |
+
return {"canvas_length": canvas_length, "encoder_norm": enc[-1], "decoder_norm": dec[-1],
|
| 150 |
+
"hidden_size": hidden_size}
|
| 151 |
+
raise RuntimeError("neither canvas length 64 nor 256 ran")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _slot_token(seed_mode: str, alias_ids: list[int], rng: random.Random, vocab: int) -> list[int]:
|
| 155 |
+
if seed_mode == "pad":
|
| 156 |
+
return [0]
|
| 157 |
+
if seed_mode == "alias":
|
| 158 |
+
return [alias_ids[0]]
|
| 159 |
+
if seed_mode == "random":
|
| 160 |
+
return [rng.randrange(10, vocab) for _ in range(RANDOM_DRAWS)]
|
| 161 |
+
raise ValueError(f"unknown seed_mode {seed_mode}")
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _rows(cap: "NormCapture", name: str, n_rows: int, what: str) -> torch.Tensor:
|
| 165 |
+
"""A captured layer's rows for this forward (batch element 0), checked against the expected row count."""
|
| 166 |
+
out = cap.outputs[name][0].float()
|
| 167 |
+
if out.shape[0] != n_rows:
|
| 168 |
+
raise RuntimeError(f"{name} output has shape {tuple(out.shape)}, expected {n_rows} {what} rows")
|
| 169 |
+
return out
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@torch.no_grad()
|
| 173 |
+
def encode_example(model, tok, ex: Example, cap: "NormCapture", info: dict, *, slot_tokens: list[int],
|
| 174 |
+
alias_ids: list[int], max_tokens: int = 1024, option_order=None) -> tuple[torch.Tensor, Record]:
|
| 175 |
+
"""One example through the backbone: features averaged over slot tokens, plus a Record with priors.
|
| 176 |
+
|
| 177 |
+
The Record's path is left empty; the caller names the file.
|
| 178 |
+
"""
|
| 179 |
+
canvas_length = info["canvas_length"]
|
| 180 |
+
tp = tokenize_prompt(ex, tok, max_tokens=max_tokens, option_order=option_order)
|
| 181 |
+
feats_sum, prob_sum, slots = None, None, None
|
| 182 |
+
for slot_tok in slot_tokens:
|
| 183 |
+
canvas, slots = canvas_ids(ex, tok, slot_token_id=slot_tok, canvas_length=canvas_length)
|
| 184 |
+
cap.outputs.clear()
|
| 185 |
+
out = _forward(model, tp.ids, canvas)
|
| 186 |
+
if out.logits.shape[1] != canvas_length:
|
| 187 |
+
raise RuntimeError(f"logits cover {out.logits.shape[1]} positions, expected canvas_length {canvas_length}")
|
| 188 |
+
extra = info.get("extra_layers") or {}
|
| 189 |
+
wanted = [info["encoder_norm"], info["decoder_norm"], *extra_layer_names(info)]
|
| 190 |
+
missing = [n for n in wanted if n not in cap.outputs]
|
| 191 |
+
if missing:
|
| 192 |
+
raise RuntimeError(f"norm outputs missing after forward: {missing}; captured {sorted(cap.outputs)}")
|
| 193 |
+
hp = cap.outputs[info["encoder_norm"]][0].float()
|
| 194 |
+
hc = cap.outputs[info["decoder_norm"]][0].float()
|
| 195 |
+
if hp.shape[0] != len(tp.ids):
|
| 196 |
+
raise RuntimeError(f"encoder norm output has shape {tuple(hp.shape)}, expected {len(tp.ids)} prompt rows")
|
| 197 |
+
if hc.shape[0] != canvas_length:
|
| 198 |
+
raise RuntimeError(f"decoder norm output has shape {tuple(hc.shape)}, expected canvas_length {canvas_length}")
|
| 199 |
+
enc_extra = [unit_rms(_rows(cap, name, len(tp.ids), "prompt")) for name in extra.get("encoder", [])]
|
| 200 |
+
dec_extra = [unit_rms(_rows(cap, name, canvas_length, "canvas")) for name in extra.get("decoder", [])]
|
| 201 |
+
feats = torch.cat([torch.cat([hp, *enc_extra], dim=1), torch.cat([hc, *dec_extra], dim=1)], dim=0)
|
| 202 |
+
probs = torch.softmax(out.logits[0].float(), dim=-1)[slots] # (n_q, vocab)
|
| 203 |
+
feats_sum = feats if feats_sum is None else feats_sum + feats
|
| 204 |
+
prob_sum = probs if prob_sum is None else prob_sum + probs
|
| 205 |
+
feats = feats_sum / len(slot_tokens)
|
| 206 |
+
logp = (prob_sum / len(slot_tokens)).clamp_min(1e-12).log()
|
| 207 |
+
decisions = []
|
| 208 |
+
for qi, q in enumerate(ex.questions):
|
| 209 |
+
ids = torch.tensor([alias_ids[a] for a in tp.option_alias[qi]], device=logp.device)
|
| 210 |
+
prior = logp[qi, ids]
|
| 211 |
+
prior = (prior - torch.logsumexp(prior, dim=0)).tolist()
|
| 212 |
+
decisions.append(Decision(qid=q.qid, type=q.type, slot=len(tp.ids) + slots[qi],
|
| 213 |
+
spans=[list(s) for s in tp.option_spans[qi]],
|
| 214 |
+
alias=list(tp.option_alias[qi]), prior=prior, gold=ex.gold.get(q.qid)))
|
| 215 |
+
rec = Record(id=ex.id, source=ex.source, path="", n_prompt=len(tp.ids),
|
| 216 |
+
n_canvas=canvas_length, decisions=decisions)
|
| 217 |
+
return feats, rec
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def extract(model, tok, examples: list[Example], out_dir, *, seed_mode: str, canvas_length: int,
|
| 221 |
+
alias_ids: list[int], hidden_size: int, max_tokens: int = 1024, shuffle_rng=None,
|
| 222 |
+
limit: int | None = None, vocab_size: int = 262144, log_every: int = 50,
|
| 223 |
+
info: dict | None = None, layer_fracs=()) -> dict:
|
| 224 |
+
"""Write a feature cache. With `layer_fracs` (depth fractions, e.g. (0.5, 0.75)) every row also carries the
|
| 225 |
+
unit-RMS outputs of the encoder (prompt rows) or decoder (canvas rows) layers at those depths, appended to the
|
| 226 |
+
final-norm block, so the feature dim is hidden_size * (1 + len(layers))."""
|
| 227 |
+
info = dict(probe(model, tok, hidden_size) if info is None else info)
|
| 228 |
+
if info["canvas_length"] != canvas_length:
|
| 229 |
+
raise ValueError(f"requested canvas {canvas_length} but probe says {info['canvas_length']}")
|
| 230 |
+
if layer_fracs:
|
| 231 |
+
info["extra_layers"] = resolve_extra_layers(model, info["encoder_norm"], info["decoder_norm"], layer_fracs)
|
| 232 |
+
writer = CacheWriter(out_dir) # opened before the hooks go on, so a folder that cannot be written leaks nothing
|
| 233 |
+
cap = NormCapture(model, info["encoder_norm"], info["decoder_norm"], extra=extra_layer_names(info))
|
| 234 |
+
rng = random.Random(1234)
|
| 235 |
+
n_dec, t0 = 0, time.time()
|
| 236 |
+
examples = examples[:limit] if limit else examples
|
| 237 |
+
try:
|
| 238 |
+
for i, ex in enumerate(examples):
|
| 239 |
+
order = shuffled_order(ex, shuffle_rng) if shuffle_rng is not None else None
|
| 240 |
+
draws = _slot_token(seed_mode, alias_ids, rng, vocab_size)
|
| 241 |
+
feats, rec = encode_example(model, tok, ex, cap, info, slot_tokens=draws, alias_ids=alias_ids,
|
| 242 |
+
max_tokens=max_tokens, option_order=order)
|
| 243 |
+
rec.path = f"{ex.id.replace('/', '_')}.safetensors"
|
| 244 |
+
writer.write(rec, feats)
|
| 245 |
+
n_dec += len(rec.decisions)
|
| 246 |
+
if (i + 1) % log_every == 0:
|
| 247 |
+
print(f"{i + 1}/{len(examples)} examples, {time.time() - t0:.0f}s", flush=True)
|
| 248 |
+
finally: # close the manifest and drop the hooks even when an example raises
|
| 249 |
+
writer.close()
|
| 250 |
+
cap.remove()
|
| 251 |
+
n_extra = len((info.get("extra_layers") or {}).get("encoder", []))
|
| 252 |
+
summary = {"examples": len(examples), "decisions": n_dec, "seed_mode": seed_mode,
|
| 253 |
+
"canvas_length": canvas_length, "seconds": round(time.time() - t0, 1),
|
| 254 |
+
"feature_dim": hidden_size * (1 + n_extra), **info}
|
| 255 |
+
Path(out_dir, "summary.json").write_text(json.dumps(summary, indent=2))
|
| 256 |
+
return summary
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def zero_shot_accuracy(cache_dir) -> float:
|
| 260 |
+
cache = FeatureCache(cache_dir)
|
| 261 |
+
hits = total = 0
|
| 262 |
+
for rec in cache.records:
|
| 263 |
+
for d in rec.decisions:
|
| 264 |
+
if d.gold is None:
|
| 265 |
+
continue
|
| 266 |
+
total += 1
|
| 267 |
+
hits += int(argmax(d.prior) == argmax(d.gold))
|
| 268 |
+
return hits / max(total, 1)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def load_backbone(model_id: str = MODEL_ID, load_in_4bit: bool = False):
|
| 272 |
+
from transformers import AutoTokenizer, DiffusionGemmaForBlockDiffusion
|
| 273 |
+
kwargs = {"device_map": "auto", "dtype": torch.bfloat16}
|
| 274 |
+
if load_in_4bit:
|
| 275 |
+
from transformers import BitsAndBytesConfig
|
| 276 |
+
kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)
|
| 277 |
+
model = DiffusionGemmaForBlockDiffusion.from_pretrained(model_id, **kwargs).eval()
|
| 278 |
+
return model, AutoTokenizer.from_pretrained(model_id)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def main():
|
| 282 |
+
ap = argparse.ArgumentParser()
|
| 283 |
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 284 |
+
p = sub.add_parser("probe")
|
| 285 |
+
p.add_argument("--model", default=MODEL_ID)
|
| 286 |
+
e = sub.add_parser("extract")
|
| 287 |
+
e.add_argument("--model", default=MODEL_ID)
|
| 288 |
+
e.add_argument("--manifest", required=True)
|
| 289 |
+
e.add_argument("--out", required=True)
|
| 290 |
+
e.add_argument("--seed-mode", default="pad",
|
| 291 |
+
help="pad, alias or random, or a comma-separated list of them; a list writes <out>_<mode> per mode")
|
| 292 |
+
e.add_argument("--canvas", type=int, default=64)
|
| 293 |
+
e.add_argument("--limit", type=int, default=None)
|
| 294 |
+
e.add_argument("--shuffle-seed", type=int, default=None, help="shuffle choice options (training augmentation)")
|
| 295 |
+
e.add_argument("--load-in-4bit", action="store_true")
|
| 296 |
+
e.add_argument("--zero-shot-log", action="store_true", help=f"append each cache's zero-shot accuracy to {ZERO_SHOT_LOG}")
|
| 297 |
+
for sp in (p, e):
|
| 298 |
+
sp.add_argument("--layers", default=None,
|
| 299 |
+
help="depth fractions of extra encoder/decoder layers to append to the features, e.g. 0.5,0.75 "
|
| 300 |
+
"(feature dim becomes hidden_size * (1 + count)); probe records their module names")
|
| 301 |
+
z = sub.add_parser("zero-shot")
|
| 302 |
+
z.add_argument("--cache", required=True)
|
| 303 |
+
args = ap.parse_args()
|
| 304 |
+
fracs = tuple(float(x) for x in args.layers.split(",")) if getattr(args, "layers", None) else ()
|
| 305 |
+
if args.cmd == "zero-shot":
|
| 306 |
+
print(json.dumps({"cache": args.cache, "zero_shot_accuracy": zero_shot_accuracy(args.cache)}))
|
| 307 |
+
return
|
| 308 |
+
if args.cmd == "extract":
|
| 309 |
+
modes = args.seed_mode.split(",")
|
| 310 |
+
if any(m not in SEED_MODES for m in modes) or len(set(modes)) != len(modes):
|
| 311 |
+
e.error(f"--seed-mode takes distinct values from {', '.join(SEED_MODES)}; got {args.seed_mode!r}")
|
| 312 |
+
model, tok = load_backbone(args.model, getattr(args, "load_in_4bit", False))
|
| 313 |
+
hidden = model.config.text_config.hidden_size
|
| 314 |
+
if args.cmd == "probe":
|
| 315 |
+
info = probe(model, tok, hidden)
|
| 316 |
+
if fracs:
|
| 317 |
+
info["extra_layers"] = resolve_extra_layers(model, info["encoder_norm"], info["decoder_norm"], fracs)
|
| 318 |
+
info["feature_dim"] = hidden * (1 + len(info["extra_layers"]["encoder"]))
|
| 319 |
+
Path("configs").mkdir(exist_ok=True)
|
| 320 |
+
Path("configs/backbone.json").write_text(json.dumps({**info, "model_id": args.model}, indent=2))
|
| 321 |
+
print(json.dumps(info, indent=2))
|
| 322 |
+
return
|
| 323 |
+
alias = alias_token_ids(tok, 64)
|
| 324 |
+
vocab = getattr(model.config.text_config, "vocab_size", 262144)
|
| 325 |
+
examples = read_manifest(args.manifest)
|
| 326 |
+
info = probe(model, tok, hidden) if len(modes) > 1 else None # one probe shared by every mode
|
| 327 |
+
for mode in modes:
|
| 328 |
+
out = f"{args.out}_{mode}" if len(modes) > 1 else args.out
|
| 329 |
+
# a fresh rng per mode, so each cache matches what a separate run of that mode would write
|
| 330 |
+
rng = random.Random(args.shuffle_seed) if args.shuffle_seed is not None else None
|
| 331 |
+
summary = extract(model, tok, examples, out, seed_mode=mode, canvas_length=args.canvas, alias_ids=alias,
|
| 332 |
+
hidden_size=hidden, shuffle_rng=rng, limit=args.limit, vocab_size=vocab, info=info,
|
| 333 |
+
layer_fracs=fracs)
|
| 334 |
+
print(json.dumps(summary, indent=2))
|
| 335 |
+
acc = zero_shot_accuracy(out)
|
| 336 |
+
print(f"zero_shot {mode} {acc:.4f}", flush=True)
|
| 337 |
+
if args.zero_shot_log:
|
| 338 |
+
log = Path(ZERO_SHOT_LOG)
|
| 339 |
+
log.parent.mkdir(parents=True, exist_ok=True)
|
| 340 |
+
with log.open("a", encoding="utf-8") as f:
|
| 341 |
+
f.write(json.dumps({"cache": out, "seed_mode": mode, "zero_shot_accuracy": acc}) + "\n")
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
if __name__ == "__main__":
|
| 345 |
+
main()
|
head.py
CHANGED
|
@@ -1,119 +1,119 @@
|
|
| 1 |
-
"""Hev decision head: projection, Hydra stack, pointer readout over option spans, backbone prior, temperatures."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import math
|
| 5 |
-
from dataclasses import asdict, dataclass
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
from torch import nn
|
| 9 |
-
|
| 10 |
-
from .cache import Batch
|
| 11 |
-
from .hydra_torch import HydraBlock, RMSNormGated
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
@dataclass
|
| 15 |
-
class HevHeadConfig:
|
| 16 |
-
d_backbone: int = 2816
|
| 17 |
-
d_model: int = 512
|
| 18 |
-
n_blocks: int = 3
|
| 19 |
-
d_state: int = 64
|
| 20 |
-
expand: int = 2
|
| 21 |
-
headdim: int = 64
|
| 22 |
-
d_conv: int = 7
|
| 23 |
-
mlp_mult: int = 2
|
| 24 |
-
chunk_size: int = 64
|
| 25 |
-
n_types: int = 3
|
| 26 |
-
|
| 27 |
-
def to_dict(self) -> dict:
|
| 28 |
-
return asdict(self)
|
| 29 |
-
|
| 30 |
-
@classmethod
|
| 31 |
-
def from_dict(cls, d: dict) -> "HevHeadConfig":
|
| 32 |
-
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
class HevHead(nn.Module):
|
| 36 |
-
def __init__(self, cfg: HevHeadConfig):
|
| 37 |
-
super().__init__()
|
| 38 |
-
self.cfg = cfg
|
| 39 |
-
self.in_proj = nn.Linear(cfg.d_backbone, cfg.d_model)
|
| 40 |
-
self.seg_emb = nn.Embedding(2, cfg.d_model)
|
| 41 |
-
self.in_norm = RMSNormGated(cfg.d_model)
|
| 42 |
-
self.blocks = nn.ModuleList([
|
| 43 |
-
HydraBlock(cfg.d_model, mlp_mult=cfg.mlp_mult, d_state=cfg.d_state, d_conv=cfg.d_conv,
|
| 44 |
-
expand=cfg.expand, headdim=cfg.headdim, chunk_size=cfg.chunk_size)
|
| 45 |
-
for _ in range(cfg.n_blocks)
|
| 46 |
-
])
|
| 47 |
-
self.final_norm = RMSNormGated(cfg.d_model)
|
| 48 |
-
self.w_q = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 49 |
-
self.w_o = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 50 |
-
nn.init.normal_(self.w_q.weight, std=0.02)
|
| 51 |
-
nn.init.zeros_(self.w_o.weight) # pointer term is exactly 0 at init, so a fresh head starts at the backbone prior
|
| 52 |
-
self.beta = nn.Parameter(torch.ones(cfg.n_types))
|
| 53 |
-
self.log_temp = nn.Parameter(torch.zeros(cfg.n_types))
|
| 54 |
-
|
| 55 |
-
def num_parameters(self) -> int:
|
| 56 |
-
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 57 |
-
|
| 58 |
-
def encode(self, feats: torch.Tensor, seg: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 59 |
-
h = self.in_proj(feats.float()) + self.seg_emb(seg)
|
| 60 |
-
h = self.in_norm(h) * mask[..., None].to(h.dtype)
|
| 61 |
-
for blk in self.blocks:
|
| 62 |
-
h = blk(h, mask)
|
| 63 |
-
return self.final_norm(h)
|
| 64 |
-
|
| 65 |
-
def forward_flat(self, batch: Batch, use_temperature: bool = False) -> torch.Tensor:
|
| 66 |
-
"""Logits for every option of every decision in the batch as one flat tensor, in the batch's option order.
|
| 67 |
-
|
| 68 |
-
This is the whole readout in a fixed number of tensor ops: a gather of the span rows, one index_add for
|
| 69 |
-
the span means, one gather for the slot queries, and elementwise arithmetic on the flat option axis.
|
| 70 |
-
The per-decision loops it replaced issued ~360 ops per decision and left a GPU idle (session 3).
|
| 71 |
-
"""
|
| 72 |
-
h = self.encode(batch.feats, batch.seg, batch.mask)
|
| 73 |
-
b, l, d = h.shape
|
| 74 |
-
if bool((batch.span_len <= 0).any()): # one host sync per batch; collate already rejects empty spans
|
| 75 |
-
bad = int((batch.span_len <= 0).nonzero()[0])
|
| 76 |
-
raise ValueError(f"empty span for decision {batch.decisions[int(batch.opt_dec[bad])]['qid']!r}")
|
| 77 |
-
hf = h.reshape(b * l, d).float()
|
| 78 |
-
sums = hf.new_zeros(batch.span_len.shape[0], d).index_add(0, batch.span_opt, hf.index_select(0, batch.span_rows))
|
| 79 |
-
reps = sums / batch.span_len[:, None]
|
| 80 |
-
q = self.w_q(h[batch.dec_b, batch.dec_slot].float()) # (n_dec, d_model)
|
| 81 |
-
scale = 1.0 / math.sqrt(self.cfg.d_model)
|
| 82 |
-
scores = (self.w_o(reps) * q[batch.opt_dec]).sum(-1).float() * scale
|
| 83 |
-
logits = scores + self.beta[batch.dec_type][batch.opt_dec] * batch.opt_prior
|
| 84 |
-
if use_temperature:
|
| 85 |
-
logits = logits / torch.exp(self.log_temp)[batch.dec_type][batch.opt_dec]
|
| 86 |
-
return logits
|
| 87 |
-
|
| 88 |
-
def forward(self, batch: Batch, use_temperature: bool = False) -> list[torch.Tensor]:
|
| 89 |
-
"""One logit vector per decision: views into forward_flat's output."""
|
| 90 |
-
return list(torch.split(self.forward_flat(batch, use_temperature), batch.counts))
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
class HevEnsemble(nn.Module):
|
| 94 |
-
"""One or more HevHeads with the same config, averaged in probability space.
|
| 95 |
-
|
| 96 |
-
forward() returns, per decision, the log of the mean of the members' softmaxes, so every consumer that
|
| 97 |
-
applies softmax to the head's output (evaluate.predict, HevForDecisions.decide) gets exactly the averaged
|
| 98 |
-
distribution. Members keep their own fitted temperatures; the ensemble is not re-calibrated."""
|
| 99 |
-
|
| 100 |
-
def __init__(self, members: list[HevHead]):
|
| 101 |
-
super().__init__()
|
| 102 |
-
if not members:
|
| 103 |
-
raise ValueError("an ensemble needs at least one member")
|
| 104 |
-
cfg = members[0].cfg
|
| 105 |
-
if any(m.cfg != cfg for m in members):
|
| 106 |
-
raise ValueError("every ensemble member must share one HevHeadConfig")
|
| 107 |
-
self.members = nn.ModuleList(members)
|
| 108 |
-
self.cfg = cfg
|
| 109 |
-
|
| 110 |
-
def num_parameters(self) -> int:
|
| 111 |
-
return sum(m.num_parameters() for m in self.members)
|
| 112 |
-
|
| 113 |
-
def forward(self, batch: Batch, use_temperature: bool = False) -> list[torch.Tensor]:
|
| 114 |
-
outs = [m(batch, use_temperature=use_temperature) for m in self.members]
|
| 115 |
-
result = []
|
| 116 |
-
for i in range(len(outs[0])):
|
| 117 |
-
probs = torch.stack([torch.softmax(o[i].float(), 0) for o in outs]).mean(0)
|
| 118 |
-
result.append(torch.log(probs.clamp_min(1e-12)))
|
| 119 |
-
return result
|
|
|
|
| 1 |
+
"""Hev decision head: projection, Hydra stack, pointer readout over option spans, backbone prior, temperatures."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import asdict, dataclass
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from .cache import Batch
|
| 11 |
+
from .hydra_torch import HydraBlock, RMSNormGated
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class HevHeadConfig:
|
| 16 |
+
d_backbone: int = 2816
|
| 17 |
+
d_model: int = 512
|
| 18 |
+
n_blocks: int = 3
|
| 19 |
+
d_state: int = 64
|
| 20 |
+
expand: int = 2
|
| 21 |
+
headdim: int = 64
|
| 22 |
+
d_conv: int = 7
|
| 23 |
+
mlp_mult: int = 2
|
| 24 |
+
chunk_size: int = 64
|
| 25 |
+
n_types: int = 3
|
| 26 |
+
|
| 27 |
+
def to_dict(self) -> dict:
|
| 28 |
+
return asdict(self)
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def from_dict(cls, d: dict) -> "HevHeadConfig":
|
| 32 |
+
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class HevHead(nn.Module):
|
| 36 |
+
def __init__(self, cfg: HevHeadConfig):
|
| 37 |
+
super().__init__()
|
| 38 |
+
self.cfg = cfg
|
| 39 |
+
self.in_proj = nn.Linear(cfg.d_backbone, cfg.d_model)
|
| 40 |
+
self.seg_emb = nn.Embedding(2, cfg.d_model)
|
| 41 |
+
self.in_norm = RMSNormGated(cfg.d_model)
|
| 42 |
+
self.blocks = nn.ModuleList([
|
| 43 |
+
HydraBlock(cfg.d_model, mlp_mult=cfg.mlp_mult, d_state=cfg.d_state, d_conv=cfg.d_conv,
|
| 44 |
+
expand=cfg.expand, headdim=cfg.headdim, chunk_size=cfg.chunk_size)
|
| 45 |
+
for _ in range(cfg.n_blocks)
|
| 46 |
+
])
|
| 47 |
+
self.final_norm = RMSNormGated(cfg.d_model)
|
| 48 |
+
self.w_q = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 49 |
+
self.w_o = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 50 |
+
nn.init.normal_(self.w_q.weight, std=0.02)
|
| 51 |
+
nn.init.zeros_(self.w_o.weight) # pointer term is exactly 0 at init, so a fresh head starts at the backbone prior
|
| 52 |
+
self.beta = nn.Parameter(torch.ones(cfg.n_types))
|
| 53 |
+
self.log_temp = nn.Parameter(torch.zeros(cfg.n_types))
|
| 54 |
+
|
| 55 |
+
def num_parameters(self) -> int:
|
| 56 |
+
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 57 |
+
|
| 58 |
+
def encode(self, feats: torch.Tensor, seg: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 59 |
+
h = self.in_proj(feats.float()) + self.seg_emb(seg)
|
| 60 |
+
h = self.in_norm(h) * mask[..., None].to(h.dtype)
|
| 61 |
+
for blk in self.blocks:
|
| 62 |
+
h = blk(h, mask)
|
| 63 |
+
return self.final_norm(h)
|
| 64 |
+
|
| 65 |
+
def forward_flat(self, batch: Batch, use_temperature: bool = False) -> torch.Tensor:
|
| 66 |
+
"""Logits for every option of every decision in the batch as one flat tensor, in the batch's option order.
|
| 67 |
+
|
| 68 |
+
This is the whole readout in a fixed number of tensor ops: a gather of the span rows, one index_add for
|
| 69 |
+
the span means, one gather for the slot queries, and elementwise arithmetic on the flat option axis.
|
| 70 |
+
The per-decision loops it replaced issued ~360 ops per decision and left a GPU idle (session 3).
|
| 71 |
+
"""
|
| 72 |
+
h = self.encode(batch.feats, batch.seg, batch.mask)
|
| 73 |
+
b, l, d = h.shape
|
| 74 |
+
if bool((batch.span_len <= 0).any()): # one host sync per batch; collate already rejects empty spans
|
| 75 |
+
bad = int((batch.span_len <= 0).nonzero()[0])
|
| 76 |
+
raise ValueError(f"empty span for decision {batch.decisions[int(batch.opt_dec[bad])]['qid']!r}")
|
| 77 |
+
hf = h.reshape(b * l, d).float()
|
| 78 |
+
sums = hf.new_zeros(batch.span_len.shape[0], d).index_add(0, batch.span_opt, hf.index_select(0, batch.span_rows))
|
| 79 |
+
reps = sums / batch.span_len[:, None]
|
| 80 |
+
q = self.w_q(h[batch.dec_b, batch.dec_slot].float()) # (n_dec, d_model)
|
| 81 |
+
scale = 1.0 / math.sqrt(self.cfg.d_model)
|
| 82 |
+
scores = (self.w_o(reps) * q[batch.opt_dec]).sum(-1).float() * scale
|
| 83 |
+
logits = scores + self.beta[batch.dec_type][batch.opt_dec] * batch.opt_prior
|
| 84 |
+
if use_temperature:
|
| 85 |
+
logits = logits / torch.exp(self.log_temp)[batch.dec_type][batch.opt_dec]
|
| 86 |
+
return logits
|
| 87 |
+
|
| 88 |
+
def forward(self, batch: Batch, use_temperature: bool = False) -> list[torch.Tensor]:
|
| 89 |
+
"""One logit vector per decision: views into forward_flat's output."""
|
| 90 |
+
return list(torch.split(self.forward_flat(batch, use_temperature), batch.counts))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class HevEnsemble(nn.Module):
|
| 94 |
+
"""One or more HevHeads with the same config, averaged in probability space.
|
| 95 |
+
|
| 96 |
+
forward() returns, per decision, the log of the mean of the members' softmaxes, so every consumer that
|
| 97 |
+
applies softmax to the head's output (evaluate.predict, HevForDecisions.decide) gets exactly the averaged
|
| 98 |
+
distribution. Members keep their own fitted temperatures; the ensemble is not re-calibrated."""
|
| 99 |
+
|
| 100 |
+
def __init__(self, members: list[HevHead]):
|
| 101 |
+
super().__init__()
|
| 102 |
+
if not members:
|
| 103 |
+
raise ValueError("an ensemble needs at least one member")
|
| 104 |
+
cfg = members[0].cfg
|
| 105 |
+
if any(m.cfg != cfg for m in members):
|
| 106 |
+
raise ValueError("every ensemble member must share one HevHeadConfig")
|
| 107 |
+
self.members = nn.ModuleList(members)
|
| 108 |
+
self.cfg = cfg
|
| 109 |
+
|
| 110 |
+
def num_parameters(self) -> int:
|
| 111 |
+
return sum(m.num_parameters() for m in self.members)
|
| 112 |
+
|
| 113 |
+
def forward(self, batch: Batch, use_temperature: bool = False) -> list[torch.Tensor]:
|
| 114 |
+
outs = [m(batch, use_temperature=use_temperature) for m in self.members]
|
| 115 |
+
result = []
|
| 116 |
+
for i in range(len(outs[0])):
|
| 117 |
+
probs = torch.stack([torch.softmax(o[i].float(), 0) for o in outs]).mean(0)
|
| 118 |
+
result.append(torch.log(probs.clamp_min(1e-12)))
|
| 119 |
+
return result
|
hydra_torch.py
CHANGED
|
@@ -1,156 +1,156 @@
|
|
| 1 |
-
"""Pure-PyTorch Hydra (Hwang, Lahoti, Dao, Gu 2024): a quasiseparable bidirectional SSM mixer.
|
| 2 |
-
|
| 3 |
-
Mirrors goombalab/hydra's HydraMixer parameter layout exactly (same names and shapes), so weights are
|
| 4 |
-
interchangeable with the CUDA implementation. The SSD scan is the chunked algorithm from the Mamba-2
|
| 5 |
-
paper (Listing 1), padded to a chunk multiple. No mamba_ssm, no einops, no CUDA kernels.
|
| 6 |
-
"""
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import math
|
| 10 |
-
|
| 11 |
-
import torch
|
| 12 |
-
import torch.nn.functional as F
|
| 13 |
-
from torch import nn
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def segsum(x: torch.Tensor) -> torch.Tensor:
|
| 17 |
-
"""out[..., i, j] = sum_{k=j+1..i} x[..., k] for i >= j, else -inf."""
|
| 18 |
-
T = x.shape[-1]
|
| 19 |
-
x = x.unsqueeze(-1).expand(*x.shape, T)
|
| 20 |
-
lower = torch.tril(torch.ones(T, T, dtype=torch.bool, device=x.device), diagonal=-1)
|
| 21 |
-
x = x.masked_fill(~lower, 0)
|
| 22 |
-
x_segsum = torch.cumsum(x, dim=-2)
|
| 23 |
-
keep = torch.tril(torch.ones(T, T, dtype=torch.bool, device=x.device), diagonal=0)
|
| 24 |
-
return x_segsum.masked_fill(~keep, float("-inf"))
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def ssd_scan(X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, chunk: int = 64) -> torch.Tensor:
|
| 28 |
-
b, l, h, p = X.shape
|
| 29 |
-
pad = (-l) % chunk
|
| 30 |
-
if pad:
|
| 31 |
-
X = F.pad(X, (0, 0, 0, 0, 0, pad))
|
| 32 |
-
A = F.pad(A, (0, 0, 0, pad))
|
| 33 |
-
B = F.pad(B, (0, 0, 0, 0, 0, pad))
|
| 34 |
-
C = F.pad(C, (0, 0, 0, 0, 0, pad))
|
| 35 |
-
L = X.shape[1]
|
| 36 |
-
c = L // chunk
|
| 37 |
-
X = X.reshape(b, c, chunk, h, p)
|
| 38 |
-
B = B.reshape(b, c, chunk, h, -1)
|
| 39 |
-
C = C.reshape(b, c, chunk, h, -1)
|
| 40 |
-
A = A.reshape(b, c, chunk, h).permute(0, 3, 1, 2) # (b, h, c, l)
|
| 41 |
-
A_cumsum = torch.cumsum(A, dim=-1)
|
| 42 |
-
Lmat = torch.exp(segsum(A)) # (b, h, c, l, s)
|
| 43 |
-
Y_diag = torch.einsum("bclhn,bcshn,bhcls,bcshp->bclhp", C, B, Lmat, X)
|
| 44 |
-
decay_states = torch.exp(A_cumsum[:, :, :, -1:] - A_cumsum)
|
| 45 |
-
states = torch.einsum("bclhn,bhcl,bclhp->bchpn", B, decay_states, X)
|
| 46 |
-
states = torch.cat([torch.zeros_like(states[:, :1]), states], dim=1)
|
| 47 |
-
decay_chunk = torch.exp(segsum(F.pad(A_cumsum[:, :, :, -1], (1, 0))))
|
| 48 |
-
new_states = torch.einsum("bhzc,bchpn->bzhpn", decay_chunk, states)
|
| 49 |
-
states = new_states[:, :-1]
|
| 50 |
-
Y_off = torch.einsum("bclhn,bchpn,bhcl->bclhp", C, states, torch.exp(A_cumsum))
|
| 51 |
-
Y = (Y_diag + Y_off).reshape(b, L, h, p)
|
| 52 |
-
return Y[:, :l]
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def ssd_naive(X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor) -> torch.Tensor:
|
| 56 |
-
b, l, h, p = X.shape
|
| 57 |
-
n = B.shape[-1]
|
| 58 |
-
state = X.new_zeros(b, h, p, n)
|
| 59 |
-
ys = []
|
| 60 |
-
for t in range(l):
|
| 61 |
-
state = state * torch.exp(A[:, t])[:, :, None, None] + X[:, t][..., None] * B[:, t][:, :, None, :]
|
| 62 |
-
ys.append(torch.einsum("bhpn,bhn->bhp", state, C[:, t]))
|
| 63 |
-
return torch.stack(ys, dim=1)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
class RMSNormGated(nn.Module):
|
| 67 |
-
"""RMSNorm followed by an optional SiLU gate (norm_before_gate=True in mamba_ssm)."""
|
| 68 |
-
|
| 69 |
-
def __init__(self, d: int, eps: float = 1e-5):
|
| 70 |
-
super().__init__()
|
| 71 |
-
self.eps = eps
|
| 72 |
-
self.weight = nn.Parameter(torch.ones(d))
|
| 73 |
-
|
| 74 |
-
def forward(self, x: torch.Tensor, z: torch.Tensor | None = None) -> torch.Tensor:
|
| 75 |
-
xf = x.float()
|
| 76 |
-
y = xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight.float()
|
| 77 |
-
if z is not None:
|
| 78 |
-
y = y * F.silu(z.float())
|
| 79 |
-
return y.to(x.dtype)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
class HydraMixer(nn.Module):
|
| 83 |
-
def __init__(self, d_model: int, d_state: int = 64, d_conv: int = 7, expand: int = 2, headdim: int = 64,
|
| 84 |
-
ngroups: int = 1, chunk_size: int = 64, dt_min: float = 0.001, dt_max: float = 0.1,
|
| 85 |
-
dt_init_floor: float = 1e-4):
|
| 86 |
-
super().__init__()
|
| 87 |
-
self.d_model, self.d_state, self.headdim, self.ngroups, self.chunk_size = d_model, d_state, headdim, ngroups, chunk_size
|
| 88 |
-
self.d_inner = expand * d_model
|
| 89 |
-
if self.d_inner % headdim != 0:
|
| 90 |
-
raise ValueError(f"d_inner (expand * d_model = {self.d_inner}) must be divisible by headdim ({headdim})")
|
| 91 |
-
self.nheads = self.d_inner // headdim
|
| 92 |
-
if self.nheads % ngroups != 0:
|
| 93 |
-
raise ValueError(f"nheads (d_inner / headdim = {self.nheads}) must be divisible by ngroups ({ngroups})")
|
| 94 |
-
if d_conv % 2 != 1:
|
| 95 |
-
raise ValueError(f"d_conv must be odd so the non-causal conv keeps the sequence length, got {d_conv}")
|
| 96 |
-
d_in_proj = 2 * self.d_inner + 2 * (2 * ngroups * d_state) + 2 * self.nheads
|
| 97 |
-
self.in_proj = nn.Linear(d_model, d_in_proj, bias=False)
|
| 98 |
-
conv_dim = self.d_inner + 2 * (2 * ngroups * d_state)
|
| 99 |
-
self.conv1d = nn.Conv1d(conv_dim, conv_dim, kernel_size=d_conv, groups=conv_dim, padding=d_conv // 2, bias=True)
|
| 100 |
-
dt = torch.exp(torch.rand(self.nheads) * (math.log(dt_max) - math.log(dt_min)) + math.log(dt_min))
|
| 101 |
-
dt = torch.clamp(dt, min=dt_init_floor)
|
| 102 |
-
self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
|
| 103 |
-
self.dt_bias._no_weight_decay = True
|
| 104 |
-
self.A_log = nn.Parameter(torch.zeros(self.nheads)) # A = -exp(0) = -1, as in the official init
|
| 105 |
-
self.A_log._no_weight_decay = True
|
| 106 |
-
self.D = nn.Parameter(torch.ones(self.nheads))
|
| 107 |
-
self.D._no_weight_decay = True
|
| 108 |
-
self.fc_D = nn.Linear(self.d_inner, self.nheads, bias=False)
|
| 109 |
-
self.norm = RMSNormGated(self.d_inner, eps=1e-5)
|
| 110 |
-
self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
|
| 111 |
-
|
| 112 |
-
def forward(self, u: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 113 |
-
b, l, _ = u.shape
|
| 114 |
-
H, P, G, N = self.nheads, self.headdim, self.ngroups, self.d_state
|
| 115 |
-
if mask is not None:
|
| 116 |
-
u = u * mask[..., None].to(u.dtype)
|
| 117 |
-
z, xBC, dt = torch.split(self.in_proj(u), [self.d_inner, self.d_inner + 4 * G * N, 2 * H], dim=-1)
|
| 118 |
-
xBC = F.silu(self.conv1d(xBC.transpose(1, 2)).transpose(1, 2))
|
| 119 |
-
dt2 = F.softplus(torch.cat([dt[..., :H], dt[..., H:].flip(1)], dim=0) + self.dt_bias) # (2b, l, H)
|
| 120 |
-
if mask is not None:
|
| 121 |
-
m = mask[..., None].to(u.dtype)
|
| 122 |
-
xBC = xBC * m
|
| 123 |
-
dt2 = dt2 * torch.cat([m, m.flip(1)], dim=0)
|
| 124 |
-
x, BC = torch.split(xBC, [self.d_inner, 4 * G * N], dim=-1)
|
| 125 |
-
x_og = x
|
| 126 |
-
x2 = torch.cat([x, x.flip(1)], dim=0)
|
| 127 |
-
BC2 = torch.cat([BC[..., :2 * G * N], BC[..., 2 * G * N:].flip(1)], dim=0)
|
| 128 |
-
Bm, Cm = torch.split(BC2, [G * N, G * N], dim=-1)
|
| 129 |
-
A = -torch.exp(self.A_log.float())
|
| 130 |
-
Xh = x2.reshape(2 * b, l, H, P).float() * dt2.float()[..., None]
|
| 131 |
-
Ah = A[None, None, :] * dt2.float()
|
| 132 |
-
Bh = Bm.reshape(2 * b, l, G, N).float().repeat_interleave(H // G, dim=2)
|
| 133 |
-
Ch = Cm.reshape(2 * b, l, G, N).float().repeat_interleave(H // G, dim=2)
|
| 134 |
-
y = ssd_scan(Xh, Ah, Bh, Ch, self.chunk_size).reshape(2 * b, l, self.d_inner).to(u.dtype)
|
| 135 |
-
y = torch.cat([torch.zeros_like(y[:, :1]), y[:, :-1]], dim=1) # shift by one, first position zero
|
| 136 |
-
y_fw, y_bw = y[:b], y[b:].flip(1)
|
| 137 |
-
d_term = F.linear(x_og, self.fc_D.weight, self.D).repeat_interleave(P, dim=-1)
|
| 138 |
-
y = y_fw + y_bw + x_og * d_term
|
| 139 |
-
y = self.norm(y, z)
|
| 140 |
-
return self.out_proj(y)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
class HydraBlock(nn.Module):
|
| 144 |
-
def __init__(self, d_model: int, mlp_mult: int = 2, **mixer_kwargs):
|
| 145 |
-
super().__init__()
|
| 146 |
-
self.norm1 = RMSNormGated(d_model)
|
| 147 |
-
self.mixer = HydraMixer(d_model, **mixer_kwargs)
|
| 148 |
-
self.norm2 = RMSNormGated(d_model)
|
| 149 |
-
hidden = mlp_mult * d_model
|
| 150 |
-
self.fc1 = nn.Linear(d_model, 2 * hidden)
|
| 151 |
-
self.fc2 = nn.Linear(hidden, d_model)
|
| 152 |
-
|
| 153 |
-
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 154 |
-
x = x + self.mixer(self.norm1(x), mask)
|
| 155 |
-
h, g = self.fc1(self.norm2(x)).chunk(2, dim=-1)
|
| 156 |
-
return x + self.fc2(F.silu(g) * h)
|
|
|
|
| 1 |
+
"""Pure-PyTorch Hydra (Hwang, Lahoti, Dao, Gu 2024): a quasiseparable bidirectional SSM mixer.
|
| 2 |
+
|
| 3 |
+
Mirrors goombalab/hydra's HydraMixer parameter layout exactly (same names and shapes), so weights are
|
| 4 |
+
interchangeable with the CUDA implementation. The SSD scan is the chunked algorithm from the Mamba-2
|
| 5 |
+
paper (Listing 1), padded to a chunk multiple. No mamba_ssm, no einops, no CUDA kernels.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import math
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def segsum(x: torch.Tensor) -> torch.Tensor:
|
| 17 |
+
"""out[..., i, j] = sum_{k=j+1..i} x[..., k] for i >= j, else -inf."""
|
| 18 |
+
T = x.shape[-1]
|
| 19 |
+
x = x.unsqueeze(-1).expand(*x.shape, T)
|
| 20 |
+
lower = torch.tril(torch.ones(T, T, dtype=torch.bool, device=x.device), diagonal=-1)
|
| 21 |
+
x = x.masked_fill(~lower, 0)
|
| 22 |
+
x_segsum = torch.cumsum(x, dim=-2)
|
| 23 |
+
keep = torch.tril(torch.ones(T, T, dtype=torch.bool, device=x.device), diagonal=0)
|
| 24 |
+
return x_segsum.masked_fill(~keep, float("-inf"))
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def ssd_scan(X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, chunk: int = 64) -> torch.Tensor:
|
| 28 |
+
b, l, h, p = X.shape
|
| 29 |
+
pad = (-l) % chunk
|
| 30 |
+
if pad:
|
| 31 |
+
X = F.pad(X, (0, 0, 0, 0, 0, pad))
|
| 32 |
+
A = F.pad(A, (0, 0, 0, pad))
|
| 33 |
+
B = F.pad(B, (0, 0, 0, 0, 0, pad))
|
| 34 |
+
C = F.pad(C, (0, 0, 0, 0, 0, pad))
|
| 35 |
+
L = X.shape[1]
|
| 36 |
+
c = L // chunk
|
| 37 |
+
X = X.reshape(b, c, chunk, h, p)
|
| 38 |
+
B = B.reshape(b, c, chunk, h, -1)
|
| 39 |
+
C = C.reshape(b, c, chunk, h, -1)
|
| 40 |
+
A = A.reshape(b, c, chunk, h).permute(0, 3, 1, 2) # (b, h, c, l)
|
| 41 |
+
A_cumsum = torch.cumsum(A, dim=-1)
|
| 42 |
+
Lmat = torch.exp(segsum(A)) # (b, h, c, l, s)
|
| 43 |
+
Y_diag = torch.einsum("bclhn,bcshn,bhcls,bcshp->bclhp", C, B, Lmat, X)
|
| 44 |
+
decay_states = torch.exp(A_cumsum[:, :, :, -1:] - A_cumsum)
|
| 45 |
+
states = torch.einsum("bclhn,bhcl,bclhp->bchpn", B, decay_states, X)
|
| 46 |
+
states = torch.cat([torch.zeros_like(states[:, :1]), states], dim=1)
|
| 47 |
+
decay_chunk = torch.exp(segsum(F.pad(A_cumsum[:, :, :, -1], (1, 0))))
|
| 48 |
+
new_states = torch.einsum("bhzc,bchpn->bzhpn", decay_chunk, states)
|
| 49 |
+
states = new_states[:, :-1]
|
| 50 |
+
Y_off = torch.einsum("bclhn,bchpn,bhcl->bclhp", C, states, torch.exp(A_cumsum))
|
| 51 |
+
Y = (Y_diag + Y_off).reshape(b, L, h, p)
|
| 52 |
+
return Y[:, :l]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def ssd_naive(X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor) -> torch.Tensor:
|
| 56 |
+
b, l, h, p = X.shape
|
| 57 |
+
n = B.shape[-1]
|
| 58 |
+
state = X.new_zeros(b, h, p, n)
|
| 59 |
+
ys = []
|
| 60 |
+
for t in range(l):
|
| 61 |
+
state = state * torch.exp(A[:, t])[:, :, None, None] + X[:, t][..., None] * B[:, t][:, :, None, :]
|
| 62 |
+
ys.append(torch.einsum("bhpn,bhn->bhp", state, C[:, t]))
|
| 63 |
+
return torch.stack(ys, dim=1)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class RMSNormGated(nn.Module):
|
| 67 |
+
"""RMSNorm followed by an optional SiLU gate (norm_before_gate=True in mamba_ssm)."""
|
| 68 |
+
|
| 69 |
+
def __init__(self, d: int, eps: float = 1e-5):
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.eps = eps
|
| 72 |
+
self.weight = nn.Parameter(torch.ones(d))
|
| 73 |
+
|
| 74 |
+
def forward(self, x: torch.Tensor, z: torch.Tensor | None = None) -> torch.Tensor:
|
| 75 |
+
xf = x.float()
|
| 76 |
+
y = xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight.float()
|
| 77 |
+
if z is not None:
|
| 78 |
+
y = y * F.silu(z.float())
|
| 79 |
+
return y.to(x.dtype)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class HydraMixer(nn.Module):
|
| 83 |
+
def __init__(self, d_model: int, d_state: int = 64, d_conv: int = 7, expand: int = 2, headdim: int = 64,
|
| 84 |
+
ngroups: int = 1, chunk_size: int = 64, dt_min: float = 0.001, dt_max: float = 0.1,
|
| 85 |
+
dt_init_floor: float = 1e-4):
|
| 86 |
+
super().__init__()
|
| 87 |
+
self.d_model, self.d_state, self.headdim, self.ngroups, self.chunk_size = d_model, d_state, headdim, ngroups, chunk_size
|
| 88 |
+
self.d_inner = expand * d_model
|
| 89 |
+
if self.d_inner % headdim != 0:
|
| 90 |
+
raise ValueError(f"d_inner (expand * d_model = {self.d_inner}) must be divisible by headdim ({headdim})")
|
| 91 |
+
self.nheads = self.d_inner // headdim
|
| 92 |
+
if self.nheads % ngroups != 0:
|
| 93 |
+
raise ValueError(f"nheads (d_inner / headdim = {self.nheads}) must be divisible by ngroups ({ngroups})")
|
| 94 |
+
if d_conv % 2 != 1:
|
| 95 |
+
raise ValueError(f"d_conv must be odd so the non-causal conv keeps the sequence length, got {d_conv}")
|
| 96 |
+
d_in_proj = 2 * self.d_inner + 2 * (2 * ngroups * d_state) + 2 * self.nheads
|
| 97 |
+
self.in_proj = nn.Linear(d_model, d_in_proj, bias=False)
|
| 98 |
+
conv_dim = self.d_inner + 2 * (2 * ngroups * d_state)
|
| 99 |
+
self.conv1d = nn.Conv1d(conv_dim, conv_dim, kernel_size=d_conv, groups=conv_dim, padding=d_conv // 2, bias=True)
|
| 100 |
+
dt = torch.exp(torch.rand(self.nheads) * (math.log(dt_max) - math.log(dt_min)) + math.log(dt_min))
|
| 101 |
+
dt = torch.clamp(dt, min=dt_init_floor)
|
| 102 |
+
self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
|
| 103 |
+
self.dt_bias._no_weight_decay = True
|
| 104 |
+
self.A_log = nn.Parameter(torch.zeros(self.nheads)) # A = -exp(0) = -1, as in the official init
|
| 105 |
+
self.A_log._no_weight_decay = True
|
| 106 |
+
self.D = nn.Parameter(torch.ones(self.nheads))
|
| 107 |
+
self.D._no_weight_decay = True
|
| 108 |
+
self.fc_D = nn.Linear(self.d_inner, self.nheads, bias=False)
|
| 109 |
+
self.norm = RMSNormGated(self.d_inner, eps=1e-5)
|
| 110 |
+
self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
|
| 111 |
+
|
| 112 |
+
def forward(self, u: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 113 |
+
b, l, _ = u.shape
|
| 114 |
+
H, P, G, N = self.nheads, self.headdim, self.ngroups, self.d_state
|
| 115 |
+
if mask is not None:
|
| 116 |
+
u = u * mask[..., None].to(u.dtype)
|
| 117 |
+
z, xBC, dt = torch.split(self.in_proj(u), [self.d_inner, self.d_inner + 4 * G * N, 2 * H], dim=-1)
|
| 118 |
+
xBC = F.silu(self.conv1d(xBC.transpose(1, 2)).transpose(1, 2))
|
| 119 |
+
dt2 = F.softplus(torch.cat([dt[..., :H], dt[..., H:].flip(1)], dim=0) + self.dt_bias) # (2b, l, H)
|
| 120 |
+
if mask is not None:
|
| 121 |
+
m = mask[..., None].to(u.dtype)
|
| 122 |
+
xBC = xBC * m
|
| 123 |
+
dt2 = dt2 * torch.cat([m, m.flip(1)], dim=0)
|
| 124 |
+
x, BC = torch.split(xBC, [self.d_inner, 4 * G * N], dim=-1)
|
| 125 |
+
x_og = x
|
| 126 |
+
x2 = torch.cat([x, x.flip(1)], dim=0)
|
| 127 |
+
BC2 = torch.cat([BC[..., :2 * G * N], BC[..., 2 * G * N:].flip(1)], dim=0)
|
| 128 |
+
Bm, Cm = torch.split(BC2, [G * N, G * N], dim=-1)
|
| 129 |
+
A = -torch.exp(self.A_log.float())
|
| 130 |
+
Xh = x2.reshape(2 * b, l, H, P).float() * dt2.float()[..., None]
|
| 131 |
+
Ah = A[None, None, :] * dt2.float()
|
| 132 |
+
Bh = Bm.reshape(2 * b, l, G, N).float().repeat_interleave(H // G, dim=2)
|
| 133 |
+
Ch = Cm.reshape(2 * b, l, G, N).float().repeat_interleave(H // G, dim=2)
|
| 134 |
+
y = ssd_scan(Xh, Ah, Bh, Ch, self.chunk_size).reshape(2 * b, l, self.d_inner).to(u.dtype)
|
| 135 |
+
y = torch.cat([torch.zeros_like(y[:, :1]), y[:, :-1]], dim=1) # shift by one, first position zero
|
| 136 |
+
y_fw, y_bw = y[:b], y[b:].flip(1)
|
| 137 |
+
d_term = F.linear(x_og, self.fc_D.weight, self.D).repeat_interleave(P, dim=-1)
|
| 138 |
+
y = y_fw + y_bw + x_og * d_term
|
| 139 |
+
y = self.norm(y, z)
|
| 140 |
+
return self.out_proj(y)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class HydraBlock(nn.Module):
|
| 144 |
+
def __init__(self, d_model: int, mlp_mult: int = 2, **mixer_kwargs):
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.norm1 = RMSNormGated(d_model)
|
| 147 |
+
self.mixer = HydraMixer(d_model, **mixer_kwargs)
|
| 148 |
+
self.norm2 = RMSNormGated(d_model)
|
| 149 |
+
hidden = mlp_mult * d_model
|
| 150 |
+
self.fc1 = nn.Linear(d_model, 2 * hidden)
|
| 151 |
+
self.fc2 = nn.Linear(hidden, d_model)
|
| 152 |
+
|
| 153 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 154 |
+
x = x + self.mixer(self.norm1(x), mask)
|
| 155 |
+
h, g = self.fc1(self.norm2(x)).chunk(2, dim=-1)
|
| 156 |
+
return x + self.fc2(F.silu(g) * h)
|
modeling_hev.py
CHANGED
|
@@ -1,163 +1,163 @@
|
|
| 1 |
-
"""HevForDecisions: frozen DiffusionGemma backbone + Hev head, exposing Jev-shaped decide()."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import math
|
| 5 |
-
import random
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
|
| 8 |
-
import torch
|
| 9 |
-
from safetensors.torch import load_file
|
| 10 |
-
from transformers import PreTrainedModel
|
| 11 |
-
|
| 12 |
-
from .cache import collate
|
| 13 |
-
from .configuration_hev import HevConfig
|
| 14 |
-
from .features import NormCapture, _slot_token, encode_example, extra_layer_names
|
| 15 |
-
from .head import HevEnsemble, HevHead, HevHeadConfig
|
| 16 |
-
from .prompting import PROMPT_FORMAT_VERSION, alias_token_ids
|
| 17 |
-
from .schema import Example, Question
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# Hub download options that from_pretrained forwards to snapshot_download and HevConfig.from_pretrained.
|
| 21 |
-
HUB_KWARGS = ("revision", "token", "cache_dir", "local_files_only", "force_download")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def _is_local_dir(name: str) -> bool:
|
| 25 |
-
try:
|
| 26 |
-
return Path(name).is_dir()
|
| 27 |
-
except (OSError, ValueError):
|
| 28 |
-
return False
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def _vocab_size(backbone) -> int:
|
| 32 |
-
"""Backbone vocabulary size, read as hev.features.main reads it for random slot draws (default 262144)."""
|
| 33 |
-
return getattr(getattr(backbone.config, "text_config", None), "vocab_size", 262144)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def _param_device(module) -> torch.device:
|
| 37 |
-
"""Device of the module's first parameter, or CPU when it has none."""
|
| 38 |
-
params = module.parameters() if hasattr(module, "parameters") else iter(())
|
| 39 |
-
first = next(params, None)
|
| 40 |
-
return first.device if first is not None else torch.device("cpu")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
class HevForDecisions(PreTrainedModel):
|
| 44 |
-
config_class = HevConfig
|
| 45 |
-
|
| 46 |
-
def __init__(self, config: HevConfig, backbone=None, tokenizer=None):
|
| 47 |
-
super().__init__(config)
|
| 48 |
-
members = []
|
| 49 |
-
temps = list(getattr(config, "member_temperatures", None) or [config.temperatures])
|
| 50 |
-
for i in range(int(getattr(config, "n_heads", 1))):
|
| 51 |
-
head = HevHead(HevHeadConfig.from_dict(config.head_config))
|
| 52 |
-
with torch.no_grad():
|
| 53 |
-
head.log_temp.copy_(torch.log(torch.tensor(temps[i] if i < len(temps) else config.temperatures,
|
| 54 |
-
dtype=torch.float32)))
|
| 55 |
-
members.append(head)
|
| 56 |
-
self.head = HevEnsemble(members)
|
| 57 |
-
self.backbone = backbone
|
| 58 |
-
self.tokenizer = tokenizer
|
| 59 |
-
self._cap = None
|
| 60 |
-
self._alias_ids = None
|
| 61 |
-
|
| 62 |
-
@classmethod
|
| 63 |
-
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, backbone_cls=None, tokenizer_cls=None,
|
| 64 |
-
dtype=torch.bfloat16, device_map="auto", load_in_4bit: bool = False, config=None, **kwargs):
|
| 65 |
-
"""Load the Hev config, head, frozen backbone and tokenizer from a local folder or a Hub repo.
|
| 66 |
-
|
| 67 |
-
`revision`, `token`, `cache_dir`, `local_files_only` and `force_download` are forwarded to the Hub
|
| 68 |
-
download and to HevConfig.from_pretrained. A HevConfig passed as `config` (AutoModel does this) is used as is.
|
| 69 |
-
"""
|
| 70 |
-
hub = {k: kwargs[k] for k in HUB_KWARGS if kwargs.get(k) is not None}
|
| 71 |
-
name = str(pretrained_model_name_or_path)
|
| 72 |
-
path = Path(name)
|
| 73 |
-
from_hub = not path.exists()
|
| 74 |
-
if from_hub:
|
| 75 |
-
from huggingface_hub import snapshot_download
|
| 76 |
-
path = Path(snapshot_download(name, **hub))
|
| 77 |
-
if not isinstance(config, HevConfig):
|
| 78 |
-
config = HevConfig.from_pretrained(str(path), **hub)
|
| 79 |
-
if from_hub:
|
| 80 |
-
config._name_or_path = name # the repo id, not the local snapshot folder
|
| 81 |
-
if backbone_cls is None or tokenizer_cls is None:
|
| 82 |
-
from transformers import AutoConfig, AutoTokenizer, DiffusionGemmaForBlockDiffusion
|
| 83 |
-
backbone_cls = backbone_cls or DiffusionGemmaForBlockDiffusion
|
| 84 |
-
tokenizer_cls = tokenizer_cls or AutoTokenizer
|
| 85 |
-
bb_cfg = AutoConfig.for_model(**config.backbone_config)
|
| 86 |
-
else:
|
| 87 |
-
bb_cfg = None
|
| 88 |
-
bb_kwargs = {"config": bb_cfg, "dtype": dtype, "device_map": device_map}
|
| 89 |
-
if load_in_4bit:
|
| 90 |
-
from transformers import BitsAndBytesConfig
|
| 91 |
-
bb_kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=dtype)
|
| 92 |
-
backbone = backbone_cls.from_pretrained(str(path), **bb_kwargs).eval()
|
| 93 |
-
tokenizer = tokenizer_cls.from_pretrained(str(path))
|
| 94 |
-
model = cls(config, backbone=backbone, tokenizer=tokenizer)
|
| 95 |
-
if len(model.head.members) == 1 and (path / "hev_head.safetensors").exists():
|
| 96 |
-
model.head.members[0].load_state_dict(load_file(str(path / "hev_head.safetensors")))
|
| 97 |
-
else:
|
| 98 |
-
for i, member in enumerate(model.head.members):
|
| 99 |
-
member.load_state_dict(load_file(str(path / f"hev_head_{i}.safetensors")))
|
| 100 |
-
model.head.to(_param_device(backbone))
|
| 101 |
-
return model.eval()
|
| 102 |
-
|
| 103 |
-
@staticmethod
|
| 104 |
-
def confidence(probs) -> float:
|
| 105 |
-
k = len(probs)
|
| 106 |
-
if k < 2:
|
| 107 |
-
return 1.0
|
| 108 |
-
h = -sum(p * math.log(p) for p in probs if p > 0)
|
| 109 |
-
return max(0.0, min(1.0, 1.0 - h / math.log(k)))
|
| 110 |
-
|
| 111 |
-
def _extra_layers(self) -> dict:
|
| 112 |
-
extra = getattr(self.config, "extra_layers", None) or {}
|
| 113 |
-
return {"encoder": list(extra.get("encoder", [])), "decoder": list(extra.get("decoder", []))}
|
| 114 |
-
|
| 115 |
-
def _ensure_capture(self):
|
| 116 |
-
if self._cap is None:
|
| 117 |
-
self._cap = NormCapture(self.backbone, self.config.encoder_norm, self.config.decoder_norm,
|
| 118 |
-
extra=extra_layer_names({"extra_layers": self._extra_layers()}))
|
| 119 |
-
self._alias_ids = alias_token_ids(self.tokenizer, len(self.config.aliases))
|
| 120 |
-
|
| 121 |
-
@torch.no_grad()
|
| 122 |
-
def decide(self, state: str, questions: dict) -> dict:
|
| 123 |
-
"""Answer Jev-shaped questions about `state` with the backbone features and the trained head.
|
| 124 |
-
|
| 125 |
-
The answer slots are seeded exactly as hev.features.extract seeded them in training, by
|
| 126 |
-
`config.seed_mode`: the pad token (0) for "pad", the first alias token for "alias", and an average
|
| 127 |
-
over RANDOM_DRAWS forward passes with random slot tokens (fixed rng seed 1234) for "random".
|
| 128 |
-
`config.slot_token_id` is not read here; seed_mode governs.
|
| 129 |
-
"""
|
| 130 |
-
version = getattr(self.config, "prompt_format_version", 1)
|
| 131 |
-
if version != PROMPT_FORMAT_VERSION:
|
| 132 |
-
raise RuntimeError(
|
| 133 |
-
f"this head was trained with prompt format {version}, but the installed hev code builds prompt format "
|
| 134 |
-
f"{PROMPT_FORMAT_VERSION}; load the code revision the head was published with, or retrain the head")
|
| 135 |
-
self._ensure_capture()
|
| 136 |
-
qs = [Question.from_jev(qid, spec) for qid, spec in questions.items()]
|
| 137 |
-
ex = Example(id="request", source="api", state=state, questions=qs, gold={})
|
| 138 |
-
info = {"canvas_length": self.config.canvas_length, "encoder_norm": self.config.encoder_norm,
|
| 139 |
-
"decoder_norm": self.config.decoder_norm, "extra_layers": self._extra_layers()}
|
| 140 |
-
slot_tokens = _slot_token(self.config.seed_mode, self._alias_ids, random.Random(1234),
|
| 141 |
-
_vocab_size(self.backbone))
|
| 142 |
-
feats, rec = encode_example(self.backbone, self.tokenizer, ex, self._cap, info,
|
| 143 |
-
slot_tokens=slot_tokens, alias_ids=self._alias_ids,
|
| 144 |
-
max_tokens=self.config.max_prompt_tokens)
|
| 145 |
-
batch = collate([(rec, feats)]).to(next(self.head.parameters()).device)
|
| 146 |
-
logits = self.head(batch, use_temperature=True)
|
| 147 |
-
answers = {}
|
| 148 |
-
for q, lg in zip(qs, logits):
|
| 149 |
-
probs = torch.softmax(lg.float(), 0).tolist()
|
| 150 |
-
if q.type == "noul":
|
| 151 |
-
answers[q.qid] = {"type": "noul", "noul": probs[0]}
|
| 152 |
-
elif q.type == "choice":
|
| 153 |
-
best = max(range(q.k), key=lambda i: probs[i])
|
| 154 |
-
answers[q.qid] = {"type": "choice", "choice": q.options[best],
|
| 155 |
-
"probabilities": dict(zip(q.options, probs)), "confidence": self.confidence(probs)}
|
| 156 |
-
else:
|
| 157 |
-
answers[q.qid] = {"type": "score", "score": sum(i * p for i, p in enumerate(probs)),
|
| 158 |
-
"probabilities": {str(i): p for i, p in enumerate(probs)},
|
| 159 |
-
"confidence": self.confidence(probs), "legend": list(q.descriptions)}
|
| 160 |
-
name = getattr(self.config, "_name_or_path", "") or ""
|
| 161 |
-
# A local folder name would leak a filesystem path into every response, so only a Hub repo id is reported.
|
| 162 |
-
return {"model": name if name and not _is_local_dir(name) else "hev",
|
| 163 |
-
"answers": answers, "usage": {"input_tokens": rec.n_prompt, "output_tokens": 0}}
|
|
|
|
| 1 |
+
"""HevForDecisions: frozen DiffusionGemma backbone + Hev head, exposing Jev-shaped decide()."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
import random
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from safetensors.torch import load_file
|
| 10 |
+
from transformers import PreTrainedModel
|
| 11 |
+
|
| 12 |
+
from .cache import collate
|
| 13 |
+
from .configuration_hev import HevConfig
|
| 14 |
+
from .features import NormCapture, _slot_token, encode_example, extra_layer_names
|
| 15 |
+
from .head import HevEnsemble, HevHead, HevHeadConfig
|
| 16 |
+
from .prompting import PROMPT_FORMAT_VERSION, alias_token_ids
|
| 17 |
+
from .schema import Example, Question
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Hub download options that from_pretrained forwards to snapshot_download and HevConfig.from_pretrained.
|
| 21 |
+
HUB_KWARGS = ("revision", "token", "cache_dir", "local_files_only", "force_download")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _is_local_dir(name: str) -> bool:
|
| 25 |
+
try:
|
| 26 |
+
return Path(name).is_dir()
|
| 27 |
+
except (OSError, ValueError):
|
| 28 |
+
return False
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _vocab_size(backbone) -> int:
|
| 32 |
+
"""Backbone vocabulary size, read as hev.features.main reads it for random slot draws (default 262144)."""
|
| 33 |
+
return getattr(getattr(backbone.config, "text_config", None), "vocab_size", 262144)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _param_device(module) -> torch.device:
|
| 37 |
+
"""Device of the module's first parameter, or CPU when it has none."""
|
| 38 |
+
params = module.parameters() if hasattr(module, "parameters") else iter(())
|
| 39 |
+
first = next(params, None)
|
| 40 |
+
return first.device if first is not None else torch.device("cpu")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class HevForDecisions(PreTrainedModel):
|
| 44 |
+
config_class = HevConfig
|
| 45 |
+
|
| 46 |
+
def __init__(self, config: HevConfig, backbone=None, tokenizer=None):
|
| 47 |
+
super().__init__(config)
|
| 48 |
+
members = []
|
| 49 |
+
temps = list(getattr(config, "member_temperatures", None) or [config.temperatures])
|
| 50 |
+
for i in range(int(getattr(config, "n_heads", 1))):
|
| 51 |
+
head = HevHead(HevHeadConfig.from_dict(config.head_config))
|
| 52 |
+
with torch.no_grad():
|
| 53 |
+
head.log_temp.copy_(torch.log(torch.tensor(temps[i] if i < len(temps) else config.temperatures,
|
| 54 |
+
dtype=torch.float32)))
|
| 55 |
+
members.append(head)
|
| 56 |
+
self.head = HevEnsemble(members)
|
| 57 |
+
self.backbone = backbone
|
| 58 |
+
self.tokenizer = tokenizer
|
| 59 |
+
self._cap = None
|
| 60 |
+
self._alias_ids = None
|
| 61 |
+
|
| 62 |
+
@classmethod
|
| 63 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, backbone_cls=None, tokenizer_cls=None,
|
| 64 |
+
dtype=torch.bfloat16, device_map="auto", load_in_4bit: bool = False, config=None, **kwargs):
|
| 65 |
+
"""Load the Hev config, head, frozen backbone and tokenizer from a local folder or a Hub repo.
|
| 66 |
+
|
| 67 |
+
`revision`, `token`, `cache_dir`, `local_files_only` and `force_download` are forwarded to the Hub
|
| 68 |
+
download and to HevConfig.from_pretrained. A HevConfig passed as `config` (AutoModel does this) is used as is.
|
| 69 |
+
"""
|
| 70 |
+
hub = {k: kwargs[k] for k in HUB_KWARGS if kwargs.get(k) is not None}
|
| 71 |
+
name = str(pretrained_model_name_or_path)
|
| 72 |
+
path = Path(name)
|
| 73 |
+
from_hub = not path.exists()
|
| 74 |
+
if from_hub:
|
| 75 |
+
from huggingface_hub import snapshot_download
|
| 76 |
+
path = Path(snapshot_download(name, **hub))
|
| 77 |
+
if not isinstance(config, HevConfig):
|
| 78 |
+
config = HevConfig.from_pretrained(str(path), **hub)
|
| 79 |
+
if from_hub:
|
| 80 |
+
config._name_or_path = name # the repo id, not the local snapshot folder
|
| 81 |
+
if backbone_cls is None or tokenizer_cls is None:
|
| 82 |
+
from transformers import AutoConfig, AutoTokenizer, DiffusionGemmaForBlockDiffusion
|
| 83 |
+
backbone_cls = backbone_cls or DiffusionGemmaForBlockDiffusion
|
| 84 |
+
tokenizer_cls = tokenizer_cls or AutoTokenizer
|
| 85 |
+
bb_cfg = AutoConfig.for_model(**config.backbone_config)
|
| 86 |
+
else:
|
| 87 |
+
bb_cfg = None
|
| 88 |
+
bb_kwargs = {"config": bb_cfg, "dtype": dtype, "device_map": device_map}
|
| 89 |
+
if load_in_4bit:
|
| 90 |
+
from transformers import BitsAndBytesConfig
|
| 91 |
+
bb_kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=dtype)
|
| 92 |
+
backbone = backbone_cls.from_pretrained(str(path), **bb_kwargs).eval()
|
| 93 |
+
tokenizer = tokenizer_cls.from_pretrained(str(path))
|
| 94 |
+
model = cls(config, backbone=backbone, tokenizer=tokenizer)
|
| 95 |
+
if len(model.head.members) == 1 and (path / "hev_head.safetensors").exists():
|
| 96 |
+
model.head.members[0].load_state_dict(load_file(str(path / "hev_head.safetensors")))
|
| 97 |
+
else:
|
| 98 |
+
for i, member in enumerate(model.head.members):
|
| 99 |
+
member.load_state_dict(load_file(str(path / f"hev_head_{i}.safetensors")))
|
| 100 |
+
model.head.to(_param_device(backbone))
|
| 101 |
+
return model.eval()
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def confidence(probs) -> float:
|
| 105 |
+
k = len(probs)
|
| 106 |
+
if k < 2:
|
| 107 |
+
return 1.0
|
| 108 |
+
h = -sum(p * math.log(p) for p in probs if p > 0)
|
| 109 |
+
return max(0.0, min(1.0, 1.0 - h / math.log(k)))
|
| 110 |
+
|
| 111 |
+
def _extra_layers(self) -> dict:
|
| 112 |
+
extra = getattr(self.config, "extra_layers", None) or {}
|
| 113 |
+
return {"encoder": list(extra.get("encoder", [])), "decoder": list(extra.get("decoder", []))}
|
| 114 |
+
|
| 115 |
+
def _ensure_capture(self):
|
| 116 |
+
if self._cap is None:
|
| 117 |
+
self._cap = NormCapture(self.backbone, self.config.encoder_norm, self.config.decoder_norm,
|
| 118 |
+
extra=extra_layer_names({"extra_layers": self._extra_layers()}))
|
| 119 |
+
self._alias_ids = alias_token_ids(self.tokenizer, len(self.config.aliases))
|
| 120 |
+
|
| 121 |
+
@torch.no_grad()
|
| 122 |
+
def decide(self, state: str, questions: dict) -> dict:
|
| 123 |
+
"""Answer Jev-shaped questions about `state` with the backbone features and the trained head.
|
| 124 |
+
|
| 125 |
+
The answer slots are seeded exactly as hev.features.extract seeded them in training, by
|
| 126 |
+
`config.seed_mode`: the pad token (0) for "pad", the first alias token for "alias", and an average
|
| 127 |
+
over RANDOM_DRAWS forward passes with random slot tokens (fixed rng seed 1234) for "random".
|
| 128 |
+
`config.slot_token_id` is not read here; seed_mode governs.
|
| 129 |
+
"""
|
| 130 |
+
version = getattr(self.config, "prompt_format_version", 1)
|
| 131 |
+
if version != PROMPT_FORMAT_VERSION:
|
| 132 |
+
raise RuntimeError(
|
| 133 |
+
f"this head was trained with prompt format {version}, but the installed hev code builds prompt format "
|
| 134 |
+
f"{PROMPT_FORMAT_VERSION}; load the code revision the head was published with, or retrain the head")
|
| 135 |
+
self._ensure_capture()
|
| 136 |
+
qs = [Question.from_jev(qid, spec) for qid, spec in questions.items()]
|
| 137 |
+
ex = Example(id="request", source="api", state=state, questions=qs, gold={})
|
| 138 |
+
info = {"canvas_length": self.config.canvas_length, "encoder_norm": self.config.encoder_norm,
|
| 139 |
+
"decoder_norm": self.config.decoder_norm, "extra_layers": self._extra_layers()}
|
| 140 |
+
slot_tokens = _slot_token(self.config.seed_mode, self._alias_ids, random.Random(1234),
|
| 141 |
+
_vocab_size(self.backbone))
|
| 142 |
+
feats, rec = encode_example(self.backbone, self.tokenizer, ex, self._cap, info,
|
| 143 |
+
slot_tokens=slot_tokens, alias_ids=self._alias_ids,
|
| 144 |
+
max_tokens=self.config.max_prompt_tokens)
|
| 145 |
+
batch = collate([(rec, feats)]).to(next(self.head.parameters()).device)
|
| 146 |
+
logits = self.head(batch, use_temperature=True)
|
| 147 |
+
answers = {}
|
| 148 |
+
for q, lg in zip(qs, logits):
|
| 149 |
+
probs = torch.softmax(lg.float(), 0).tolist()
|
| 150 |
+
if q.type == "noul":
|
| 151 |
+
answers[q.qid] = {"type": "noul", "noul": probs[0]}
|
| 152 |
+
elif q.type == "choice":
|
| 153 |
+
best = max(range(q.k), key=lambda i: probs[i])
|
| 154 |
+
answers[q.qid] = {"type": "choice", "choice": q.options[best],
|
| 155 |
+
"probabilities": dict(zip(q.options, probs)), "confidence": self.confidence(probs)}
|
| 156 |
+
else:
|
| 157 |
+
answers[q.qid] = {"type": "score", "score": sum(i * p for i, p in enumerate(probs)),
|
| 158 |
+
"probabilities": {str(i): p for i, p in enumerate(probs)},
|
| 159 |
+
"confidence": self.confidence(probs), "legend": list(q.descriptions)}
|
| 160 |
+
name = getattr(self.config, "_name_or_path", "") or ""
|
| 161 |
+
# A local folder name would leak a filesystem path into every response, so only a Hub repo id is reported.
|
| 162 |
+
return {"model": name if name and not _is_local_dir(name) else "hev",
|
| 163 |
+
"answers": answers, "usage": {"input_tokens": rec.n_prompt, "output_tokens": 0}}
|
prompting.py
CHANGED
|
@@ -1,118 +1,118 @@
|
|
| 1 |
-
"""Prompt and canvas construction. Token spans are exact because every piece is tokenised separately."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import random
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
|
| 7 |
-
from .schema import ALIASES, Example
|
| 8 |
-
|
| 9 |
-
# Bump when anything below changes the token ids, option spans or canvas a trained head was fitted on; every
|
| 10 |
-
# feature cache must then be re-extracted and the head retrained. tests/test_format_pin.py pins version 1.
|
| 11 |
-
PROMPT_FORMAT_VERSION = 1
|
| 12 |
-
SENTINEL = "<<HEV_USER_TEXT>>"
|
| 13 |
-
HEADER = "State:\n"
|
| 14 |
-
QUESTIONS_HEADER = "\nQuestions:\n"
|
| 15 |
-
FOOTER = "\nAnswer as JSON with one option letter per question.\n"
|
| 16 |
-
MIN_STATE_TOKENS = 16
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@dataclass
|
| 20 |
-
class Piece:
|
| 21 |
-
text: str
|
| 22 |
-
kind: str # header | state | question | option | footer
|
| 23 |
-
qidx: int = -1
|
| 24 |
-
oidx: int = -1
|
| 25 |
-
alias_idx: int = -1
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
@dataclass
|
| 29 |
-
class TokenizedPrompt:
|
| 30 |
-
ids: list[int]
|
| 31 |
-
option_spans: list[list[tuple[int, int]]]
|
| 32 |
-
option_alias: list[list[int]]
|
| 33 |
-
truncated: bool
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def render_pieces(example: Example, option_order: dict[int, list[int]] | None = None) -> list[Piece]:
|
| 37 |
-
pieces = [Piece(HEADER, "header"), Piece(example.state.strip() + "\n", "state"),
|
| 38 |
-
Piece(QUESTIONS_HEADER, "header")]
|
| 39 |
-
for qi, q in enumerate(example.questions):
|
| 40 |
-
pieces.append(Piece(f"{q.qid} ({q.type}): {q.instructions}\n", "question", qi))
|
| 41 |
-
order = (option_order or {}).get(qi) or list(range(q.k))
|
| 42 |
-
for pos, oi in enumerate(order):
|
| 43 |
-
pieces.append(Piece(f" {ALIASES[pos]}: {q.descriptions[oi]}\n", "option", qi, oi, pos))
|
| 44 |
-
pieces.append(Piece(FOOTER, "footer"))
|
| 45 |
-
return pieces
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def chat_affixes(tok) -> tuple[list[int], list[int]]:
|
| 49 |
-
text = tok.apply_chat_template([{"role": "user", "content": SENTINEL}],
|
| 50 |
-
tokenize=False, add_generation_prompt=True)
|
| 51 |
-
pre, post = text.split(SENTINEL)
|
| 52 |
-
return tok.encode(pre, add_special_tokens=False), tok.encode(post, add_special_tokens=False)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def tokenize_prompt(example: Example, tok, max_tokens: int = 1024,
|
| 56 |
-
option_order: dict[int, list[int]] | None = None) -> TokenizedPrompt:
|
| 57 |
-
pre, post = chat_affixes(tok)
|
| 58 |
-
pieces = render_pieces(example, option_order)
|
| 59 |
-
enc = [tok.encode(p.text, add_special_tokens=False) for p in pieces]
|
| 60 |
-
budget = max_tokens - len(pre) - len(post)
|
| 61 |
-
fixed = sum(len(e) for e, p in zip(enc, pieces) if p.kind != "state")
|
| 62 |
-
truncated = False
|
| 63 |
-
for i, p in enumerate(pieces):
|
| 64 |
-
if p.kind == "state" and fixed + len(enc[i]) > budget:
|
| 65 |
-
keep = max(budget - fixed, MIN_STATE_TOKENS)
|
| 66 |
-
half = keep // 2
|
| 67 |
-
enc[i] = enc[i][:half] + enc[i][-(keep - half):]
|
| 68 |
-
truncated = True
|
| 69 |
-
ids = list(pre)
|
| 70 |
-
spans = [[(0, 0)] * q.k for q in example.questions]
|
| 71 |
-
alias = [[0] * q.k for q in example.questions]
|
| 72 |
-
for p, e in zip(pieces, enc):
|
| 73 |
-
start = len(ids)
|
| 74 |
-
ids.extend(e)
|
| 75 |
-
if p.kind == "option":
|
| 76 |
-
spans[p.qidx][p.oidx] = (start, len(ids))
|
| 77 |
-
alias[p.qidx][p.oidx] = p.alias_idx
|
| 78 |
-
ids.extend(post)
|
| 79 |
-
if len(ids) > max_tokens:
|
| 80 |
-
raise ValueError(f"{example.id}: prompt has {len(ids)} tokens even after truncation (max {max_tokens})")
|
| 81 |
-
return TokenizedPrompt(ids=ids, option_spans=spans, option_alias=alias, truncated=truncated)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def canvas_ids(example: Example, tok, slot_token_id: int, canvas_length: int,
|
| 85 |
-
eos_id: int = 1) -> tuple[list[int], list[int]]:
|
| 86 |
-
ids: list[int] = []
|
| 87 |
-
slots: list[int] = []
|
| 88 |
-
for qi, q in enumerate(example.questions):
|
| 89 |
-
prefix = '{"' if qi == 0 else ', "'
|
| 90 |
-
ids.extend(tok.encode(prefix + q.qid + '": "', add_special_tokens=False))
|
| 91 |
-
slots.append(len(ids))
|
| 92 |
-
ids.append(slot_token_id)
|
| 93 |
-
ids.extend(tok.encode('"', add_special_tokens=False))
|
| 94 |
-
ids.extend(tok.encode("}", add_special_tokens=False))
|
| 95 |
-
if len(ids) > canvas_length:
|
| 96 |
-
raise ValueError(f"{example.id}: canvas needs {len(ids)} tokens, canvas_length is {canvas_length}")
|
| 97 |
-
ids.extend([eos_id] * (canvas_length - len(ids)))
|
| 98 |
-
return ids, slots
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def alias_token_ids(tok, n: int) -> list[int]:
|
| 102 |
-
out = []
|
| 103 |
-
for a in ALIASES[:n]:
|
| 104 |
-
ids = tok.encode(a, add_special_tokens=False)
|
| 105 |
-
if len(ids) != 1:
|
| 106 |
-
raise ValueError(f"alias {a!r} is not a single token: {ids}")
|
| 107 |
-
out.append(ids[0])
|
| 108 |
-
return out
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def shuffled_order(example: Example, rng: random.Random) -> dict[int, list[int]]:
|
| 112 |
-
order = {}
|
| 113 |
-
for qi, q in enumerate(example.questions):
|
| 114 |
-
if q.type == "choice":
|
| 115 |
-
perm = list(range(q.k))
|
| 116 |
-
rng.shuffle(perm)
|
| 117 |
-
order[qi] = perm
|
| 118 |
-
return order
|
|
|
|
| 1 |
+
"""Prompt and canvas construction. Token spans are exact because every piece is tokenised separately."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import random
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
from .schema import ALIASES, Example
|
| 8 |
+
|
| 9 |
+
# Bump when anything below changes the token ids, option spans or canvas a trained head was fitted on; every
|
| 10 |
+
# feature cache must then be re-extracted and the head retrained. tests/test_format_pin.py pins version 1.
|
| 11 |
+
PROMPT_FORMAT_VERSION = 1
|
| 12 |
+
SENTINEL = "<<HEV_USER_TEXT>>"
|
| 13 |
+
HEADER = "State:\n"
|
| 14 |
+
QUESTIONS_HEADER = "\nQuestions:\n"
|
| 15 |
+
FOOTER = "\nAnswer as JSON with one option letter per question.\n"
|
| 16 |
+
MIN_STATE_TOKENS = 16
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class Piece:
|
| 21 |
+
text: str
|
| 22 |
+
kind: str # header | state | question | option | footer
|
| 23 |
+
qidx: int = -1
|
| 24 |
+
oidx: int = -1
|
| 25 |
+
alias_idx: int = -1
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class TokenizedPrompt:
|
| 30 |
+
ids: list[int]
|
| 31 |
+
option_spans: list[list[tuple[int, int]]]
|
| 32 |
+
option_alias: list[list[int]]
|
| 33 |
+
truncated: bool
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def render_pieces(example: Example, option_order: dict[int, list[int]] | None = None) -> list[Piece]:
|
| 37 |
+
pieces = [Piece(HEADER, "header"), Piece(example.state.strip() + "\n", "state"),
|
| 38 |
+
Piece(QUESTIONS_HEADER, "header")]
|
| 39 |
+
for qi, q in enumerate(example.questions):
|
| 40 |
+
pieces.append(Piece(f"{q.qid} ({q.type}): {q.instructions}\n", "question", qi))
|
| 41 |
+
order = (option_order or {}).get(qi) or list(range(q.k))
|
| 42 |
+
for pos, oi in enumerate(order):
|
| 43 |
+
pieces.append(Piece(f" {ALIASES[pos]}: {q.descriptions[oi]}\n", "option", qi, oi, pos))
|
| 44 |
+
pieces.append(Piece(FOOTER, "footer"))
|
| 45 |
+
return pieces
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def chat_affixes(tok) -> tuple[list[int], list[int]]:
|
| 49 |
+
text = tok.apply_chat_template([{"role": "user", "content": SENTINEL}],
|
| 50 |
+
tokenize=False, add_generation_prompt=True)
|
| 51 |
+
pre, post = text.split(SENTINEL)
|
| 52 |
+
return tok.encode(pre, add_special_tokens=False), tok.encode(post, add_special_tokens=False)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def tokenize_prompt(example: Example, tok, max_tokens: int = 1024,
|
| 56 |
+
option_order: dict[int, list[int]] | None = None) -> TokenizedPrompt:
|
| 57 |
+
pre, post = chat_affixes(tok)
|
| 58 |
+
pieces = render_pieces(example, option_order)
|
| 59 |
+
enc = [tok.encode(p.text, add_special_tokens=False) for p in pieces]
|
| 60 |
+
budget = max_tokens - len(pre) - len(post)
|
| 61 |
+
fixed = sum(len(e) for e, p in zip(enc, pieces) if p.kind != "state")
|
| 62 |
+
truncated = False
|
| 63 |
+
for i, p in enumerate(pieces):
|
| 64 |
+
if p.kind == "state" and fixed + len(enc[i]) > budget:
|
| 65 |
+
keep = max(budget - fixed, MIN_STATE_TOKENS)
|
| 66 |
+
half = keep // 2
|
| 67 |
+
enc[i] = enc[i][:half] + enc[i][-(keep - half):]
|
| 68 |
+
truncated = True
|
| 69 |
+
ids = list(pre)
|
| 70 |
+
spans = [[(0, 0)] * q.k for q in example.questions]
|
| 71 |
+
alias = [[0] * q.k for q in example.questions]
|
| 72 |
+
for p, e in zip(pieces, enc):
|
| 73 |
+
start = len(ids)
|
| 74 |
+
ids.extend(e)
|
| 75 |
+
if p.kind == "option":
|
| 76 |
+
spans[p.qidx][p.oidx] = (start, len(ids))
|
| 77 |
+
alias[p.qidx][p.oidx] = p.alias_idx
|
| 78 |
+
ids.extend(post)
|
| 79 |
+
if len(ids) > max_tokens:
|
| 80 |
+
raise ValueError(f"{example.id}: prompt has {len(ids)} tokens even after truncation (max {max_tokens})")
|
| 81 |
+
return TokenizedPrompt(ids=ids, option_spans=spans, option_alias=alias, truncated=truncated)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def canvas_ids(example: Example, tok, slot_token_id: int, canvas_length: int,
|
| 85 |
+
eos_id: int = 1) -> tuple[list[int], list[int]]:
|
| 86 |
+
ids: list[int] = []
|
| 87 |
+
slots: list[int] = []
|
| 88 |
+
for qi, q in enumerate(example.questions):
|
| 89 |
+
prefix = '{"' if qi == 0 else ', "'
|
| 90 |
+
ids.extend(tok.encode(prefix + q.qid + '": "', add_special_tokens=False))
|
| 91 |
+
slots.append(len(ids))
|
| 92 |
+
ids.append(slot_token_id)
|
| 93 |
+
ids.extend(tok.encode('"', add_special_tokens=False))
|
| 94 |
+
ids.extend(tok.encode("}", add_special_tokens=False))
|
| 95 |
+
if len(ids) > canvas_length:
|
| 96 |
+
raise ValueError(f"{example.id}: canvas needs {len(ids)} tokens, canvas_length is {canvas_length}")
|
| 97 |
+
ids.extend([eos_id] * (canvas_length - len(ids)))
|
| 98 |
+
return ids, slots
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def alias_token_ids(tok, n: int) -> list[int]:
|
| 102 |
+
out = []
|
| 103 |
+
for a in ALIASES[:n]:
|
| 104 |
+
ids = tok.encode(a, add_special_tokens=False)
|
| 105 |
+
if len(ids) != 1:
|
| 106 |
+
raise ValueError(f"alias {a!r} is not a single token: {ids}")
|
| 107 |
+
out.append(ids[0])
|
| 108 |
+
return out
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def shuffled_order(example: Example, rng: random.Random) -> dict[int, list[int]]:
|
| 112 |
+
order = {}
|
| 113 |
+
for qi, q in enumerate(example.questions):
|
| 114 |
+
if q.type == "choice":
|
| 115 |
+
perm = list(range(q.k))
|
| 116 |
+
rng.shuffle(perm)
|
| 117 |
+
order[qi] = perm
|
| 118 |
+
return order
|
schema.py
CHANGED
|
@@ -1,104 +1,104 @@
|
|
| 1 |
-
"""Request schema shared by data loading, prompting, the head and the client."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import math
|
| 5 |
-
from dataclasses import asdict, dataclass, field
|
| 6 |
-
from typing import Optional
|
| 7 |
-
|
| 8 |
-
QUESTION_TYPES = ("noul", "choice", "score")
|
| 9 |
-
TYPE_INDEX = {t: i for i, t in enumerate(QUESTION_TYPES)}
|
| 10 |
-
MAX_CHOICE_OPTIONS = 64
|
| 11 |
-
MAX_SCORE_LEVELS = 10
|
| 12 |
-
NOUL_OPTIONS = ["true", "false"]
|
| 13 |
-
NOUL_DEFAULT_DESCRIPTIONS = ["yes, the statement is true", "no, the statement is false"]
|
| 14 |
-
ALIASES = (
|
| 15 |
-
[chr(c) for c in range(ord("A"), ord("Z") + 1)]
|
| 16 |
-
+ [chr(c) for c in range(ord("a"), ord("z") + 1)]
|
| 17 |
-
+ ["A" + chr(c) for c in range(ord("A"), ord("L") + 1)]
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
@dataclass
|
| 22 |
-
class Question:
|
| 23 |
-
qid: str
|
| 24 |
-
type: str
|
| 25 |
-
instructions: str
|
| 26 |
-
options: list[str]
|
| 27 |
-
descriptions: list[str]
|
| 28 |
-
|
| 29 |
-
@classmethod
|
| 30 |
-
def from_jev(cls, qid: str, spec: dict) -> "Question":
|
| 31 |
-
qtype = spec.get("type")
|
| 32 |
-
if qtype not in QUESTION_TYPES:
|
| 33 |
-
raise ValueError(f"{qid}: unknown question type {qtype!r}")
|
| 34 |
-
instructions = str(spec.get("instructions", "")).strip()
|
| 35 |
-
criteria = spec.get("criteria")
|
| 36 |
-
if qtype == "noul":
|
| 37 |
-
options = list(NOUL_OPTIONS)
|
| 38 |
-
descriptions = list(NOUL_DEFAULT_DESCRIPTIONS)
|
| 39 |
-
if criteria is not None:
|
| 40 |
-
if not isinstance(criteria, dict) or set(criteria) - set(NOUL_OPTIONS):
|
| 41 |
-
raise ValueError(f"{qid}: noul criteria must be a mapping with 'true' and 'false' keys")
|
| 42 |
-
descriptions = [str(criteria.get("true") or descriptions[0]),
|
| 43 |
-
str(criteria.get("false") or descriptions[1])]
|
| 44 |
-
elif qtype == "choice":
|
| 45 |
-
if not isinstance(criteria, dict):
|
| 46 |
-
raise ValueError(f"{qid}: choice needs a criteria mapping")
|
| 47 |
-
options = [str(k) for k in criteria.keys()]
|
| 48 |
-
descriptions = [str(v) if v else str(k) for k, v in criteria.items()]
|
| 49 |
-
if not (2 <= len(options) <= MAX_CHOICE_OPTIONS):
|
| 50 |
-
raise ValueError(f"{qid}: choice needs 2..{MAX_CHOICE_OPTIONS} options, got {len(options)}")
|
| 51 |
-
else:
|
| 52 |
-
if not isinstance(criteria, list):
|
| 53 |
-
raise ValueError(f"{qid}: score needs a criteria list")
|
| 54 |
-
if not (2 <= len(criteria) <= MAX_SCORE_LEVELS):
|
| 55 |
-
raise ValueError(f"{qid}: score needs 2..{MAX_SCORE_LEVELS} levels, got {len(criteria)}")
|
| 56 |
-
options = [str(i) for i in range(len(criteria))]
|
| 57 |
-
descriptions = [str(c) for c in criteria]
|
| 58 |
-
if len(set(options)) != len(options):
|
| 59 |
-
raise ValueError(f"{qid}: duplicate options")
|
| 60 |
-
return cls(qid=qid, type=qtype, instructions=instructions,
|
| 61 |
-
options=options, descriptions=descriptions)
|
| 62 |
-
|
| 63 |
-
@property
|
| 64 |
-
def k(self) -> int:
|
| 65 |
-
return len(self.options)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def argmax(values) -> int:
|
| 69 |
-
"""Index of the first maximum, so ties go to the lowest index. Shared by evaluation and the zero-shot read."""
|
| 70 |
-
return max(range(len(values)), key=lambda i: values[i])
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def align_distribution(probs: dict, options: list[str]) -> list[float]:
|
| 74 |
-
vals = [float(probs.get(o, 0.0)) for o in options]
|
| 75 |
-
if any(v < 0 or not math.isfinite(v) for v in vals):
|
| 76 |
-
raise ValueError("distribution values must be finite and non-negative")
|
| 77 |
-
total = sum(vals)
|
| 78 |
-
if total <= 0:
|
| 79 |
-
raise ValueError("distribution has no mass on the listed options")
|
| 80 |
-
return [v / total for v in vals]
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
@dataclass
|
| 84 |
-
class Example:
|
| 85 |
-
id: str
|
| 86 |
-
source: str
|
| 87 |
-
state: str
|
| 88 |
-
questions: list[Question]
|
| 89 |
-
gold: dict[str, Optional[list[float]]]
|
| 90 |
-
meta: dict = field(default_factory=dict)
|
| 91 |
-
|
| 92 |
-
def to_dict(self) -> dict:
|
| 93 |
-
return {
|
| 94 |
-
"id": self.id, "source": self.source, "state": self.state,
|
| 95 |
-
"questions": [asdict(q) for q in self.questions],
|
| 96 |
-
"gold": self.gold, "meta": self.meta,
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
@classmethod
|
| 100 |
-
def from_dict(cls, d: dict) -> "Example":
|
| 101 |
-
return cls(id=d["id"], source=d["source"], state=d["state"],
|
| 102 |
-
questions=[Question(**q) for q in d["questions"]],
|
| 103 |
-
gold={k: (list(v) if v is not None else None) for k, v in d["gold"].items()},
|
| 104 |
-
meta=dict(d.get("meta", {})))
|
|
|
|
| 1 |
+
"""Request schema shared by data loading, prompting, the head and the client."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import asdict, dataclass, field
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
QUESTION_TYPES = ("noul", "choice", "score")
|
| 9 |
+
TYPE_INDEX = {t: i for i, t in enumerate(QUESTION_TYPES)}
|
| 10 |
+
MAX_CHOICE_OPTIONS = 64
|
| 11 |
+
MAX_SCORE_LEVELS = 10
|
| 12 |
+
NOUL_OPTIONS = ["true", "false"]
|
| 13 |
+
NOUL_DEFAULT_DESCRIPTIONS = ["yes, the statement is true", "no, the statement is false"]
|
| 14 |
+
ALIASES = (
|
| 15 |
+
[chr(c) for c in range(ord("A"), ord("Z") + 1)]
|
| 16 |
+
+ [chr(c) for c in range(ord("a"), ord("z") + 1)]
|
| 17 |
+
+ ["A" + chr(c) for c in range(ord("A"), ord("L") + 1)]
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Question:
|
| 23 |
+
qid: str
|
| 24 |
+
type: str
|
| 25 |
+
instructions: str
|
| 26 |
+
options: list[str]
|
| 27 |
+
descriptions: list[str]
|
| 28 |
+
|
| 29 |
+
@classmethod
|
| 30 |
+
def from_jev(cls, qid: str, spec: dict) -> "Question":
|
| 31 |
+
qtype = spec.get("type")
|
| 32 |
+
if qtype not in QUESTION_TYPES:
|
| 33 |
+
raise ValueError(f"{qid}: unknown question type {qtype!r}")
|
| 34 |
+
instructions = str(spec.get("instructions", "")).strip()
|
| 35 |
+
criteria = spec.get("criteria")
|
| 36 |
+
if qtype == "noul":
|
| 37 |
+
options = list(NOUL_OPTIONS)
|
| 38 |
+
descriptions = list(NOUL_DEFAULT_DESCRIPTIONS)
|
| 39 |
+
if criteria is not None:
|
| 40 |
+
if not isinstance(criteria, dict) or set(criteria) - set(NOUL_OPTIONS):
|
| 41 |
+
raise ValueError(f"{qid}: noul criteria must be a mapping with 'true' and 'false' keys")
|
| 42 |
+
descriptions = [str(criteria.get("true") or descriptions[0]),
|
| 43 |
+
str(criteria.get("false") or descriptions[1])]
|
| 44 |
+
elif qtype == "choice":
|
| 45 |
+
if not isinstance(criteria, dict):
|
| 46 |
+
raise ValueError(f"{qid}: choice needs a criteria mapping")
|
| 47 |
+
options = [str(k) for k in criteria.keys()]
|
| 48 |
+
descriptions = [str(v) if v else str(k) for k, v in criteria.items()]
|
| 49 |
+
if not (2 <= len(options) <= MAX_CHOICE_OPTIONS):
|
| 50 |
+
raise ValueError(f"{qid}: choice needs 2..{MAX_CHOICE_OPTIONS} options, got {len(options)}")
|
| 51 |
+
else:
|
| 52 |
+
if not isinstance(criteria, list):
|
| 53 |
+
raise ValueError(f"{qid}: score needs a criteria list")
|
| 54 |
+
if not (2 <= len(criteria) <= MAX_SCORE_LEVELS):
|
| 55 |
+
raise ValueError(f"{qid}: score needs 2..{MAX_SCORE_LEVELS} levels, got {len(criteria)}")
|
| 56 |
+
options = [str(i) for i in range(len(criteria))]
|
| 57 |
+
descriptions = [str(c) for c in criteria]
|
| 58 |
+
if len(set(options)) != len(options):
|
| 59 |
+
raise ValueError(f"{qid}: duplicate options")
|
| 60 |
+
return cls(qid=qid, type=qtype, instructions=instructions,
|
| 61 |
+
options=options, descriptions=descriptions)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def k(self) -> int:
|
| 65 |
+
return len(self.options)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def argmax(values) -> int:
|
| 69 |
+
"""Index of the first maximum, so ties go to the lowest index. Shared by evaluation and the zero-shot read."""
|
| 70 |
+
return max(range(len(values)), key=lambda i: values[i])
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def align_distribution(probs: dict, options: list[str]) -> list[float]:
|
| 74 |
+
vals = [float(probs.get(o, 0.0)) for o in options]
|
| 75 |
+
if any(v < 0 or not math.isfinite(v) for v in vals):
|
| 76 |
+
raise ValueError("distribution values must be finite and non-negative")
|
| 77 |
+
total = sum(vals)
|
| 78 |
+
if total <= 0:
|
| 79 |
+
raise ValueError("distribution has no mass on the listed options")
|
| 80 |
+
return [v / total for v in vals]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass
|
| 84 |
+
class Example:
|
| 85 |
+
id: str
|
| 86 |
+
source: str
|
| 87 |
+
state: str
|
| 88 |
+
questions: list[Question]
|
| 89 |
+
gold: dict[str, Optional[list[float]]]
|
| 90 |
+
meta: dict = field(default_factory=dict)
|
| 91 |
+
|
| 92 |
+
def to_dict(self) -> dict:
|
| 93 |
+
return {
|
| 94 |
+
"id": self.id, "source": self.source, "state": self.state,
|
| 95 |
+
"questions": [asdict(q) for q in self.questions],
|
| 96 |
+
"gold": self.gold, "meta": self.meta,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def from_dict(cls, d: dict) -> "Example":
|
| 101 |
+
return cls(id=d["id"], source=d["source"], state=d["state"],
|
| 102 |
+
questions=[Question(**q) for q in d["questions"]],
|
| 103 |
+
gold={k: (list(v) if v is not None else None) for k, v in d["gold"].items()},
|
| 104 |
+
meta=dict(d.get("meta", {})))
|