Text Classification
Transformers
Safetensors
truetype_system_one
feature-extraction
custom_code
gemma4
typed-decisions
Instructions to use stephenlb/system-one-model with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use stephenlb/system-one-model with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="stephenlb/system-one-model", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("stephenlb/system-one-model", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- README.md +79 -0
- __init__.py +0 -0
- __pycache__/__init__.cpython-314.pyc +0 -0
- __pycache__/configuration_truetype.cpython-314.pyc +0 -0
- __pycache__/letters.cpython-314.pyc +0 -0
- __pycache__/modeling_truetype.cpython-314.pyc +0 -0
- __pycache__/questions.cpython-314.pyc +0 -0
- __pycache__/render.cpython-314.pyc +0 -0
- config.json +241 -0
- configuration_truetype.py +23 -0
- generation_config.json +14 -0
- letters.py +138 -0
- model.safetensors +3 -0
- modeling_truetype.py +108 -0
- pipeline_truetype.py +58 -0
- questions.py +270 -0
- render.py +169 -0
- tokenizer.json +3 -0
- tokenizer_config.json +55 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: gemma
|
| 4 |
+
base_model: google/gemma-4-12B
|
| 5 |
+
pipeline_tag: text-classification
|
| 6 |
+
tags:
|
| 7 |
+
- custom_code
|
| 8 |
+
- gemma4
|
| 9 |
+
- typed-decisions
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Truetype System One (Gemma 4 12B, 26-letter output layer)
|
| 13 |
+
|
| 14 |
+
`google/gemma-4-12B` with its 262k-token language-model head replaced by a
|
| 15 |
+
26-row layer that outputs one logit per letter `A`-`Z`. The decoder is
|
| 16 |
+
unchanged. One forward pass gives the 26 logits; questions (`noul`, `choice`,
|
| 17 |
+
`score`) are answered by softmax over the letters each question declares legal.
|
| 18 |
+
|
| 19 |
+
Requires `transformers>=5.17`. The repo ships custom code, so pass
|
| 20 |
+
`trust_remote_code=True`.
|
| 21 |
+
|
| 22 |
+
## Model API
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
from transformers import AutoModelForMultimodalLM, AutoTokenizer
|
| 26 |
+
|
| 27 |
+
repo = "stephenlb/system-one-model"
|
| 28 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 29 |
+
model = AutoModelForMultimodalLM.from_pretrained(
|
| 30 |
+
repo, trust_remote_code=True, dtype="bfloat16", device_map="auto"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
result = model.system_one(
|
| 34 |
+
tokenizer,
|
| 35 |
+
state="I was charged twice for order A-104.",
|
| 36 |
+
questions={
|
| 37 |
+
"refund": {"type": "noul", "instructions": "Does the text request a refund?"},
|
| 38 |
+
"team": {
|
| 39 |
+
"type": "choice",
|
| 40 |
+
"instructions": "Which team should handle this?",
|
| 41 |
+
"criteria": {"billing": "Charges", "returns": "Refunds"},
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
)
|
| 45 |
+
print(result["answers"])
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Pipeline API
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
from transformers import pipeline
|
| 52 |
+
|
| 53 |
+
pipe = pipeline("system-one", model=repo, trust_remote_code=True, dtype="bfloat16", device_map="auto")
|
| 54 |
+
print(pipe({"state": "I was charged twice.", "questions": {...}}))
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Output
|
| 58 |
+
|
| 59 |
+
```json
|
| 60 |
+
{"answers": {
|
| 61 |
+
"refund": {"type": "noul", "noul": 0.93},
|
| 62 |
+
"team": {"type": "choice", "choice": "billing",
|
| 63 |
+
"probabilities": {"billing": 0.97, "returns": 0.03}, "confidence": 0.8},
|
| 64 |
+
"urgency": {"type": "score", "score": 3.1, "legend": {"0": "..."},
|
| 65 |
+
"probabilities": {"0": 0.0, "1": 0.1}, "confidence": 0.5}}}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
`noul` is the probability of yes. `choice` returns the top label with
|
| 69 |
+
probabilities per option. `score` returns the expected level over an ordered
|
| 70 |
+
`criteria` list. Pass `temperature=` (default 0.7) to sharpen or soften.
|
| 71 |
+
|
| 72 |
+
## Raw letter logits
|
| 73 |
+
|
| 74 |
+
```python
|
| 75 |
+
enc = tokenizer(prompts, return_tensors="pt", padding=True) # padding_side="left"
|
| 76 |
+
logits = model.letter_logits(enc.input_ids, enc.attention_mask) # [batch, 26]
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Source and prompt format: https://github.com/stephenlb/truetype.ai-open
|
__init__.py
ADDED
|
File without changes
|
__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (177 Bytes). View file
|
|
|
__pycache__/configuration_truetype.cpython-314.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
__pycache__/letters.cpython-314.pyc
ADDED
|
Binary file (8.8 kB). View file
|
|
|
__pycache__/modeling_truetype.cpython-314.pyc
ADDED
|
Binary file (5.99 kB). View file
|
|
|
__pycache__/questions.cpython-314.pyc
ADDED
|
Binary file (17.2 kB). View file
|
|
|
__pycache__/render.cpython-314.pyc
ADDED
|
Binary file (9.43 kB). View file
|
|
|
config.json
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"TrueTypeSystemOneModel"
|
| 4 |
+
],
|
| 5 |
+
"audio_config": {
|
| 6 |
+
"_name_or_path": "",
|
| 7 |
+
"architectures": null,
|
| 8 |
+
"audio_embed_dim": 640,
|
| 9 |
+
"chunk_size_feed_forward": 0,
|
| 10 |
+
"dtype": "bfloat16",
|
| 11 |
+
"id2label": {
|
| 12 |
+
"0": "LABEL_0",
|
| 13 |
+
"1": "LABEL_1"
|
| 14 |
+
},
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"is_encoder_decoder": false,
|
| 17 |
+
"label2id": {
|
| 18 |
+
"LABEL_0": 0,
|
| 19 |
+
"LABEL_1": 1
|
| 20 |
+
},
|
| 21 |
+
"model_type": "gemma4_unified_audio",
|
| 22 |
+
"output_attentions": false,
|
| 23 |
+
"output_hidden_states": false,
|
| 24 |
+
"problem_type": null,
|
| 25 |
+
"return_dict": true,
|
| 26 |
+
"rms_norm_eps": 1e-06
|
| 27 |
+
},
|
| 28 |
+
"audio_token_id": 258881,
|
| 29 |
+
"boa_token_id": 256000,
|
| 30 |
+
"boi_token_id": 255999,
|
| 31 |
+
"dtype": "bfloat16",
|
| 32 |
+
"eoa_token_index": 258883,
|
| 33 |
+
"eoi_token_id": 258882,
|
| 34 |
+
"image_token_id": 258880,
|
| 35 |
+
"initializer_range": 0.02,
|
| 36 |
+
"letter_token_ids": [
|
| 37 |
+
562,
|
| 38 |
+
603,
|
| 39 |
+
565,
|
| 40 |
+
622,
|
| 41 |
+
645,
|
| 42 |
+
633,
|
| 43 |
+
667,
|
| 44 |
+
640,
|
| 45 |
+
564,
|
| 46 |
+
730,
|
| 47 |
+
751,
|
| 48 |
+
639,
|
| 49 |
+
587,
|
| 50 |
+
646,
|
| 51 |
+
708,
|
| 52 |
+
593,
|
| 53 |
+
1366,
|
| 54 |
+
630,
|
| 55 |
+
555,
|
| 56 |
+
558,
|
| 57 |
+
749,
|
| 58 |
+
782,
|
| 59 |
+
649,
|
| 60 |
+
1684,
|
| 61 |
+
895,
|
| 62 |
+
1491
|
| 63 |
+
],
|
| 64 |
+
"model_type": "truetype_system_one",
|
| 65 |
+
"num_letters": 26,
|
| 66 |
+
"text_config": {
|
| 67 |
+
"attention_bias": false,
|
| 68 |
+
"attention_dropout": 0.0,
|
| 69 |
+
"attention_k_eq_v": true,
|
| 70 |
+
"bos_token_id": 2,
|
| 71 |
+
"dtype": "bfloat16",
|
| 72 |
+
"enable_moe_block": false,
|
| 73 |
+
"eos_token_id": 1,
|
| 74 |
+
"final_logit_softcapping": 30.0,
|
| 75 |
+
"head_dim": 256,
|
| 76 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 77 |
+
"hidden_size": 3840,
|
| 78 |
+
"hidden_size_per_layer_input": 0,
|
| 79 |
+
"initializer_range": 0.02,
|
| 80 |
+
"intermediate_size": 15360,
|
| 81 |
+
"layer_types": [
|
| 82 |
+
"sliding_attention",
|
| 83 |
+
"sliding_attention",
|
| 84 |
+
"sliding_attention",
|
| 85 |
+
"sliding_attention",
|
| 86 |
+
"sliding_attention",
|
| 87 |
+
"full_attention",
|
| 88 |
+
"sliding_attention",
|
| 89 |
+
"sliding_attention",
|
| 90 |
+
"sliding_attention",
|
| 91 |
+
"sliding_attention",
|
| 92 |
+
"sliding_attention",
|
| 93 |
+
"full_attention",
|
| 94 |
+
"sliding_attention",
|
| 95 |
+
"sliding_attention",
|
| 96 |
+
"sliding_attention",
|
| 97 |
+
"sliding_attention",
|
| 98 |
+
"sliding_attention",
|
| 99 |
+
"full_attention",
|
| 100 |
+
"sliding_attention",
|
| 101 |
+
"sliding_attention",
|
| 102 |
+
"sliding_attention",
|
| 103 |
+
"sliding_attention",
|
| 104 |
+
"sliding_attention",
|
| 105 |
+
"full_attention",
|
| 106 |
+
"sliding_attention",
|
| 107 |
+
"sliding_attention",
|
| 108 |
+
"sliding_attention",
|
| 109 |
+
"sliding_attention",
|
| 110 |
+
"sliding_attention",
|
| 111 |
+
"full_attention",
|
| 112 |
+
"sliding_attention",
|
| 113 |
+
"sliding_attention",
|
| 114 |
+
"sliding_attention",
|
| 115 |
+
"sliding_attention",
|
| 116 |
+
"sliding_attention",
|
| 117 |
+
"full_attention",
|
| 118 |
+
"sliding_attention",
|
| 119 |
+
"sliding_attention",
|
| 120 |
+
"sliding_attention",
|
| 121 |
+
"sliding_attention",
|
| 122 |
+
"sliding_attention",
|
| 123 |
+
"full_attention",
|
| 124 |
+
"sliding_attention",
|
| 125 |
+
"sliding_attention",
|
| 126 |
+
"sliding_attention",
|
| 127 |
+
"sliding_attention",
|
| 128 |
+
"sliding_attention",
|
| 129 |
+
"full_attention"
|
| 130 |
+
],
|
| 131 |
+
"max_position_embeddings": 262144,
|
| 132 |
+
"model_type": "gemma4_unified_text",
|
| 133 |
+
"moe_intermediate_size": null,
|
| 134 |
+
"num_attention_heads": 16,
|
| 135 |
+
"num_experts": null,
|
| 136 |
+
"num_hidden_layers": 48,
|
| 137 |
+
"num_key_value_heads": 8,
|
| 138 |
+
"num_kv_shared_layers": 0,
|
| 139 |
+
"pad_token_id": 0,
|
| 140 |
+
"per_layer_config": {
|
| 141 |
+
"05": {
|
| 142 |
+
"head_dim": 512,
|
| 143 |
+
"num_key_value_heads": 1
|
| 144 |
+
},
|
| 145 |
+
"11": {
|
| 146 |
+
"head_dim": 512,
|
| 147 |
+
"num_key_value_heads": 1
|
| 148 |
+
},
|
| 149 |
+
"17": {
|
| 150 |
+
"head_dim": 512,
|
| 151 |
+
"num_key_value_heads": 1
|
| 152 |
+
},
|
| 153 |
+
"23": {
|
| 154 |
+
"head_dim": 512,
|
| 155 |
+
"num_key_value_heads": 1
|
| 156 |
+
},
|
| 157 |
+
"29": {
|
| 158 |
+
"head_dim": 512,
|
| 159 |
+
"num_key_value_heads": 1
|
| 160 |
+
},
|
| 161 |
+
"35": {
|
| 162 |
+
"head_dim": 512,
|
| 163 |
+
"num_key_value_heads": 1
|
| 164 |
+
},
|
| 165 |
+
"41": {
|
| 166 |
+
"head_dim": 512,
|
| 167 |
+
"num_key_value_heads": 1
|
| 168 |
+
},
|
| 169 |
+
"47": {
|
| 170 |
+
"head_dim": 512,
|
| 171 |
+
"num_key_value_heads": 1
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
"rms_norm_eps": 1e-06,
|
| 175 |
+
"rope_parameters": {
|
| 176 |
+
"full_attention": {
|
| 177 |
+
"partial_rotary_factor": 0.25,
|
| 178 |
+
"rope_theta": 1000000.0,
|
| 179 |
+
"rope_type": "proportional"
|
| 180 |
+
},
|
| 181 |
+
"sliding_attention": {
|
| 182 |
+
"rope_theta": 10000.0,
|
| 183 |
+
"rope_type": "default"
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
"sliding_window": 1024,
|
| 187 |
+
"tie_word_embeddings": true,
|
| 188 |
+
"top_k_experts": null,
|
| 189 |
+
"use_bidirectional_attention": "vision",
|
| 190 |
+
"use_cache": true,
|
| 191 |
+
"use_double_wide_mlp": false,
|
| 192 |
+
"vocab_size": 262144,
|
| 193 |
+
"vocab_size_per_layer_input": 262144
|
| 194 |
+
},
|
| 195 |
+
"tie_word_embeddings": false,
|
| 196 |
+
"transformers_version": "5.17.0",
|
| 197 |
+
"video_token_id": 258884,
|
| 198 |
+
"vision_config": {
|
| 199 |
+
"_name_or_path": "",
|
| 200 |
+
"architectures": null,
|
| 201 |
+
"chunk_size_feed_forward": 0,
|
| 202 |
+
"dtype": "bfloat16",
|
| 203 |
+
"id2label": {
|
| 204 |
+
"0": "LABEL_0",
|
| 205 |
+
"1": "LABEL_1"
|
| 206 |
+
},
|
| 207 |
+
"initializer_range": 0.02,
|
| 208 |
+
"is_encoder_decoder": false,
|
| 209 |
+
"label2id": {
|
| 210 |
+
"LABEL_0": 0,
|
| 211 |
+
"LABEL_1": 1
|
| 212 |
+
},
|
| 213 |
+
"mm_embed_dim": 3840,
|
| 214 |
+
"mm_posemb_size": 1120,
|
| 215 |
+
"model_type": "gemma4_unified_vision",
|
| 216 |
+
"num_soft_tokens": 280,
|
| 217 |
+
"output_attentions": false,
|
| 218 |
+
"output_hidden_states": false,
|
| 219 |
+
"output_proj_dims": 3840,
|
| 220 |
+
"patch_size": 16,
|
| 221 |
+
"pooling_kernel_size": 3,
|
| 222 |
+
"problem_type": null,
|
| 223 |
+
"return_dict": true,
|
| 224 |
+
"rms_norm_eps": 1e-06
|
| 225 |
+
},
|
| 226 |
+
"auto_map": {
|
| 227 |
+
"AutoConfig": "configuration_truetype.TrueTypeSystemOneConfig",
|
| 228 |
+
"AutoModel": "modeling_truetype.TrueTypeSystemOneModel",
|
| 229 |
+
"AutoModelForMultimodalLM": "modeling_truetype.TrueTypeSystemOneModel",
|
| 230 |
+
"AutoModelForImageTextToText": "modeling_truetype.TrueTypeSystemOneModel"
|
| 231 |
+
},
|
| 232 |
+
"custom_pipelines": {
|
| 233 |
+
"system-one": {
|
| 234 |
+
"impl": "pipeline_truetype.SystemOnePipeline",
|
| 235 |
+
"pt": [
|
| 236 |
+
"AutoModelForMultimodalLM"
|
| 237 |
+
],
|
| 238 |
+
"tf": []
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
}
|
configuration_truetype.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Config for the Gemma 4 System One model with a 26-logit A-Z output layer."""
|
| 2 |
+
|
| 3 |
+
from transformers.models.gemma4_unified.configuration_gemma4_unified import Gemma4UnifiedConfig
|
| 4 |
+
|
| 5 |
+
LETTERS = tuple("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TrueTypeSystemOneConfig(Gemma4UnifiedConfig):
|
| 9 |
+
"""Gemma 4 config plus the tokenizer ids the 26 output rows were copied from.
|
| 10 |
+
|
| 11 |
+
``letter_token_ids[i]`` is the vocabulary id whose embedding row became output
|
| 12 |
+
row ``i`` (``A`` to ``Z``). It is kept for provenance and for verification
|
| 13 |
+
against the original vocabulary head.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
model_type = "truetype_system_one"
|
| 17 |
+
|
| 18 |
+
def __init__(self, letter_token_ids=None, num_letters=len(LETTERS), **kwargs):
|
| 19 |
+
# The 26-row head is its own parameter, not the input embedding.
|
| 20 |
+
kwargs["tie_word_embeddings"] = False
|
| 21 |
+
super().__init__(**kwargs)
|
| 22 |
+
self.letter_token_ids = list(letter_token_ids or [])
|
| 23 |
+
self.num_letters = num_letters
|
generation_config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 2,
|
| 3 |
+
"do_sample": true,
|
| 4 |
+
"eos_token_id": 1,
|
| 5 |
+
"pad_token_id": 0,
|
| 6 |
+
"suppress_tokens": [
|
| 7 |
+
258883,
|
| 8 |
+
258882
|
| 9 |
+
],
|
| 10 |
+
"temperature": 1.0,
|
| 11 |
+
"top_k": 64,
|
| 12 |
+
"top_p": 0.95,
|
| 13 |
+
"transformers_version": "5.17.0"
|
| 14 |
+
}
|
letters.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extract single-letter logits and compute top-k softmax scores.
|
| 2 |
+
|
| 3 |
+
The engine reads one next-token position, keeps logits for the 26 uppercase
|
| 4 |
+
ASCII letters, and converts the top-k letter logits into a probability
|
| 5 |
+
distribution. It does not sample or decode text.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
import string
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
LETTERS: tuple[str, ...] = tuple(string.ascii_uppercase)
|
| 15 |
+
LETTER_INDEX: dict[str, int] = {c: i for i, c in enumerate(LETTERS)}
|
| 16 |
+
|
| 17 |
+
# Rank at which we stop tracking for the reported top-k. k <= 26 by construction.
|
| 18 |
+
MAX_TOP_K = len(LETTERS)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class LetterLogitError(ValueError):
|
| 22 |
+
"""Raised when the model's next-token distribution cannot be read as letters."""
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass(frozen=True)
|
| 26 |
+
class LetterReadout:
|
| 27 |
+
"""The full 26-letter logit readout for one prompt, plus a top-k view.
|
| 28 |
+
|
| 29 |
+
``logits`` keeps every letter when a question has more options than ``top_k``.
|
| 30 |
+
``top`` is the reported top-k softmax distribution.
|
| 31 |
+
``letter_mass`` is retained as a nullable compatibility field. The optimized
|
| 32 |
+
26-row output head intentionally cannot report it because it never computes
|
| 33 |
+
a full-vocabulary distribution.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
logits: dict[str, float]
|
| 37 |
+
top: "SoftmaxDistribution"
|
| 38 |
+
letter_mass: float | None = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass(frozen=True)
|
| 42 |
+
class SoftmaxDistribution:
|
| 43 |
+
"""A probability distribution over a chosen slice of letters."""
|
| 44 |
+
|
| 45 |
+
ranked: tuple[tuple[str, float], ...]
|
| 46 |
+
probabilities: dict[str, float]
|
| 47 |
+
top_k: int
|
| 48 |
+
temperature: float
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def top_letter(self) -> str:
|
| 52 |
+
return self.ranked[0][0]
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def top_probability(self) -> float:
|
| 56 |
+
return self.probabilities[self.top_letter]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _softmax(logits: list[float], temperature: float) -> list[float]:
|
| 60 |
+
if temperature <= 0:
|
| 61 |
+
raise ValueError("temperature must be > 0")
|
| 62 |
+
scaled = [x / temperature for x in logits]
|
| 63 |
+
peak = max(scaled)
|
| 64 |
+
exps = [math.exp(x - peak) for x in scaled]
|
| 65 |
+
total = sum(exps)
|
| 66 |
+
if total <= 0 or not math.isfinite(total):
|
| 67 |
+
raise LetterLogitError("softmax denominator is not finite")
|
| 68 |
+
return [e / total for e in exps]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def distribution_from_letter_logits(
|
| 72 |
+
letter_logits: dict[str, float],
|
| 73 |
+
*,
|
| 74 |
+
top_k: int = 5,
|
| 75 |
+
temperature: float = 1.0,
|
| 76 |
+
) -> SoftmaxDistribution:
|
| 77 |
+
"""Softmax the top-k letter logits into a probability distribution.
|
| 78 |
+
|
| 79 |
+
`top_k` is clamped to 2..26. The softmax is taken *only* over the k letters we
|
| 80 |
+
keep, which is what TypeSafe-compatible consumers expect: the reported
|
| 81 |
+
probabilities sum to 1 across the surfaced options.
|
| 82 |
+
"""
|
| 83 |
+
unknown = set(letter_logits) - set(LETTER_INDEX)
|
| 84 |
+
if unknown:
|
| 85 |
+
raise LetterLogitError(f"non-letter logit keys: {sorted(unknown)}")
|
| 86 |
+
if not letter_logits:
|
| 87 |
+
raise LetterLogitError("no letter logits supplied")
|
| 88 |
+
|
| 89 |
+
k = max(1, min(int(top_k), len(letter_logits), MAX_TOP_K))
|
| 90 |
+
ranked = sorted(letter_logits.items(), key=lambda kv: kv[1], reverse=True)[:k]
|
| 91 |
+
probs = _softmax([v for _, v in ranked], temperature)
|
| 92 |
+
|
| 93 |
+
distribution = {letter: p for (letter, _), p in zip(ranked, probs)}
|
| 94 |
+
# Keep the distribution in descending-probability order for stable output.
|
| 95 |
+
ordered = dict(sorted(distribution.items(), key=lambda kv: kv[1], reverse=True))
|
| 96 |
+
return SoftmaxDistribution(
|
| 97 |
+
ranked=tuple((letter, logit) for letter, logit in ranked),
|
| 98 |
+
probabilities=ordered,
|
| 99 |
+
top_k=k,
|
| 100 |
+
temperature=temperature,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def gather_letter_logits(next_token_logits, letter_token_ids):
|
| 105 |
+
"""Return the ``[batch, 26]`` A-Z slice of a vocabulary-logit tensor.
|
| 106 |
+
|
| 107 |
+
``letter_token_ids`` may be the device-resident tensor owned by the engine.
|
| 108 |
+
Keeping it on-device avoids allocating a Python list and an indexing tensor on
|
| 109 |
+
every request.
|
| 110 |
+
"""
|
| 111 |
+
if next_token_logits.dim() != 2:
|
| 112 |
+
raise LetterLogitError("expected next-token logits of shape [batch, vocab]")
|
| 113 |
+
|
| 114 |
+
import torch
|
| 115 |
+
|
| 116 |
+
ids = (
|
| 117 |
+
letter_token_ids
|
| 118 |
+
if isinstance(letter_token_ids, torch.Tensor)
|
| 119 |
+
else torch.tensor(list(letter_token_ids.values()), device=next_token_logits.device)
|
| 120 |
+
)
|
| 121 |
+
return next_token_logits.index_select(1, ids)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def batch_letter_logits(
|
| 125 |
+
gathered_logits, letters: tuple[str, ...] | dict[str, int] = LETTERS
|
| 126 |
+
) -> list[dict[str, float]]:
|
| 127 |
+
"""Convert a previously gathered ``[batch, 26]`` A-Z tensor to dictionaries."""
|
| 128 |
+
# Preserve the original public helper signature for downstream callers while
|
| 129 |
+
# allowing the engine to pass the already-gathered slice.
|
| 130 |
+
if isinstance(letters, dict):
|
| 131 |
+
gathered_logits = gather_letter_logits(gathered_logits, letters)
|
| 132 |
+
letters = tuple(letters)
|
| 133 |
+
if gathered_logits.dim() != 2 or gathered_logits.shape[1] != len(letters):
|
| 134 |
+
raise LetterLogitError("expected gathered letter logits of shape [batch, 26]")
|
| 135 |
+
return [
|
| 136 |
+
{letter: float(value) for letter, value in zip(letters, row)}
|
| 137 |
+
for row in gathered_logits
|
| 138 |
+
]
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8cc96bd25caeb0cccae009757983a7a4cba3b0866b55915c54b903149d0066d8
|
| 3 |
+
size 23919749176
|
modeling_truetype.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gemma 4 with a 26-logit A-Z output layer instead of the 262k-token LM head.
|
| 2 |
+
|
| 3 |
+
The decoder is unchanged. Only the final projection differs: ``lm_head`` has 26
|
| 4 |
+
rows (the A-Z rows of the original tied embedding), so one forward pass returns
|
| 5 |
+
one logit per letter and the vocabulary-sized matmul disappears. Gemma's final
|
| 6 |
+
logit soft-capping still runs on those 26 values, exactly as it did on the
|
| 7 |
+
original rows.
|
| 8 |
+
|
| 9 |
+
model = AutoModelForMultimodalLM.from_pretrained(repo, trust_remote_code=True)
|
| 10 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 11 |
+
result = model.system_one(tokenizer, state="...", questions={...})
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from torch import nn
|
| 20 |
+
from transformers.conversion_mapping import (
|
| 21 |
+
get_checkpoint_conversion_mapping,
|
| 22 |
+
register_checkpoint_conversion_mapping,
|
| 23 |
+
)
|
| 24 |
+
from transformers.models.gemma4_unified.modeling_gemma4_unified import (
|
| 25 |
+
Gemma4UnifiedForConditionalGeneration,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
from .configuration_truetype import LETTERS, TrueTypeSystemOneConfig
|
| 29 |
+
from .questions import build_question, score_question_tensor
|
| 30 |
+
from .render import render_batch
|
| 31 |
+
|
| 32 |
+
DEFAULT_TEMPERATURE = 0.7
|
| 33 |
+
|
| 34 |
+
# Gemma's checkpoints keep the vision embedder under legacy names that
|
| 35 |
+
# transformers renames per model_type. This model shares that layout.
|
| 36 |
+
register_checkpoint_conversion_mapping(
|
| 37 |
+
TrueTypeSystemOneConfig.model_type, get_checkpoint_conversion_mapping("gemma4_unified"), overwrite=True
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class TrueTypeSystemOneModel(Gemma4UnifiedForConditionalGeneration):
|
| 42 |
+
config_class = TrueTypeSystemOneConfig
|
| 43 |
+
_tied_weights_keys = {}
|
| 44 |
+
|
| 45 |
+
def __init__(self, config: TrueTypeSystemOneConfig):
|
| 46 |
+
super().__init__(config)
|
| 47 |
+
self.lm_head = nn.Linear(config.text_config.hidden_size, len(LETTERS), bias=False)
|
| 48 |
+
self.post_init()
|
| 49 |
+
|
| 50 |
+
def letter_logits(self, input_ids, attention_mask=None, **kwargs) -> torch.Tensor:
|
| 51 |
+
"""``[batch, 26]`` float32 logits for A-Z at the next-token position.
|
| 52 |
+
|
| 53 |
+
Inputs must be left-padded so the last position is the answer slot.
|
| 54 |
+
"""
|
| 55 |
+
out = self(
|
| 56 |
+
input_ids=input_ids,
|
| 57 |
+
attention_mask=attention_mask,
|
| 58 |
+
logits_to_keep=1,
|
| 59 |
+
use_cache=False,
|
| 60 |
+
**kwargs,
|
| 61 |
+
)
|
| 62 |
+
return out.logits[:, -1, :].float()
|
| 63 |
+
|
| 64 |
+
@torch.inference_mode()
|
| 65 |
+
def system_one(
|
| 66 |
+
self,
|
| 67 |
+
tokenizer,
|
| 68 |
+
state,
|
| 69 |
+
questions: dict[str, dict],
|
| 70 |
+
temperature: float = DEFAULT_TEMPERATURE,
|
| 71 |
+
batch_size: int = 16,
|
| 72 |
+
include_letter_logits: bool = False,
|
| 73 |
+
) -> dict:
|
| 74 |
+
"""Answer typed questions (``noul``, ``choice``, ``score``) about ``state``.
|
| 75 |
+
|
| 76 |
+
``questions`` maps an id to ``{"type", "instructions", "criteria"}``.
|
| 77 |
+
Returns ``{"answers": {id: {...probabilities and answer...}}}``.
|
| 78 |
+
"""
|
| 79 |
+
if not questions:
|
| 80 |
+
raise ValueError("at least one question is required")
|
| 81 |
+
state_text = state if isinstance(state, str) else json.dumps(state, indent=2)
|
| 82 |
+
parsed = [build_question(qid, spec) for qid, spec in questions.items()]
|
| 83 |
+
prompts = [item.text for item in render_batch(parsed, state_text)]
|
| 84 |
+
|
| 85 |
+
rows = []
|
| 86 |
+
padding_side, tokenizer.padding_side = tokenizer.padding_side, "left"
|
| 87 |
+
try:
|
| 88 |
+
for start in range(0, len(prompts), max(1, batch_size)):
|
| 89 |
+
encoded = tokenizer(
|
| 90 |
+
prompts[start : start + batch_size],
|
| 91 |
+
return_tensors="pt",
|
| 92 |
+
padding=True,
|
| 93 |
+
add_special_tokens=True,
|
| 94 |
+
).to(self.device)
|
| 95 |
+
rows.extend(self.letter_logits(encoded["input_ids"], encoded["attention_mask"]))
|
| 96 |
+
finally:
|
| 97 |
+
tokenizer.padding_side = padding_side
|
| 98 |
+
|
| 99 |
+
result = {"answers": {}}
|
| 100 |
+
for question, row in zip(parsed, rows):
|
| 101 |
+
result["answers"][question.id] = score_question_tensor(
|
| 102 |
+
question, row, temperature=temperature
|
| 103 |
+
).to_dict()
|
| 104 |
+
if include_letter_logits:
|
| 105 |
+
result["letter_logits"] = {
|
| 106 |
+
question.id: dict(zip(LETTERS, row.tolist())) for question, row in zip(parsed, rows)
|
| 107 |
+
}
|
| 108 |
+
return result
|
pipeline_truetype.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``pipeline("system-one", ...)``: typed questions in, probabilities and answers out.
|
| 2 |
+
|
| 3 |
+
pipe = pipeline("system-one", model=repo, trust_remote_code=True)
|
| 4 |
+
pipe({"state": "I was charged twice.",
|
| 5 |
+
"questions": {"refund": {"type": "noul", "instructions": "Does the text request a refund?"}}})
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from transformers import Pipeline
|
| 14 |
+
|
| 15 |
+
from .configuration_truetype import LETTERS
|
| 16 |
+
from .questions import build_question, score_question_tensor
|
| 17 |
+
from .render import render_batch
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class SystemOnePipeline(Pipeline):
|
| 21 |
+
def _sanitize_parameters(self, temperature=None, include_letter_logits=None, **kwargs):
|
| 22 |
+
postprocess = {}
|
| 23 |
+
if temperature is not None:
|
| 24 |
+
postprocess["temperature"] = temperature
|
| 25 |
+
if include_letter_logits is not None:
|
| 26 |
+
postprocess["include_letter_logits"] = include_letter_logits
|
| 27 |
+
return {}, {}, postprocess
|
| 28 |
+
|
| 29 |
+
def preprocess(self, inputs):
|
| 30 |
+
state, questions = inputs["state"], inputs["questions"]
|
| 31 |
+
if not questions:
|
| 32 |
+
raise ValueError("at least one question is required")
|
| 33 |
+
state_text = state if isinstance(state, str) else json.dumps(state, indent=2)
|
| 34 |
+
parsed = [build_question(qid, spec) for qid, spec in questions.items()]
|
| 35 |
+
prompts = [item.text for item in render_batch(parsed, state_text)]
|
| 36 |
+
self.tokenizer.padding_side = "left"
|
| 37 |
+
encoded = self.tokenizer(prompts, return_tensors="pt", padding=True, add_special_tokens=True)
|
| 38 |
+
return {"questions": parsed, "input_ids": encoded["input_ids"], "attention_mask": encoded["attention_mask"]}
|
| 39 |
+
|
| 40 |
+
def _forward(self, model_inputs):
|
| 41 |
+
with torch.inference_mode():
|
| 42 |
+
logits = self.model.letter_logits(
|
| 43 |
+
model_inputs["input_ids"].to(self.model.device),
|
| 44 |
+
model_inputs["attention_mask"].to(self.model.device),
|
| 45 |
+
)
|
| 46 |
+
return {"questions": model_inputs["questions"], "logits": logits}
|
| 47 |
+
|
| 48 |
+
def postprocess(self, model_outputs, temperature=0.7, include_letter_logits=False):
|
| 49 |
+
questions, logits = model_outputs["questions"], model_outputs["logits"]
|
| 50 |
+
result = {
|
| 51 |
+
"answers": {
|
| 52 |
+
q.id: score_question_tensor(q, row, temperature=temperature).to_dict()
|
| 53 |
+
for q, row in zip(questions, logits)
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
if include_letter_logits:
|
| 57 |
+
result["letter_logits"] = {q.id: dict(zip(LETTERS, row.tolist())) for q, row in zip(questions, logits)}
|
| 58 |
+
return result
|
questions.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Map Gemma letter logits onto TypeSafe's three question primitives.
|
| 2 |
+
|
| 3 |
+
The letter distribution is the model's raw output. Each primitive renormalizes
|
| 4 |
+
the distribution over its legal answer letters, so the probabilities sum to 1
|
| 5 |
+
across the caller's options.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
|
| 13 |
+
from .letters import LETTER_INDEX, LETTERS, _softmax
|
| 14 |
+
|
| 15 |
+
# Fixed bank of question-agnostic demonstrations. Their predicates differ from the
|
| 16 |
+
# target question on purpose: they teach the letter-slot format, not the task.
|
| 17 |
+
GENERIC_EXAMPLES: tuple[tuple[str, str, str], ...] = (
|
| 18 |
+
("I need this fixed immediately, it is blocking everything.", "Does the text express urgency?", "Y"),
|
| 19 |
+
("Here is the documentation link for reference.", "Does the text express urgency?", "N"),
|
| 20 |
+
("The app crashes when I open the settings page.", "Does the text contain a bug report?", "Y"),
|
| 21 |
+
("Just wanted to share the meeting notes from today.", "Does the text contain a bug report?", "N"),
|
| 22 |
+
("Please send this to a human agent right now.", "Does the text ask for a human agent?", "Y"),
|
| 23 |
+
("No rush on this, whenever you get a chance.", "Does the text ask for a human agent?", "N"),
|
| 24 |
+
("URGENT: the payment system is failing right now.", "Does the text express urgency?", "Y"),
|
| 25 |
+
("The sky is blue and the grass is green.", "Does the text express urgency?", "N"),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
DEFAULT_EXAMPLE_COUNT = 6
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass(frozen=True)
|
| 32 |
+
class QuestionOption:
|
| 33 |
+
letter: str
|
| 34 |
+
label: str
|
| 35 |
+
description: str | None = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class Question:
|
| 40 |
+
id: str
|
| 41 |
+
type: str # noul | choice | score
|
| 42 |
+
instructions: str
|
| 43 |
+
options: list[QuestionOption] = field(default_factory=list)
|
| 44 |
+
criteria: object = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass(frozen=True)
|
| 48 |
+
class QuestionAnswer:
|
| 49 |
+
type: str
|
| 50 |
+
choice: str | None = None
|
| 51 |
+
probabilities: dict[str, float] = field(default_factory=dict)
|
| 52 |
+
confidence: float | None = None
|
| 53 |
+
noul: float | None = None
|
| 54 |
+
score: float | None = None
|
| 55 |
+
legend: dict[str, str] | None = None
|
| 56 |
+
|
| 57 |
+
def to_dict(self) -> dict:
|
| 58 |
+
payload: dict = {"type": self.type}
|
| 59 |
+
if self.type == "noul":
|
| 60 |
+
payload["noul"] = round(self.noul or 0.0, 6)
|
| 61 |
+
elif self.type == "choice":
|
| 62 |
+
payload["choice"] = self.choice
|
| 63 |
+
payload["probabilities"] = {k: round(v, 6) for k, v in self.probabilities.items()}
|
| 64 |
+
payload["confidence"] = round(self.confidence or 0.0, 6)
|
| 65 |
+
elif self.type == "score":
|
| 66 |
+
payload["score"] = round(self.score or 0.0, 6)
|
| 67 |
+
payload["legend"] = self.legend or {}
|
| 68 |
+
payload["probabilities"] = {k: round(v, 6) for k, v in self.probabilities.items()}
|
| 69 |
+
payload["confidence"] = round(self.confidence or 0.0, 6)
|
| 70 |
+
return payload
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def confidence_from_probabilities(probabilities: list[float]) -> float:
|
| 74 |
+
"""1.0 when all mass is on one option, 0.0 when uniform."""
|
| 75 |
+
n = len(probabilities)
|
| 76 |
+
if n <= 1:
|
| 77 |
+
return 1.0
|
| 78 |
+
entropy = -sum(p * math.log(p) for p in probabilities if p > 0)
|
| 79 |
+
return max(0.0, 1.0 - entropy / math.log(n))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _predicate_forms(instructions: str) -> tuple[str, str]:
|
| 83 |
+
"""Turn an instruction into affirmative/negative option text.
|
| 84 |
+
|
| 85 |
+
"Does the text request a refund?" -> ("Yes, the text requests a refund.",
|
| 86 |
+
"No, the text does not request a refund.")
|
| 87 |
+
"""
|
| 88 |
+
text = instructions.strip().rstrip(".").strip()
|
| 89 |
+
lowered = text.lower()
|
| 90 |
+
|
| 91 |
+
if lowered.startswith("does the text "):
|
| 92 |
+
body = text[len("Does the text ") :]
|
| 93 |
+
affirmative = f"the text {body}"
|
| 94 |
+
elif lowered.startswith("is the text "):
|
| 95 |
+
body = text[len("Is the text ") :]
|
| 96 |
+
affirmative = f"the text {body}"
|
| 97 |
+
elif lowered.startswith("is this "):
|
| 98 |
+
body = text[len("Is this ") :]
|
| 99 |
+
affirmative = f"this {body}"
|
| 100 |
+
elif lowered.startswith("does this "):
|
| 101 |
+
body = text[len("Does this ") :]
|
| 102 |
+
affirmative = f"this {body}"
|
| 103 |
+
elif lowered.startswith("does the "):
|
| 104 |
+
body = text[len("Does the ") :]
|
| 105 |
+
affirmative = f"the {body}"
|
| 106 |
+
elif lowered.startswith("is the "):
|
| 107 |
+
body = text[len("Is the ") :]
|
| 108 |
+
affirmative = f"the {body}"
|
| 109 |
+
elif lowered.startswith("did the "):
|
| 110 |
+
body = text[len("Did the ") :]
|
| 111 |
+
affirmative = f"the {body}"
|
| 112 |
+
elif lowered.startswith("can the "):
|
| 113 |
+
body = text[len("Can the ") :]
|
| 114 |
+
affirmative = f"the {body}"
|
| 115 |
+
else:
|
| 116 |
+
affirmative = text
|
| 117 |
+
|
| 118 |
+
affirmative = affirmative.rstrip("?").strip()
|
| 119 |
+
if affirmative.lower().startswith("the "):
|
| 120 |
+
negative = "the " + affirmative[4:].split(" ", 1)[0] + " does not" + (
|
| 121 |
+
" " + affirmative[4:].split(" ", 1)[1] if " " in affirmative[4:] else ""
|
| 122 |
+
)
|
| 123 |
+
else:
|
| 124 |
+
negative = f"it is not the case that {affirmative}"
|
| 125 |
+
return f"Yes, {affirmative}.", f"No, {negative}."
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def build_question(qid: str, spec: dict) -> Question:
|
| 129 |
+
"""Parse one TypeSafe question spec into a letter-addressable Question."""
|
| 130 |
+
qtype = spec.get("type")
|
| 131 |
+
if qtype not in {"noul", "choice", "score"}:
|
| 132 |
+
raise ValueError(f"question {qid!r} has unsupported type {qtype!r}")
|
| 133 |
+
|
| 134 |
+
instructions = spec.get("instructions")
|
| 135 |
+
if not isinstance(instructions, str) or not instructions.strip():
|
| 136 |
+
raise ValueError(f"question {qid!r} requires non-empty string instructions")
|
| 137 |
+
|
| 138 |
+
criteria = spec.get("criteria")
|
| 139 |
+
|
| 140 |
+
if qtype == "noul":
|
| 141 |
+
if isinstance(criteria, dict):
|
| 142 |
+
yes_desc = criteria.get("true")
|
| 143 |
+
no_desc = criteria.get("false")
|
| 144 |
+
else:
|
| 145 |
+
yes_desc = no_desc = None
|
| 146 |
+
yes_text, no_text = _predicate_forms(instructions)
|
| 147 |
+
return Question(
|
| 148 |
+
id=qid,
|
| 149 |
+
type=qtype,
|
| 150 |
+
instructions=instructions,
|
| 151 |
+
options=[
|
| 152 |
+
QuestionOption("Y", "yes", yes_desc or yes_text),
|
| 153 |
+
QuestionOption("N", "no", no_desc or no_text),
|
| 154 |
+
],
|
| 155 |
+
criteria=criteria,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
if qtype == "choice":
|
| 159 |
+
if not isinstance(criteria, dict) or not criteria:
|
| 160 |
+
raise ValueError(f"choice question {qid!r} requires a non-empty criteria map")
|
| 161 |
+
if len(criteria) > len(LETTERS):
|
| 162 |
+
raise ValueError(f"choice question {qid!r} supports at most 26 options")
|
| 163 |
+
entries = list(criteria.items())
|
| 164 |
+
else:
|
| 165 |
+
if not isinstance(criteria, list) or len(criteria) < 2:
|
| 166 |
+
raise ValueError(f"score question {qid!r} requires an ordered list of at least two levels")
|
| 167 |
+
if len(criteria) > len(LETTERS):
|
| 168 |
+
raise ValueError(f"score question {qid!r} supports at most 26 levels")
|
| 169 |
+
entries = [(f"level {i}", level) for i, level in enumerate(criteria)]
|
| 170 |
+
|
| 171 |
+
options = [
|
| 172 |
+
QuestionOption(
|
| 173 |
+
LETTERS[i],
|
| 174 |
+
label,
|
| 175 |
+
description if isinstance(description, str) else str(description),
|
| 176 |
+
)
|
| 177 |
+
for i, (label, description) in enumerate(entries)
|
| 178 |
+
]
|
| 179 |
+
return Question(id=qid, type=qtype, instructions=instructions, options=options, criteria=criteria)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def score_question(question: Question, letter_logits: dict[str, float], temperature: float = 1.0) -> QuestionAnswer:
|
| 183 |
+
"""Turn raw letter logits into a typed answer, renormalized over legal options."""
|
| 184 |
+
option_letters = [option.letter for option in question.options]
|
| 185 |
+
missing = [letter for letter in option_letters if letter not in letter_logits]
|
| 186 |
+
if missing:
|
| 187 |
+
raise ValueError(f"question {question.id!r} is missing logits for {missing}")
|
| 188 |
+
|
| 189 |
+
logits = [letter_logits[letter] for letter in option_letters]
|
| 190 |
+
probs = _softmax(logits, temperature)
|
| 191 |
+
|
| 192 |
+
if question.type == "noul":
|
| 193 |
+
noul = probs[0]
|
| 194 |
+
return QuestionAnswer(
|
| 195 |
+
type="noul",
|
| 196 |
+
noul=noul,
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
if question.type == "choice":
|
| 200 |
+
pairs = sorted(zip(question.options, probs), key=lambda op: op[1], reverse=True)
|
| 201 |
+
top = pairs[0]
|
| 202 |
+
return QuestionAnswer(
|
| 203 |
+
type="choice",
|
| 204 |
+
choice=top[0].label,
|
| 205 |
+
probabilities={option.label: p for option, p in pairs},
|
| 206 |
+
confidence=confidence_from_probabilities(probs),
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
score = sum(i * p for i, p in enumerate(probs))
|
| 210 |
+
return QuestionAnswer(
|
| 211 |
+
type="score",
|
| 212 |
+
score=score,
|
| 213 |
+
probabilities={str(i): p for i, p in enumerate(probs)},
|
| 214 |
+
legend={str(i): option.description or "" for i, option in enumerate(question.options)},
|
| 215 |
+
confidence=confidence_from_probabilities(probs),
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def score_question_tensor(question: Question, letter_logits, temperature: float = 1.0) -> QuestionAnswer:
|
| 220 |
+
"""Score a ``[26]`` device tensor, transferring only final answer values.
|
| 221 |
+
|
| 222 |
+
The logits, legal-option gather, softmax, entropy, ranking, and score
|
| 223 |
+
reduction all remain on the tensor's device. Constructing ``QuestionAnswer``
|
| 224 |
+
necessarily creates Python/JSON values at the API boundary.
|
| 225 |
+
"""
|
| 226 |
+
if temperature <= 0:
|
| 227 |
+
raise ValueError("temperature must be > 0")
|
| 228 |
+
if letter_logits.dim() != 1 or letter_logits.shape[0] != len(LETTERS):
|
| 229 |
+
raise ValueError("expected one [26] letter-logit tensor")
|
| 230 |
+
|
| 231 |
+
import torch
|
| 232 |
+
|
| 233 |
+
indices = torch.tensor(
|
| 234 |
+
[LETTER_INDEX[option.letter] for option in question.options],
|
| 235 |
+
device=letter_logits.device,
|
| 236 |
+
dtype=torch.long,
|
| 237 |
+
)
|
| 238 |
+
probs = torch.softmax(letter_logits.index_select(0, indices).float() / temperature, dim=0)
|
| 239 |
+
|
| 240 |
+
if question.type == "noul":
|
| 241 |
+
return QuestionAnswer(type="noul", noul=float(probs[0].item()))
|
| 242 |
+
|
| 243 |
+
if len(question.options) <= 1:
|
| 244 |
+
confidence_value = 1.0
|
| 245 |
+
else:
|
| 246 |
+
entropy = -(probs * torch.where(probs > 0, probs.log(), torch.zeros_like(probs))).sum()
|
| 247 |
+
confidence = 1.0 - entropy / torch.log(torch.tensor(float(len(question.options)), device=probs.device))
|
| 248 |
+
confidence_value = float(torch.clamp(confidence, min=0.0).item())
|
| 249 |
+
|
| 250 |
+
if question.type == "choice":
|
| 251 |
+
# Stable GPU argsort preserves the request's option order for exact ties.
|
| 252 |
+
order = torch.argsort(probs, descending=True, stable=True).tolist()
|
| 253 |
+
probability_values = probs.tolist()
|
| 254 |
+
return QuestionAnswer(
|
| 255 |
+
type="choice",
|
| 256 |
+
choice=question.options[order[0]].label,
|
| 257 |
+
probabilities={question.options[index].label: probability_values[index] for index in order},
|
| 258 |
+
confidence=confidence_value,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
probability_values = probs.tolist()
|
| 262 |
+
levels = torch.arange(len(question.options), device=probs.device, dtype=probs.dtype)
|
| 263 |
+
score = float((levels * probs).sum().item())
|
| 264 |
+
return QuestionAnswer(
|
| 265 |
+
type="score",
|
| 266 |
+
score=score,
|
| 267 |
+
probabilities={str(index): value for index, value in enumerate(probability_values)},
|
| 268 |
+
legend={str(index): option.description or "" for index, option in enumerate(question.options)},
|
| 269 |
+
confidence=confidence_value,
|
| 270 |
+
)
|
render.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render every question in a request to a base-completion prompt.
|
| 2 |
+
|
| 3 |
+
Prompt layout
|
| 4 |
+
-------------
|
| 5 |
+
Every block puts the answer-relevant scaffolding *first* and the text being
|
| 6 |
+
judged *last*, so a prompt is::
|
| 7 |
+
|
| 8 |
+
Choices:
|
| 9 |
+
Y. Yes, ...
|
| 10 |
+
N. No, ...
|
| 11 |
+
Question: <question>
|
| 12 |
+
Text: <example text>
|
| 13 |
+
Answer: Y
|
| 14 |
+
|
| 15 |
+
... more examples ...
|
| 16 |
+
|
| 17 |
+
Choices:
|
| 18 |
+
...
|
| 19 |
+
Question: <target question>
|
| 20 |
+
Text: <the caller's state>
|
| 21 |
+
Answer:
|
| 22 |
+
|
| 23 |
+
Putting the state last makes the engine's prefix KV cache effective:
|
| 24 |
+
everything above ``Text:`` depends only on the question, so it is byte-identical
|
| 25 |
+
across calls and can be cached once. That took a warm Doom decision from 357ms to
|
| 26 |
+
178ms (mean prompt tail 68 -> 20 tokens) with no accuracy change: 63/63 on the
|
| 27 |
+
50-case suite plus the Doom probe and 8 held-out noul cases.
|
| 28 |
+
|
| 29 |
+
Two example strategies, chosen by question type:
|
| 30 |
+
|
| 31 |
+
- Noul (Y/N) uses a fixed bank of demonstrations with their own questions.
|
| 32 |
+
The predicate in the options ("Yes, the text requests a refund.") carries the
|
| 33 |
+
task; the examples teach the letter slot. Their questions
|
| 34 |
+
differ from the target question, so their Y/N labels stay truthful rather than
|
| 35 |
+
being relabelled under a question they were not written for. This lifted
|
| 36 |
+
accuracy from 3/6 to 6/6 in tests/example_transfer.py.
|
| 37 |
+
|
| 38 |
+
- Choice and Score use examples generated from the caller's criteria. Each
|
| 39 |
+
option gets one synthetic demonstration mapping it to its letter, visited in a
|
| 40 |
+
non-alphabetical order so the model cannot exploit A,B,C positional cues. This
|
| 41 |
+
reached 10/10 on Choice and 11/12 on Score in tests/choice_score_bakeoff2.py and
|
| 42 |
+
needs no hand-authoring.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
from dataclasses import dataclass
|
| 48 |
+
|
| 49 |
+
from .questions import DEFAULT_EXAMPLE_COUNT, GENERIC_EXAMPLES, Question
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class RenderedPrompt:
|
| 54 |
+
question_id: str
|
| 55 |
+
text: str
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _render_options(question: Question) -> str:
|
| 59 |
+
lines = []
|
| 60 |
+
for option in question.options:
|
| 61 |
+
if question.type == "noul":
|
| 62 |
+
lines.append(f"{option.letter}. {option.description or option.label}")
|
| 63 |
+
elif option.description:
|
| 64 |
+
lines.append(f"{option.letter}. {option.label} - {option.description}")
|
| 65 |
+
else:
|
| 66 |
+
lines.append(f"{option.letter}. {option.label}")
|
| 67 |
+
return "\n".join(lines)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _header(question: Question, instructions: str | None = None) -> str:
|
| 71 |
+
"""Choices + question. Identical for every call on the same question."""
|
| 72 |
+
return (
|
| 73 |
+
f"Choices:\n{_render_options(question)}\n"
|
| 74 |
+
f"Question: {instructions or question.instructions}\n"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _noul_examples(question: Question, count: int) -> list[str]:
|
| 79 |
+
blocks = []
|
| 80 |
+
for ex_text, ex_question, ex_letter in GENERIC_EXAMPLES[:count]:
|
| 81 |
+
blocks.append(f"{_header(question, ex_question)}Text: {ex_text}\nAnswer: {ex_letter}")
|
| 82 |
+
return blocks
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _criteria_roundtrip_examples(question: Question) -> list[str]:
|
| 86 |
+
"""One demonstration per option, built from the option's own description.
|
| 87 |
+
|
| 88 |
+
Options use a non-alphabetical order that
|
| 89 |
+
interleaves the scale (low, high, middle, ...) so no positional shortcut exists.
|
| 90 |
+
"""
|
| 91 |
+
header = _header(question)
|
| 92 |
+
blocks = []
|
| 93 |
+
for index in _interleaved_order(len(question.options)):
|
| 94 |
+
option = question.options[index]
|
| 95 |
+
if question.type == "choice":
|
| 96 |
+
detail = f" This example is about the topic of {option.label}."
|
| 97 |
+
else:
|
| 98 |
+
detail = " " + (option.description or option.label)
|
| 99 |
+
blocks.append(
|
| 100 |
+
f"{header}Text: An example of {option.label}:{detail}\nAnswer: {option.letter}"
|
| 101 |
+
)
|
| 102 |
+
return blocks
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _interleaved_order(n: int) -> list[int]:
|
| 106 |
+
"""Low, high, then remaining indices ascending. Breaks A,B,C positional cues."""
|
| 107 |
+
if n <= 2:
|
| 108 |
+
return list(range(n))
|
| 109 |
+
order = [0, n - 1]
|
| 110 |
+
order.extend(i for i in range(1, n - 1))
|
| 111 |
+
return order
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def render_example_prefix(
|
| 115 |
+
question: Question,
|
| 116 |
+
*,
|
| 117 |
+
example_count: int = DEFAULT_EXAMPLE_COUNT,
|
| 118 |
+
) -> str:
|
| 119 |
+
"""The part of the prompt that depends only on the question, never on the state.
|
| 120 |
+
|
| 121 |
+
This is what the engine keeps in a KV cache across calls: the few-shot examples
|
| 122 |
+
plus the target question's own Choices/Question header. For a fixed question it
|
| 123 |
+
is byte-identical every time, and it is ~90% of the prompt.
|
| 124 |
+
"""
|
| 125 |
+
if question.type == "noul":
|
| 126 |
+
blocks = _noul_examples(question, example_count)
|
| 127 |
+
else:
|
| 128 |
+
blocks = _criteria_roundtrip_examples(question)
|
| 129 |
+
body = "\n\n".join(blocks) + "\n\n" if blocks else ""
|
| 130 |
+
return body + _header(question)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def render_target_block(question: Question, state: str) -> str:
|
| 134 |
+
"""The state-dependent tail, ending at the ``Answer:`` slot.
|
| 135 |
+
|
| 136 |
+
There is no trailing space after ``Answer:``. The letter tokens
|
| 137 |
+
the engine reads are space-prefixed (``"▁Y"``), which is how the examples above
|
| 138 |
+
tokenize (``['Answer', ':', '▁Y']``). Adding a trailing space here would emit a
|
| 139 |
+
standalone ``'��'`` token and strand the space, so the model would want a bare
|
| 140 |
+
``"Y"`` while the engine read ``"▁Y"``. This measured 0.0000 probability mass on
|
| 141 |
+
the letters being scored, versus 0.9871 without the space.
|
| 142 |
+
"""
|
| 143 |
+
return f"Text: {state}\nAnswer:"
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def render_question(
|
| 147 |
+
question: Question,
|
| 148 |
+
state: str,
|
| 149 |
+
*,
|
| 150 |
+
example_count: int = DEFAULT_EXAMPLE_COUNT,
|
| 151 |
+
) -> RenderedPrompt:
|
| 152 |
+
# Composed from the two halves so the cached-prefix path and the plain path
|
| 153 |
+
# always produce the exact same prompt string.
|
| 154 |
+
text = render_example_prefix(question, example_count=example_count) + render_target_block(
|
| 155 |
+
question, state
|
| 156 |
+
)
|
| 157 |
+
return RenderedPrompt(question_id=question.id, text=text)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def render_batch(
|
| 161 |
+
questions: list[Question],
|
| 162 |
+
state: str,
|
| 163 |
+
*,
|
| 164 |
+
example_count: int = DEFAULT_EXAMPLE_COUNT,
|
| 165 |
+
) -> list[RenderedPrompt]:
|
| 166 |
+
"""One prompt per question. Every question sees the same state, as in TypeSafe."""
|
| 167 |
+
return [
|
| 168 |
+
render_question(question, state, example_count=example_count) for question in questions
|
| 169 |
+
]
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:12bac982b793c44b03d52a250a9f0d0b666813da566b910c24a6da0695fd11e6
|
| 3 |
+
size 32170070
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_token": "<|audio|>",
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"boa_token": "<|audio>",
|
| 5 |
+
"boi_token": "<|image>",
|
| 6 |
+
"bos_token": "<bos>",
|
| 7 |
+
"eoa_token": "<audio|>",
|
| 8 |
+
"eoc_token": "<channel|>",
|
| 9 |
+
"eoi_token": "<image|>",
|
| 10 |
+
"eos_token": "<eos>",
|
| 11 |
+
"eot_token": "<turn|>",
|
| 12 |
+
"escape_token": "<|\"|>",
|
| 13 |
+
"etc_token": "<tool_call|>",
|
| 14 |
+
"etd_token": "<tool|>",
|
| 15 |
+
"etr_token": "<tool_response|>",
|
| 16 |
+
"extra_special_tokens": [
|
| 17 |
+
"<|video|>"
|
| 18 |
+
],
|
| 19 |
+
"image_token": "<|image|>",
|
| 20 |
+
"is_local": false,
|
| 21 |
+
"local_files_only": false,
|
| 22 |
+
"mask_token": "<mask>",
|
| 23 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 24 |
+
"model_specific_special_tokens": {
|
| 25 |
+
"audio_token": "<|audio|>",
|
| 26 |
+
"boa_token": "<|audio>",
|
| 27 |
+
"boi_token": "<|image>",
|
| 28 |
+
"eoa_token": "<audio|>",
|
| 29 |
+
"eoc_token": "<channel|>",
|
| 30 |
+
"eoi_token": "<image|>",
|
| 31 |
+
"eot_token": "<turn|>",
|
| 32 |
+
"escape_token": "<|\"|>",
|
| 33 |
+
"etc_token": "<tool_call|>",
|
| 34 |
+
"etd_token": "<tool|>",
|
| 35 |
+
"etr_token": "<tool_response|>",
|
| 36 |
+
"image_token": "<|image|>",
|
| 37 |
+
"soc_token": "<|channel>",
|
| 38 |
+
"sot_token": "<|turn>",
|
| 39 |
+
"stc_token": "<|tool_call>",
|
| 40 |
+
"std_token": "<|tool>",
|
| 41 |
+
"str_token": "<|tool_response>",
|
| 42 |
+
"think_token": "<|think|>"
|
| 43 |
+
},
|
| 44 |
+
"pad_token": "<pad>",
|
| 45 |
+
"padding_side": "left",
|
| 46 |
+
"processor_class": "Gemma4UnifiedProcessor",
|
| 47 |
+
"soc_token": "<|channel>",
|
| 48 |
+
"sot_token": "<|turn>",
|
| 49 |
+
"stc_token": "<|tool_call>",
|
| 50 |
+
"std_token": "<|tool>",
|
| 51 |
+
"str_token": "<|tool_response>",
|
| 52 |
+
"think_token": "<|think|>",
|
| 53 |
+
"tokenizer_class": "GemmaTokenizer",
|
| 54 |
+
"unk_token": "<unk>"
|
| 55 |
+
}
|