Instructions to use MeTask-Rain/MetaDecision-31B-Rain with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use MeTask-Rain/MetaDecision-31B-Rain with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Serving package (same as GitHub chenyuwang166-oss/MetaDecision-31B-Rain@6b8ed2d0)
Browse files- service/.gitattributes +1 -0
- service/.gitignore +5 -0
- service/ADAPTER_MANIFEST.json +134 -0
- service/DISCLOSURE.md +81 -0
- service/FREEZE.json +54 -0
- service/LICENSE +25 -0
- service/MANIFEST.sha256 +32 -0
- service/MODEL_CARD.md +53 -0
- service/MODEL_MANIFEST.json +117 -0
- service/PIPELINE_PROVENANCE.json +145 -0
- service/README.md +164 -0
- service/SERVICE_LOCK.json +48 -0
- service/attention.py +37 -0
- service/backends.py +691 -0
- service/bench.py +795 -0
- service/calibration.json +136 -0
- service/config.json +346 -0
- service/core.py +1197 -0
- service/fit_report.json +246 -0
- service/item_deadline.py +551 -0
- service/launch.py +456 -0
- service/model_setup.py +14 -0
- service/pkg_check.py +455 -0
- service/post_calibration.py +212 -0
- service/prepare_model.sh +24 -0
- service/quota.py +454 -0
- service/reconcile_usage.py +156 -0
- service/requirements.lock +204 -0
- service/requirements.measured.lock +201 -0
- service/selftest.py +286 -0
- service/selftest.sh +6 -0
- service/serve.sh +9 -0
- service/server.py +242 -0
service/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
* -text
|
service/.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
runs/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
state/
|
| 5 |
+
*.log
|
service/ADAPTER_MANIFEST.json
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_config_normalization": {
|
| 3 |
+
"changed_fields": [
|
| 4 |
+
"base_model_name_or_path",
|
| 5 |
+
"revision"
|
| 6 |
+
],
|
| 7 |
+
"rule": "Only these keys differ from the selected checkpoint adapter_config.json.",
|
| 8 |
+
"serving_values": {
|
| 9 |
+
"base_model_name_or_path": "google/gemma-4-31B-it-qat-q4_0-unquantized",
|
| 10 |
+
"revision": "1e4d8beecacb8b7590c1d8bedd7335f687bf311f"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"files": {
|
| 14 |
+
"adapter_config.json": {
|
| 15 |
+
"sha256": "140bb00c832d117f57d29650dabe50e992b2e5064e908aaa29230b3ce19ca2c4",
|
| 16 |
+
"size": 22953
|
| 17 |
+
},
|
| 18 |
+
"adapter_model.safetensors": {
|
| 19 |
+
"sha256": "2b9568998ffed5d133223ffdff03056be297285c643122db4d45365d0bd86970",
|
| 20 |
+
"size": 494595208
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"key_value_alignment": {
|
| 24 |
+
"added_count": 20,
|
| 25 |
+
"added_tensors": [
|
| 26 |
+
"base_model.model.model.language_model.layers.11.self_attn.v_proj.lora_A.weight",
|
| 27 |
+
"base_model.model.model.language_model.layers.11.self_attn.v_proj.lora_B.weight",
|
| 28 |
+
"base_model.model.model.language_model.layers.17.self_attn.v_proj.lora_A.weight",
|
| 29 |
+
"base_model.model.model.language_model.layers.17.self_attn.v_proj.lora_B.weight",
|
| 30 |
+
"base_model.model.model.language_model.layers.23.self_attn.v_proj.lora_A.weight",
|
| 31 |
+
"base_model.model.model.language_model.layers.23.self_attn.v_proj.lora_B.weight",
|
| 32 |
+
"base_model.model.model.language_model.layers.29.self_attn.v_proj.lora_A.weight",
|
| 33 |
+
"base_model.model.model.language_model.layers.29.self_attn.v_proj.lora_B.weight",
|
| 34 |
+
"base_model.model.model.language_model.layers.35.self_attn.v_proj.lora_A.weight",
|
| 35 |
+
"base_model.model.model.language_model.layers.35.self_attn.v_proj.lora_B.weight",
|
| 36 |
+
"base_model.model.model.language_model.layers.41.self_attn.v_proj.lora_A.weight",
|
| 37 |
+
"base_model.model.model.language_model.layers.41.self_attn.v_proj.lora_B.weight",
|
| 38 |
+
"base_model.model.model.language_model.layers.47.self_attn.v_proj.lora_A.weight",
|
| 39 |
+
"base_model.model.model.language_model.layers.47.self_attn.v_proj.lora_B.weight",
|
| 40 |
+
"base_model.model.model.language_model.layers.5.self_attn.v_proj.lora_A.weight",
|
| 41 |
+
"base_model.model.model.language_model.layers.5.self_attn.v_proj.lora_B.weight",
|
| 42 |
+
"base_model.model.model.language_model.layers.53.self_attn.v_proj.lora_A.weight",
|
| 43 |
+
"base_model.model.model.language_model.layers.53.self_attn.v_proj.lora_B.weight",
|
| 44 |
+
"base_model.model.model.language_model.layers.59.self_attn.v_proj.lora_A.weight",
|
| 45 |
+
"base_model.model.model.language_model.layers.59.self_attn.v_proj.lora_B.weight"
|
| 46 |
+
],
|
| 47 |
+
"applied": true,
|
| 48 |
+
"layers": [
|
| 49 |
+
5,
|
| 50 |
+
11,
|
| 51 |
+
17,
|
| 52 |
+
23,
|
| 53 |
+
29,
|
| 54 |
+
35,
|
| 55 |
+
41,
|
| 56 |
+
47,
|
| 57 |
+
53,
|
| 58 |
+
59
|
| 59 |
+
],
|
| 60 |
+
"rule": "v_proj.lora_A/B = byte copy of k_proj.lora_A/B in every attention_k_eq_v layer; all source tensors and their data unchanged"
|
| 61 |
+
},
|
| 62 |
+
"kind": "unmerged_lora_serving_adapter",
|
| 63 |
+
"lora": {
|
| 64 |
+
"lora_alpha": 32,
|
| 65 |
+
"merged": false,
|
| 66 |
+
"peft_type": "LORA",
|
| 67 |
+
"r": 16,
|
| 68 |
+
"stored_tensors": {
|
| 69 |
+
"dtypes": {
|
| 70 |
+
"F32": 840
|
| 71 |
+
},
|
| 72 |
+
"k_eq_v_layers_without_v_proj_lora": [],
|
| 73 |
+
"layers": 60,
|
| 74 |
+
"module_count": 420,
|
| 75 |
+
"module_prefix": "model.language_model.",
|
| 76 |
+
"rank": 16,
|
| 77 |
+
"targets": [
|
| 78 |
+
"down_proj",
|
| 79 |
+
"gate_proj",
|
| 80 |
+
"k_proj",
|
| 81 |
+
"o_proj",
|
| 82 |
+
"q_proj",
|
| 83 |
+
"up_proj",
|
| 84 |
+
"v_proj"
|
| 85 |
+
],
|
| 86 |
+
"tensor_count": 840,
|
| 87 |
+
"v_proj_copied_from_k_proj_layers": [
|
| 88 |
+
5,
|
| 89 |
+
11,
|
| 90 |
+
17,
|
| 91 |
+
23,
|
| 92 |
+
29,
|
| 93 |
+
35,
|
| 94 |
+
41,
|
| 95 |
+
47,
|
| 96 |
+
53,
|
| 97 |
+
59
|
| 98 |
+
],
|
| 99 |
+
"variant": "key_value_aligned"
|
| 100 |
+
},
|
| 101 |
+
"target_modules": [
|
| 102 |
+
"q_proj",
|
| 103 |
+
"k_proj",
|
| 104 |
+
"v_proj",
|
| 105 |
+
"o_proj",
|
| 106 |
+
"gate_proj",
|
| 107 |
+
"up_proj",
|
| 108 |
+
"down_proj"
|
| 109 |
+
]
|
| 110 |
+
},
|
| 111 |
+
"protocol": "adapter_arm_31b_v1",
|
| 112 |
+
"schema_version": 1,
|
| 113 |
+
"served_base": {
|
| 114 |
+
"files_byte_identical": true,
|
| 115 |
+
"model_sha256": "ca750c75ea690276947dfc6d3c775668282e9180e4c56f180bfad912fed606a5",
|
| 116 |
+
"repo_id": "google/gemma-4-31B-it-qat-w4a16-ct",
|
| 117 |
+
"revision": "52f3f65bc7a02d555763bc923bd1d9094898219d"
|
| 118 |
+
},
|
| 119 |
+
"source": {
|
| 120 |
+
"adapter_model_sha256": "9dbf1acc152ef1a327b934b9674c232eec1129bee0fad51707021ad16285fc27",
|
| 121 |
+
"checkpoint_adapter_config_sha256": "52553a60f6155af2707d05d11d17a4e0a3d20c35233ea9261fdc56a7e7b4c4ca",
|
| 122 |
+
"checkpoint_complete_sha256": "056e81fa6b71062de77b6a640ffdb91c3680a16ffeb0154a6a10cf58b4c325fb",
|
| 123 |
+
"checkpoint_step": 150,
|
| 124 |
+
"run_identity_sha256": "7c94956f79b06b60a1066ebbf094e346ade3c70e9b900888563d878b2850b2b3",
|
| 125 |
+
"selection_sha256": "621fde9159b24e773298511418d3373dcd2da45bed535327c5be01facf218994",
|
| 126 |
+
"training_manifest_sha256": "ced7511dd24f895ad18a65616736ddbad4a4a906c3c015879e7d8148a49fa879"
|
| 127 |
+
},
|
| 128 |
+
"subdir": "adapter",
|
| 129 |
+
"training_base": {
|
| 130 |
+
"repo_id": "google/gemma-4-31B-it-qat-q4_0-unquantized",
|
| 131 |
+
"revision": "1e4d8beecacb8b7590c1d8bedd7335f687bf311f"
|
| 132 |
+
},
|
| 133 |
+
"variant": "key_value_aligned"
|
| 134 |
+
}
|
service/DISCLOSURE.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MetaDecision-31B-Rain: disclosure
|
| 2 |
+
|
| 3 |
+
## Model
|
| 4 |
+
|
| 5 |
+
- Base model: `google/gemma-4-31B-it`. The weights used are Google's official
|
| 6 |
+
quantization-aware-training (QAT) releases of this model:
|
| 7 |
+
- training base: `google/gemma-4-31B-it-qat-q4_0-unquantized` at revision
|
| 8 |
+
`1e4d8beecacb8b7590c1d8bedd7335f687bf311f` (BF16);
|
| 9 |
+
- served base: `google/gemma-4-31B-it-qat-w4a16-ct` at revision
|
| 10 |
+
`52f3f65bc7a02d555763bc923bd1d9094898219d`, Google's compressed-tensors W4A16 checkpoint
|
| 11 |
+
(4-bit weights, group size 32, symmetric; activations unquantized), used unchanged. We did not
|
| 12 |
+
quantize anything.
|
| 13 |
+
- Own trained weights: the LoRA adapter in `adapter/` (PEFT format), rank 16, alpha 32, on the
|
| 14 |
+
language model's q/k/v/o/gate/up/down projections, trained by us in BF16 on the training base.
|
| 15 |
+
- The adapter is not merged: vLLM 0.31.0 loads it with `enable_lora` and `max_lora_rank=16` and
|
| 16 |
+
applies it to every request on top of the W4A16 base.
|
| 17 |
+
- In the 10 full-attention layers (5, 11, …, 59) Gemma 4 computes the attention values with the key
|
| 18 |
+
projection (`attention_k_eq_v`); these layers have no v_proj, and vLLM serves them with separate
|
| 19 |
+
key and value slots. `adapter/adapter_model.safetensors` therefore contains the trained checkpoint
|
| 20 |
+
tensors unchanged plus, for these 10 layers, a v_proj LoRA that is a byte copy of the trained
|
| 21 |
+
k_proj LoRA (20 tensors), so that the update applies to keys and values as in training.
|
| 22 |
+
- The base files and the adapter are exactly those of `MeTask-Rain/metask-jev-rain-31B-trained`
|
| 23 |
+
(same SHA-256 for every file, `MODEL_MANIFEST.json`); no weight was trained or changed for this
|
| 24 |
+
entry. It differs from that entry only in the serving rules below.
|
| 25 |
+
|
| 26 |
+
## Data
|
| 27 |
+
|
| 28 |
+
- The adapter is the one of `MeTask-Rain/metask-jev-rain-31B-trained`, trained on our own generated
|
| 29 |
+
data as disclosed for that entry.
|
| 30 |
+
- Inherited from the earlier service of these weights, unchanged: the per-type temperatures of the
|
| 31 |
+
first calibration step, fitted on the 231 public JevBench items, and the drafting prompt limit
|
| 32 |
+
(840 tokens, below), chosen using the 231 public items together with our own development data.
|
| 33 |
+
Fits and choices made on these public items are in-sample. The yes/no confidence constant 0.898
|
| 34 |
+
(first-step answers carry 0.898 or 0.102) is the yes/no accuracy of our own held-out development
|
| 35 |
+
data (1,419 yes/no items), rounded to 3 decimals; its value does not depend on any leaderboard
|
| 36 |
+
result.
|
| 37 |
+
- The serving parameters new in this entry (drafting threshold and share, draft length, the re-read
|
| 38 |
+
rule and the post-calibration below) were fitted on our own independently built development set.
|
| 39 |
+
The 9 s deadline and the drafting chunk settings are fixed rules, not fitted.
|
| 40 |
+
- The 231 public items were also used for the validation run of the workflow in README.md; no
|
| 41 |
+
parameter of this entry was chosen from that run.
|
| 42 |
+
|
| 43 |
+
## Serving
|
| 44 |
+
|
| 45 |
+
- Each decision is first read in one prompt pass: the answer distribution comes from the next-token
|
| 46 |
+
log-probabilities of the answer forms. If the gap between the two highest answer scores of this
|
| 47 |
+
pass is below a threshold (fitted so that 52.1% of the decisions of our development set
|
| 48 |
+
that can take the drafting pass fall below it), the decision may take a second, drafting pass: the
|
| 49 |
+
model writes at most 192 tokens of reasoning (the instruction asks for at most
|
| 50 |
+
50 words), then the answer distribution is read again. If the draft ends without an
|
| 51 |
+
answer line, an answer line is appended and the answer is read after the draft.
|
| 52 |
+
- Deadline: a decision that takes the drafting pass has a deadline of 9 s from the
|
| 53 |
+
moment the service starts answering the request. Drafting runs in chunks sized so that the answer
|
| 54 |
+
can still be read before the deadline (an engine call in progress is always completed, never
|
| 55 |
+
abandoned); a draft that reaches its time budget is ended with an answer line, and a drafting pass
|
| 56 |
+
that cannot be read in time returns the first-pass answer (with a `warning` note, also listed in
|
| 57 |
+
`partial_errors`).
|
| 58 |
+
- Decisions whose drafting prompt (the first-pass prompt plus the drafting instruction) is
|
| 59 |
+
longer than 840 tokens are answered in one pass. Inputs longer than 6,653 tokens never take the drafting
|
| 60 |
+
pass and are answered from a context shortened to at most 6,653 tokens by the service's existing
|
| 61 |
+
extraction rule; an input is not refused because of its length.
|
| 62 |
+
- Drafting share (slow-path quota 49%): a counter kept below the service's state
|
| 63 |
+
directory admits a drafting pass only while drafting decisions stay
|
| 64 |
+
at most 49% of all decisions so far.
|
| 65 |
+
- Re-read: a choice decision answered in one pass although its length allowed the drafting pass (for example because the drafting share or the cost fuse refused it), whose first-pass margin (the log-score difference between its two highest answers) is below 4, is read once more in one pass with the options in reverse order (each option keeps its text and label). The two answer distributions are averaged by label. The second read is included in the decision's token usage and in the cost fuse; it is not a drafting pass and does not count toward the drafting share.
|
| 66 |
+
- Cost fuse: from the 101st decision on, when the running engine cost per decision reaches 90% of
|
| 67 |
+
USD 0.0646 per 1,000 decisions, drafting passes are limited to the share that brings the running
|
| 68 |
+
cost back to 85% of that limit (none while first-pass decisions alone cost more than 85% of it),
|
| 69 |
+
until the running cost falls to 85% of the limit. The fuse prices engine token counts at
|
| 70 |
+
USD 0.0598 per million tokens, input and output alike (the GPU-time basis of the
|
| 71 |
+
leaderboard's cost rule for this architecture). The fuse never changes the first pass.
|
| 72 |
+
- Answers after a drafting pass use the same first-step calibration parameters (per-type
|
| 73 |
+
temperatures and yes/no confidence) as first-pass answers.
|
| 74 |
+
- Post-calibration: after the first-step calibration, the reported probabilities are transformed per type, without any model call or token (a temperature t replaces the probabilities p by p^(1/t), renormalised; parameters fitted on our own independently built development set; values in `config.json`, block `post_calibration`). yes/no: temperature 0.5 (the first-step 0.898/0.102 become 0.987/0.013); score: all probability mass is put on the most probable level. The answer label is recomputed from the transformed probabilities; types not listed are unchanged.
|
| 75 |
+
|
| 76 |
+
## Run conditions and usage accounting
|
| 77 |
+
|
| 78 |
+
- vLLM 0.31.0 with `VLLM_BATCH_INVARIANT=1` (also set by the launcher for serving) and
|
| 79 |
+
`VLLM_USE_FLASHINFER_SAMPLER=0`, one request at a time; model loading and warm-up are excluded.
|
| 80 |
+
- Cost basis: measured engine token counts (input tokens minus cached prefix tokens; generated
|
| 81 |
+
tokens).
|
service/FREEZE.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"config": {
|
| 3 |
+
"change": "package.finalized: false -> true (single textual replacement)",
|
| 4 |
+
"frozen_sha256": "94d5765076019628a0dc736f79e470eefeea72661895599ea2670ef391b0fed0",
|
| 5 |
+
"prepared_sha256": "0c84b933ac60e43394dbd748da21cb27bf91fe640cac7674acc3d02b80e8de11"
|
| 6 |
+
},
|
| 7 |
+
"frozen_utc": "2026-10-30T20:10:25Z",
|
| 8 |
+
"model_identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 9 |
+
"prepared_files": {
|
| 10 |
+
".gitattributes": "705fd4d6451a31d36b3df7de96f83f30ac976c9b4a6d1e51671d8e2f33e2d0da",
|
| 11 |
+
".gitignore": "4254e460a0ff807e7fd68e104efa8935a769377f09876b8b4fec0fb0b544a0e6",
|
| 12 |
+
"ADAPTER_MANIFEST.json": "6c40a7534f5d2b87cc8591748f85c7958f0259821c98659a60e25308904aa430",
|
| 13 |
+
"DISCLOSURE.md": "f50f14b50c9399ebf1cbf8867a8f807374448e0fbc9218dd3f3cd4a350a36135",
|
| 14 |
+
"LICENSE": "199f78d6c6e3b98c1601e4407aa092af57e42842d195409569c199b0393afac9",
|
| 15 |
+
"MODEL_CARD.md": "311bf55c3eda8fb9e464c0870d662c87fc99ae8f9d1a38fa64e12b9cfed8b59f",
|
| 16 |
+
"MODEL_MANIFEST.json": "d82f71ba20a912a3d5fe1ef8a5a2f8e004be16e7c67df2ee5308864b6832eb5f",
|
| 17 |
+
"PIPELINE_PROVENANCE.json": "b069a1fdbe6b56867a84ecaad10ce7521cfe135cead029a002c83383f8bf6f44",
|
| 18 |
+
"README.md": "91995c8d1a63816e14ec73a3d2c9d25993072592c98ddbbd073ac211d1f445c5",
|
| 19 |
+
"SERVICE_LOCK.json": "f777656d2cd1d40d1df1bb8a9d55661175663564f5ad5c1de1db57d9be72af3b",
|
| 20 |
+
"attention.py": "9c887f4e5f8ea2858f79a629e684ea71b07359d49d2a0bf31b5f993cb367e547",
|
| 21 |
+
"backends.py": "e0613ebb8e05fd22673e0502a131dcc33a5af563998cf5a12143878131518f93",
|
| 22 |
+
"bench.py": "7f158bcb4dcd90395c21535527abe4283d6ca5dab92ded0be47cc352f022f0cd",
|
| 23 |
+
"calibration.json": "5b60ca8516b93982724145bc2227404c4c53a6f5657b4bf2155108c2f001685f",
|
| 24 |
+
"config.json": "0c84b933ac60e43394dbd748da21cb27bf91fe640cac7674acc3d02b80e8de11",
|
| 25 |
+
"core.py": "fb4169ed1c6c9d69e406e9a33d69a71cc49aa48e0155d9544bb9f3bf41690a17",
|
| 26 |
+
"fit_report.json": "fc20f2f631c98f043f7c02d4cadbe8122580ee400377eef5420d93621111248b",
|
| 27 |
+
"item_deadline.py": "8d3d637ab3db1be9769e26900b4fc46964e48425a05bb92402a2b504ae66ff3a",
|
| 28 |
+
"launch.py": "d8f966264dfa5d977ef6444364e1eb4128cf34ed1b60bc94a45f2649f6fce2d3",
|
| 29 |
+
"model_setup.py": "6c7cd992039327178b72247b8b0f44214a5a00e43abcc0f583130dfe91586dd7",
|
| 30 |
+
"pkg_check.py": "0e8bf9d4c723f6205e25244a28cae31f4534d8356e1cc1a1390dad199b2904c0",
|
| 31 |
+
"post_calibration.py": "62a89b2b87cee77322333774a6e081f28646804c926af0f0c0e798a2018b3ffe",
|
| 32 |
+
"prepare_model.sh": "454a0090ce0187bf6f4b4825a537183cf095c503592a7f673f7f57329258dc44",
|
| 33 |
+
"quota.py": "8b039bc30c13e6c5a63668f3064fbb311358f9295922a817ec26f09893936f8d",
|
| 34 |
+
"reconcile_usage.py": "a381dca414201652f254f5101f08d4cbba27b69c3abeb50e6dad86e0e151aba7",
|
| 35 |
+
"requirements.lock": "4536b444d83518a5d696ebf262aa9d3da156fe2d898f96582e4ab52d70cb4e4d",
|
| 36 |
+
"requirements.measured.lock": "888ec6b0d3fad1a17e9323c1ab9321040303c79df3c9fe43e6f755cf2ab24b91",
|
| 37 |
+
"selftest.py": "08d5cfceb703748b407ce0223dee2807bf49f27b4f9bbd28c43e9738673699e6",
|
| 38 |
+
"selftest.sh": "d0cc8cc9068667aec3525861e0d61ac8fc9bb617b90f7fee7818af9dc0af154e",
|
| 39 |
+
"serve.sh": "13008367a9b37988de9c7acfa1d2340ca7dea502fd352887fc61783749d279c3",
|
| 40 |
+
"server.py": "dbce4f70c2c684ac8ea5069a9dfa1d30aa33a8e1c33d274d91fd3c2a953c6951"
|
| 41 |
+
},
|
| 42 |
+
"replaced_documents": {},
|
| 43 |
+
"repo_id": "MeTask-Rain/metask-jev-rain-31B-trained",
|
| 44 |
+
"revision": "9392335f5a17395ecd70049f85b1909d53500144",
|
| 45 |
+
"schema": "metadecision_31b_freeze_v1",
|
| 46 |
+
"serving_selection": {
|
| 47 |
+
"freeze_sha256": "6e5325eb9713ed73ed9fc94992b67b1179ea60d6969757af61d5c8860d2bb22e",
|
| 48 |
+
"protocol_sha256": "1c279f3d780a0a522a217c38f74892e9c7019be5012409a9c83bbd9dbc179771",
|
| 49 |
+
"readout_sha256": "8cc627afcf00b3b9ba9edafe004adc8294d45c62bec8a2dc5b3f593cac4ea0a2",
|
| 50 |
+
"selected": "serving rules selected on our own development set",
|
| 51 |
+
"selection_sha256": "cc1070b2d79a088983cdb5c300adc7c1ed5c25232003830a9d1a8611e4df6820"
|
| 52 |
+
},
|
| 53 |
+
"status": "FROZEN"
|
| 54 |
+
}
|
service/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 MeTask (Hangzhou MeTask Technology Co., Ltd.)
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
Model weights are not part of this repository. This service loads Google's
|
| 24 |
+
official Gemma 4 31B W4A16 base and our unmerged LoRA adapter. See MODEL_CARD.md
|
| 25 |
+
and DISCLOSURE.md for the model sources and applicable weight licensing.
|
service/MANIFEST.sha256
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
705fd4d6451a31d36b3df7de96f83f30ac976c9b4a6d1e51671d8e2f33e2d0da .gitattributes
|
| 2 |
+
4254e460a0ff807e7fd68e104efa8935a769377f09876b8b4fec0fb0b544a0e6 .gitignore
|
| 3 |
+
6c40a7534f5d2b87cc8591748f85c7958f0259821c98659a60e25308904aa430 ADAPTER_MANIFEST.json
|
| 4 |
+
f50f14b50c9399ebf1cbf8867a8f807374448e0fbc9218dd3f3cd4a350a36135 DISCLOSURE.md
|
| 5 |
+
e2cfe584c132b9b3fd666bbabae51dccca455aaad541aad649ccb92741f75d1c FREEZE.json
|
| 6 |
+
199f78d6c6e3b98c1601e4407aa092af57e42842d195409569c199b0393afac9 LICENSE
|
| 7 |
+
311bf55c3eda8fb9e464c0870d662c87fc99ae8f9d1a38fa64e12b9cfed8b59f MODEL_CARD.md
|
| 8 |
+
d82f71ba20a912a3d5fe1ef8a5a2f8e004be16e7c67df2ee5308864b6832eb5f MODEL_MANIFEST.json
|
| 9 |
+
b069a1fdbe6b56867a84ecaad10ce7521cfe135cead029a002c83383f8bf6f44 PIPELINE_PROVENANCE.json
|
| 10 |
+
91995c8d1a63816e14ec73a3d2c9d25993072592c98ddbbd073ac211d1f445c5 README.md
|
| 11 |
+
f777656d2cd1d40d1df1bb8a9d55661175663564f5ad5c1de1db57d9be72af3b SERVICE_LOCK.json
|
| 12 |
+
9c887f4e5f8ea2858f79a629e684ea71b07359d49d2a0bf31b5f993cb367e547 attention.py
|
| 13 |
+
e0613ebb8e05fd22673e0502a131dcc33a5af563998cf5a12143878131518f93 backends.py
|
| 14 |
+
7f158bcb4dcd90395c21535527abe4283d6ca5dab92ded0be47cc352f022f0cd bench.py
|
| 15 |
+
5b60ca8516b93982724145bc2227404c4c53a6f5657b4bf2155108c2f001685f calibration.json
|
| 16 |
+
94d5765076019628a0dc736f79e470eefeea72661895599ea2670ef391b0fed0 config.json
|
| 17 |
+
fb4169ed1c6c9d69e406e9a33d69a71cc49aa48e0155d9544bb9f3bf41690a17 core.py
|
| 18 |
+
fc20f2f631c98f043f7c02d4cadbe8122580ee400377eef5420d93621111248b fit_report.json
|
| 19 |
+
8d3d637ab3db1be9769e26900b4fc46964e48425a05bb92402a2b504ae66ff3a item_deadline.py
|
| 20 |
+
d8f966264dfa5d977ef6444364e1eb4128cf34ed1b60bc94a45f2649f6fce2d3 launch.py
|
| 21 |
+
6c7cd992039327178b72247b8b0f44214a5a00e43abcc0f583130dfe91586dd7 model_setup.py
|
| 22 |
+
0e8bf9d4c723f6205e25244a28cae31f4534d8356e1cc1a1390dad199b2904c0 pkg_check.py
|
| 23 |
+
62a89b2b87cee77322333774a6e081f28646804c926af0f0c0e798a2018b3ffe post_calibration.py
|
| 24 |
+
454a0090ce0187bf6f4b4825a537183cf095c503592a7f673f7f57329258dc44 prepare_model.sh
|
| 25 |
+
8b039bc30c13e6c5a63668f3064fbb311358f9295922a817ec26f09893936f8d quota.py
|
| 26 |
+
a381dca414201652f254f5101f08d4cbba27b69c3abeb50e6dad86e0e151aba7 reconcile_usage.py
|
| 27 |
+
4536b444d83518a5d696ebf262aa9d3da156fe2d898f96582e4ab52d70cb4e4d requirements.lock
|
| 28 |
+
888ec6b0d3fad1a17e9323c1ab9321040303c79df3c9fe43e6f755cf2ab24b91 requirements.measured.lock
|
| 29 |
+
08d5cfceb703748b407ce0223dee2807bf49f27b4f9bbd28c43e9738673699e6 selftest.py
|
| 30 |
+
d0cc8cc9068667aec3525861e0d61ac8fc9bb617b90f7fee7818af9dc0af154e selftest.sh
|
| 31 |
+
13008367a9b37988de9c7acfa1d2340ca7dea502fd352887fc61783749d279c3 serve.sh
|
| 32 |
+
dbce4f70c2c684ac8ea5069a9dfa1d30aa33a8e1c33d274d91fd3c2a953c6951 server.py
|
service/MODEL_CARD.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
license_link: https://ai.google.dev/gemma/docs/gemma_4_license
|
| 4 |
+
base_model: google/gemma-4-31B-it
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
tags:
|
| 7 |
+
- gemma4
|
| 8 |
+
- compressed-tensors
|
| 9 |
+
- lora
|
| 10 |
+
- peft
|
| 11 |
+
- vllm
|
| 12 |
+
- jevbench
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# MetaDecision-31B-Rain
|
| 16 |
+
|
| 17 |
+
Decision service for JevBench. Base model:
|
| 18 |
+
[`google/gemma-4-31B-it`](https://huggingface.co/google/gemma-4-31B-it), used through Google's
|
| 19 |
+
official quantization-aware-training (QAT) releases of it. The model has two parts that are loaded
|
| 20 |
+
together and **not merged**; both are the files of
|
| 21 |
+
[`MeTask-Rain/metask-jev-rain-31B-trained`](https://huggingface.co/MeTask-Rain/metask-jev-rain-31B-trained/tree/2feff076f07711fe2fefe7a6fdcedfe4132b773e),
|
| 22 |
+
unchanged and pinned by SHA-256 in `MODEL_MANIFEST.json` of the service code. This entry uses the same weights as that
|
| 23 |
+
entry and changes only the serving rules.
|
| 24 |
+
|
| 25 |
+
| Part | Location in that model repository | What it is |
|
| 26 |
+
|---|---|---|
|
| 27 |
+
| Base | repository root | The files of [`google/gemma-4-31B-it-qat-w4a16-ct`](https://huggingface.co/google/gemma-4-31B-it-qat-w4a16-ct) at revision `52f3f65bc7a02d555763bc923bd1d9094898219d`, byte for byte: Google's official compressed-tensors W4A16 checkpoint (4-bit weights, group size 32, symmetric). |
|
| 28 |
+
| Adapter | `adapter/` | Our LoRA (PEFT format): rank 16, alpha 32, on the language model's q/k/v/o/gate/up/down projections, trained by us in BF16 on [`google/gemma-4-31B-it-qat-q4_0-unquantized`](https://huggingface.co/google/gemma-4-31B-it-qat-q4_0-unquantized) at revision `1e4d8beecacb8b7590c1d8bedd7335f687bf311f`. |
|
| 29 |
+
|
| 30 |
+
`adapter/adapter_model.safetensors` contains the trained checkpoint tensors unchanged plus, for the 10 full-attention layers (5, 11, …, 59) that compute values with the key projection, a v_proj LoRA that is a byte copy of the trained k_proj LoRA (20 tensors), so that vLLM applies the update to keys and values as in training.
|
| 31 |
+
|
| 32 |
+
## Serving
|
| 33 |
+
|
| 34 |
+
The service code (`service/` in the Hugging Face repository `MeTask-Rain/MetaDecision-31B-Rain`, mirrored at https://github.com/chenyuwang166-oss/MetaDecision-31B-Rain) starts vLLM 0.31.0 on a folder holding the files of that weight
|
| 35 |
+
repository, with `enable_lora`, `max_lora_rank=16`, and the `adapter/` folder as the single LoRA,
|
| 36 |
+
applied to every request. Each decision is read from the next-token log-probabilities of the answer
|
| 37 |
+
forms in one prompt pass; up to 49% of decisions then take a second, short drafting pass
|
| 38 |
+
(at most 192 tokens) under a 9 s deadline, a choice decision answered in one pass although its length allowed the drafting pass is read a second time with the options in reverse order when its first-pass margin is below 4, and the reported probabilities get a per-type post-calibration (DISCLOSURE.md).
|
| 39 |
+
|
| 40 |
+
Equivalent vLLM usage of the model:
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from vllm import LLM
|
| 44 |
+
from vllm.lora.request import LoRARequest
|
| 45 |
+
llm = LLM(model=MODEL_DIR, dtype="bfloat16", enable_lora=True, max_lora_rank=16)
|
| 46 |
+
adapter = LoRARequest("adapter", 1, MODEL_DIR + "/adapter")
|
| 47 |
+
# llm.generate(prompts, sampling_params, lora_request=adapter)
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Facts on data and the serving rules are in `DISCLOSURE.md` of the service code (`service/DISCLOSURE.md` in the
|
| 51 |
+
Hugging Face repository).
|
| 52 |
+
|
| 53 |
+
Contact: chenyuwang166@gmail.com
|
service/MODEL_MANIFEST.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter": {
|
| 3 |
+
"adapter_manifest_sha256": "6c40a7534f5d2b87cc8591748f85c7958f0259821c98659a60e25308904aa430",
|
| 4 |
+
"alpha": 32,
|
| 5 |
+
"files": {
|
| 6 |
+
"adapter_config.json": {
|
| 7 |
+
"sha256": "140bb00c832d117f57d29650dabe50e992b2e5064e908aaa29230b3ce19ca2c4",
|
| 8 |
+
"size": 22953
|
| 9 |
+
},
|
| 10 |
+
"adapter_model.safetensors": {
|
| 11 |
+
"sha256": "2b9568998ffed5d133223ffdff03056be297285c643122db4d45365d0bd86970",
|
| 12 |
+
"size": 494595208
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"format": "peft-lora",
|
| 16 |
+
"merged": false,
|
| 17 |
+
"rank": 16,
|
| 18 |
+
"stored_tensors": {
|
| 19 |
+
"dtypes": {
|
| 20 |
+
"F32": 840
|
| 21 |
+
},
|
| 22 |
+
"k_eq_v_layers_without_v_proj_lora": [],
|
| 23 |
+
"layers": 60,
|
| 24 |
+
"module_count": 420,
|
| 25 |
+
"module_prefix": "model.language_model.",
|
| 26 |
+
"rank": 16,
|
| 27 |
+
"targets": [
|
| 28 |
+
"down_proj",
|
| 29 |
+
"gate_proj",
|
| 30 |
+
"k_proj",
|
| 31 |
+
"o_proj",
|
| 32 |
+
"q_proj",
|
| 33 |
+
"up_proj",
|
| 34 |
+
"v_proj"
|
| 35 |
+
],
|
| 36 |
+
"tensor_count": 840,
|
| 37 |
+
"v_proj_copied_from_k_proj_layers": [
|
| 38 |
+
5,
|
| 39 |
+
11,
|
| 40 |
+
17,
|
| 41 |
+
23,
|
| 42 |
+
29,
|
| 43 |
+
35,
|
| 44 |
+
41,
|
| 45 |
+
47,
|
| 46 |
+
53,
|
| 47 |
+
59
|
| 48 |
+
],
|
| 49 |
+
"variant": "key_value_aligned"
|
| 50 |
+
},
|
| 51 |
+
"subdir": "adapter",
|
| 52 |
+
"target_modules": [
|
| 53 |
+
"down_proj",
|
| 54 |
+
"gate_proj",
|
| 55 |
+
"k_proj",
|
| 56 |
+
"o_proj",
|
| 57 |
+
"q_proj",
|
| 58 |
+
"up_proj",
|
| 59 |
+
"v_proj"
|
| 60 |
+
],
|
| 61 |
+
"training_base": "google/gemma-4-31B-it-qat-q4_0-unquantized",
|
| 62 |
+
"training_revision": "1e4d8beecacb8b7590c1d8bedd7335f687bf311f",
|
| 63 |
+
"variant": "key_value_aligned"
|
| 64 |
+
},
|
| 65 |
+
"arm": "trained",
|
| 66 |
+
"excluded_repository_documents": [
|
| 67 |
+
"README.md",
|
| 68 |
+
".gitattributes"
|
| 69 |
+
],
|
| 70 |
+
"files": {
|
| 71 |
+
"chat_template.jinja": {
|
| 72 |
+
"sha256": "ae53464bf3be25802b3a5b37def7fd89667067d7577049b3b2d74c4d8de4c6d4",
|
| 73 |
+
"size": 18683
|
| 74 |
+
},
|
| 75 |
+
"config.json": {
|
| 76 |
+
"sha256": "b100d85e571c25b688e82919d819253063a1defb0e6cee3b9b1c1fbad99bd0c2",
|
| 77 |
+
"size": 18711
|
| 78 |
+
},
|
| 79 |
+
"generation_config.json": {
|
| 80 |
+
"sha256": "70110aa886fa7ee25ce47346d102dd29ca5f967500490b4362fc72068e1db70a",
|
| 81 |
+
"size": 208
|
| 82 |
+
},
|
| 83 |
+
"model.safetensors": {
|
| 84 |
+
"sha256": "1b9b1d622a93f02c0d33f98e502f233b5d707443af6ddc464ed0bf5498506c20",
|
| 85 |
+
"size": 23265352448
|
| 86 |
+
},
|
| 87 |
+
"processor_config.json": {
|
| 88 |
+
"sha256": "32bdf45d2ad4cc29a0822ddd157a182de76644f0419a6228d151495256e9813c",
|
| 89 |
+
"size": 1689
|
| 90 |
+
},
|
| 91 |
+
"tokenizer.json": {
|
| 92 |
+
"sha256": "cc8d3a0ce36466ccc1278bf987df5f71db1719b9ca6b4118264f45cb627bfe0f",
|
| 93 |
+
"size": 32169626
|
| 94 |
+
},
|
| 95 |
+
"tokenizer_config.json": {
|
| 96 |
+
"sha256": "b8045a4576903e86903291d5cbdd4adfc8859e9ce3c98621bdbd957f73ed394b",
|
| 97 |
+
"size": 3728
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"format": "compressed-tensors",
|
| 101 |
+
"identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 102 |
+
"pricing_base_model": "google/gemma-4-31B-it",
|
| 103 |
+
"publish_authority": "repository owner",
|
| 104 |
+
"repo_id": "MeTask-Rain/metask-jev-rain-31B-trained",
|
| 105 |
+
"revision": "9392335f5a17395ecd70049f85b1909d53500144",
|
| 106 |
+
"served_base": {
|
| 107 |
+
"files_byte_identical": true,
|
| 108 |
+
"model_sha256": "ca750c75ea690276947dfc6d3c775668282e9180e4c56f180bfad912fed606a5",
|
| 109 |
+
"repo_id": "google/gemma-4-31B-it-qat-w4a16-ct",
|
| 110 |
+
"revision": "52f3f65bc7a02d555763bc923bd1d9094898219d"
|
| 111 |
+
},
|
| 112 |
+
"status": "evaluated_service_mirror",
|
| 113 |
+
"training_base": {
|
| 114 |
+
"repo_id": "google/gemma-4-31B-it-qat-q4_0-unquantized",
|
| 115 |
+
"revision": "1e4d8beecacb8b7590c1d8bedd7335f687bf311f"
|
| 116 |
+
}
|
| 117 |
+
}
|
service/PIPELINE_PROVENANCE.json
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_reference": "model-subdir:adapter",
|
| 3 |
+
"arm": "trained",
|
| 4 |
+
"core_patch": null,
|
| 5 |
+
"input_budget": 6653,
|
| 6 |
+
"measured_environment": {
|
| 7 |
+
"entries": 201,
|
| 8 |
+
"equal_to_source_package_measured_lock": false,
|
| 9 |
+
"files": {
|
| 10 |
+
"requirements.lock": "4536b444d83518a5d696ebf262aa9d3da156fe2d898f96582e4ab52d70cb4e4d",
|
| 11 |
+
"requirements.measured.lock": "888ec6b0d3fad1a17e9323c1ab9321040303c79df3c9fe43e6f755cf2ab24b91"
|
| 12 |
+
},
|
| 13 |
+
"freeze_sha256": "888ec6b0d3fad1a17e9323c1ab9321040303c79df3c9fe43e6f755cf2ab24b91",
|
| 14 |
+
"source": "pip freeze of the measurement service environment"
|
| 15 |
+
},
|
| 16 |
+
"model_key": "metask-gemma4-31b-lora-w4a16",
|
| 17 |
+
"model_name": "MetaDecision-31B-Rain",
|
| 18 |
+
"notes": [
|
| 19 |
+
"fit_report.json, calibration.json and service_policy.base_fit / fit_n describe the first-step calibration of the earlier service of these weights (unchanged); the serving rules of this entry are config.json routing / generation / item_deadline / item_rules / post_calibration",
|
| 20 |
+
"weights.revision is the model manifest revision; evaluated_service_revision pins the same model files"
|
| 21 |
+
],
|
| 22 |
+
"protocol": "adapter_arm_31b_v3",
|
| 23 |
+
"quota": 0.49,
|
| 24 |
+
"reference_source_files": {
|
| 25 |
+
"attention.py": "9c887f4e5f8ea2858f79a629e684ea71b07359d49d2a0bf31b5f993cb367e547",
|
| 26 |
+
"backends.py": "e0613ebb8e05fd22673e0502a131dcc33a5af563998cf5a12143878131518f93",
|
| 27 |
+
"bench.py": "7f158bcb4dcd90395c21535527abe4283d6ca5dab92ded0be47cc352f022f0cd",
|
| 28 |
+
"core.py": "fb4169ed1c6c9d69e406e9a33d69a71cc49aa48e0155d9544bb9f3bf41690a17",
|
| 29 |
+
"model_setup.py": "6c7cd992039327178b72247b8b0f44214a5a00e43abcc0f583130dfe91586dd7",
|
| 30 |
+
"quota.py": "8b039bc30c13e6c5a63668f3064fbb311358f9295922a817ec26f09893936f8d",
|
| 31 |
+
"reconcile_usage.py": "a381dca414201652f254f5101f08d4cbba27b69c3abeb50e6dad86e0e151aba7",
|
| 32 |
+
"server.py": "2cb085c1bf0f68758097afee55ecad2ef7ecd76dcced00c1b3f927b49151e213"
|
| 33 |
+
},
|
| 34 |
+
"route": "unmerged LoRA adapter (adapter/) on the official W4A16 compressed-tensors base",
|
| 35 |
+
"served_base": {
|
| 36 |
+
"repo_id": "google/gemma-4-31B-it-qat-w4a16-ct",
|
| 37 |
+
"revision": "52f3f65bc7a02d555763bc923bd1d9094898219d"
|
| 38 |
+
},
|
| 39 |
+
"served_mode": "routed",
|
| 40 |
+
"server_patch": {
|
| 41 |
+
"change": "install_extensions(): item_deadline with item_rules, then post_calibration",
|
| 42 |
+
"patched_server_sha256": "dbce4f70c2c684ac8ea5069a9dfa1d30aa33a8e1c33d274d91fd3c2a953c6951",
|
| 43 |
+
"reference_server_sha256": "2cb085c1bf0f68758097afee55ecad2ef7ecd76dcced00c1b3f927b49151e213"
|
| 44 |
+
},
|
| 45 |
+
"service_policy": {
|
| 46 |
+
"base_fit": {
|
| 47 |
+
"draft": 256,
|
| 48 |
+
"q": 0.056
|
| 49 |
+
},
|
| 50 |
+
"core_patch": null,
|
| 51 |
+
"cost_fuse": {
|
| 52 |
+
"arm": 0.9,
|
| 53 |
+
"enabled": true,
|
| 54 |
+
"release": 0.85,
|
| 55 |
+
"warmup": 100
|
| 56 |
+
},
|
| 57 |
+
"cost_fuse_prices": {
|
| 58 |
+
"input_per_million": 0.059819178473256074,
|
| 59 |
+
"output_per_million": 0.059819178473256074
|
| 60 |
+
},
|
| 61 |
+
"draft": {
|
| 62 |
+
"max_new_tokens": 192,
|
| 63 |
+
"reasoning_words": 50
|
| 64 |
+
},
|
| 65 |
+
"fit_n": 231,
|
| 66 |
+
"fit_protocol": "31b-adapter-arm-v3-routed",
|
| 67 |
+
"fuse": 0.49,
|
| 68 |
+
"item_deadline": {
|
| 69 |
+
"chunk_tokens": 32,
|
| 70 |
+
"ewma_alpha": 0.3,
|
| 71 |
+
"item_seconds": 9.0,
|
| 72 |
+
"min_chunk_tokens": 8,
|
| 73 |
+
"readout_reserve_init_s": 1.0,
|
| 74 |
+
"readout_reserve_min_s": 0.3,
|
| 75 |
+
"readout_safety": 1.5,
|
| 76 |
+
"response_reserve_s": 0.35,
|
| 77 |
+
"token_safety": 1.25,
|
| 78 |
+
"warmup_repeats": 3,
|
| 79 |
+
"warmup_tokens": 33
|
| 80 |
+
},
|
| 81 |
+
"item_rules": {
|
| 82 |
+
"reread_tau": 4.0,
|
| 83 |
+
"slow_types": [
|
| 84 |
+
"choice",
|
| 85 |
+
"noul",
|
| 86 |
+
"score"
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
"margin_threshold": 6.625023671062799,
|
| 90 |
+
"mode": "routed",
|
| 91 |
+
"noul_p_cal": 0.898,
|
| 92 |
+
"post_calibration": {
|
| 93 |
+
"choice": {
|
| 94 |
+
"config": {
|
| 95 |
+
"debias": false,
|
| 96 |
+
"temperature": false
|
| 97 |
+
},
|
| 98 |
+
"params": {}
|
| 99 |
+
},
|
| 100 |
+
"noul": {
|
| 101 |
+
"config": {
|
| 102 |
+
"floor": null,
|
| 103 |
+
"temperature": "type"
|
| 104 |
+
},
|
| 105 |
+
"params": {
|
| 106 |
+
"T": 0.5
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"score": {
|
| 110 |
+
"config": {
|
| 111 |
+
"mode_weight": 1.0,
|
| 112 |
+
"temperature": false
|
| 113 |
+
},
|
| 114 |
+
"params": {}
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
"post_calibration_active": true,
|
| 118 |
+
"protocol": "adapter_arm_31b_v3",
|
| 119 |
+
"quota": 0.49,
|
| 120 |
+
"slow_excluded_types": [],
|
| 121 |
+
"slow_max_prompt_tokens": 840,
|
| 122 |
+
"threshold_rule": "eligible_items",
|
| 123 |
+
"unparsed_policy": "force_answer",
|
| 124 |
+
"v3_params_sha256": "b28ed73bb5a36e99a280a47a6b846e0c5b2de54f34a96efa4a10754b71ea7573"
|
| 125 |
+
},
|
| 126 |
+
"source_files": {
|
| 127 |
+
"attention.py": "9c887f4e5f8ea2858f79a629e684ea71b07359d49d2a0bf31b5f993cb367e547",
|
| 128 |
+
"backends.py": "e0613ebb8e05fd22673e0502a131dcc33a5af563998cf5a12143878131518f93",
|
| 129 |
+
"bench.py": "7f158bcb4dcd90395c21535527abe4283d6ca5dab92ded0be47cc352f022f0cd",
|
| 130 |
+
"core.py": "fb4169ed1c6c9d69e406e9a33d69a71cc49aa48e0155d9544bb9f3bf41690a17",
|
| 131 |
+
"item_deadline.py": "8d3d637ab3db1be9769e26900b4fc46964e48425a05bb92402a2b504ae66ff3a",
|
| 132 |
+
"model_setup.py": "6c7cd992039327178b72247b8b0f44214a5a00e43abcc0f583130dfe91586dd7",
|
| 133 |
+
"post_calibration.py": "62a89b2b87cee77322333774a6e081f28646804c926af0f0c0e798a2018b3ffe",
|
| 134 |
+
"quota.py": "8b039bc30c13e6c5a63668f3064fbb311358f9295922a817ec26f09893936f8d",
|
| 135 |
+
"reconcile_usage.py": "a381dca414201652f254f5101f08d4cbba27b69c3abeb50e6dad86e0e151aba7",
|
| 136 |
+
"server.py": "dbce4f70c2c684ac8ea5069a9dfa1d30aa33a8e1c33d274d91fd3c2a953c6951"
|
| 137 |
+
},
|
| 138 |
+
"weights": {
|
| 139 |
+
"evaluated_service_revision": "2feff076f07711fe2fefe7a6fdcedfe4132b773e",
|
| 140 |
+
"identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 141 |
+
"note": "same model files as the earlier service of these weights; not copied",
|
| 142 |
+
"repo_id": "MeTask-Rain/metask-jev-rain-31B-trained",
|
| 143 |
+
"revision": "9392335f5a17395ecd70049f85b1909d53500144"
|
| 144 |
+
}
|
| 145 |
+
}
|
service/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MetaDecision-31B-Rain
|
| 2 |
+
|
| 3 |
+
A self-hosted decision service for JevBench. It serves `POST /v1/systemone` for the unchanged
|
| 4 |
+
`typesafe` adapter of the official harness, and `GET /health`.
|
| 5 |
+
|
| 6 |
+
The model weights are exactly those of
|
| 7 |
+
[`MeTask-Rain/metask-jev-rain-31B-trained`](https://huggingface.co/MeTask-Rain/metask-jev-rain-31B-trained/tree/2feff076f07711fe2fefe7a6fdcedfe4132b773e)
|
| 8 |
+
(the same files, pinned by SHA-256 in `MODEL_MANIFEST.json`): Google's official W4A16 checkpoint
|
| 9 |
+
`google/gemma-4-31B-it-qat-w4a16-ct` (unchanged) and our LoRA adapter, loaded together and never
|
| 10 |
+
merged. `MODEL_MANIFEST.json` names that repository's earlier revision `9392335f5a17395ecd70049f85b1909d53500144`, which
|
| 11 |
+
holds the same model files. This entry changes only how the service uses them:
|
| 12 |
+
|
| 13 |
+
- a larger share of decisions may take a short drafting pass (slow-path quota 49%, at most 192 drafted tokens);
|
| 14 |
+
- a decision that takes the drafting pass has a deadline of 9 s: the draft is stopped in time for its answer to be read, and a drafting pass that cannot be read in time returns the first-pass answer;
|
| 15 |
+
- a choice decision answered in one pass although its length allowed the drafting pass, whose first-pass margin is below 4, is read once more with the options in reverse order, and the two answer distributions are averaged;
|
| 16 |
+
- the reported probabilities get a per-type post-calibration fitted on our own development set (no model call);
|
| 17 |
+
- the cost fuse prices engine tokens at the leaderboard's GPU-time rate for this architecture (USD 0.0598 per million tokens).
|
| 18 |
+
|
| 19 |
+
Facts and serving rules are in [DISCLOSURE.md](DISCLOSURE.md).
|
| 20 |
+
|
| 21 |
+
## Requirements
|
| 22 |
+
|
| 23 |
+
- One NVIDIA GPU used only by this service: compute capability 8.0 or newer, NVIDIA driver 580 or
|
| 24 |
+
newer, 80 GB or more of GPU memory. Validated: steps 2-6 below, run with this package and the model files downloaded in step 2, completed the 231 public items on one NVIDIA A100 80GB (231/231, zero failed). The service starts vLLM with
|
| 25 |
+
`gpu_memory_utilization=0.94` (it reserves 94% of the GPU memory), `max_model_len=6656`,
|
| 26 |
+
`enable_lora` and `max_lora_rank=16`. The model files take 23.3 GB (base) plus 495 MB (adapter).
|
| 27 |
+
The commands use the host's GPU 0 (`--gpus '"device=0"'`).
|
| 28 |
+
- Docker with the NVIDIA Container Toolkit.
|
| 29 |
+
- On the host: Git and Python with pip; step 2 installs the Hugging Face command line tool `hf`.
|
| 30 |
+
- Runtime image `vllm/vllm-openai@sha256:a4a4c0437bf7240089da5f08aa370c4aee17ae5290f7a3b468825ee26c4c3a6b`
|
| 31 |
+
(Python 3.12.3, vLLM 0.31.0, torch 2.13.0+cu130), the linux/amd64 image of vLLM release
|
| 32 |
+
`v0.31.0`. Nothing is installed and the service has no network access.
|
| 33 |
+
- Model folder: the model files of https://huggingface.co/MeTask-Rain/metask-jev-rain-31B-trained/tree/2feff076f07711fe2fefe7a6fdcedfe4132b773e
|
| 34 |
+
as regular files (step 2). The repository root holds the official base files; `adapter/` holds
|
| 35 |
+
the adapter (`adapter_config.json`, `adapter_model.safetensors`). A Hugging Face cache
|
| 36 |
+
`snapshots/<revision>` directory holds links and cannot be mounted on its own. At startup the
|
| 37 |
+
service checks the SHA-256 of the small model files, the size of the weight file and the full
|
| 38 |
+
SHA-256 of both adapter files (`MODEL_MANIFEST.json`), and refuses other weight, tokenizer,
|
| 39 |
+
processor or template files in the folder; it never writes to the folder.
|
| 40 |
+
- Code: this repository (download command in step 1).
|
| 41 |
+
- Harness: https://github.com/fstandhartinger/jevbench/tree/bb05a335bc809e61b20c0f745d25499a82b326fc
|
| 42 |
+
|
| 43 |
+
## Run
|
| 44 |
+
|
| 45 |
+
Copy the commands as they are, in the same Bash terminal.
|
| 46 |
+
|
| 47 |
+
1. Obtain the code and harness, and create a fresh work directory. Run from a writable parent
|
| 48 |
+
directory; the new directories must not already contain another run:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
git clone https://github.com/chenyuwang166-oss/MetaDecision-31B-Rain.git
|
| 52 |
+
PKG="$(pwd)/MetaDecision-31B-Rain"
|
| 53 |
+
git clone https://github.com/fstandhartinger/jevbench.git jevbench
|
| 54 |
+
git -C jevbench checkout bb05a335bc809e61b20c0f745d25499a82b326fc
|
| 55 |
+
JEVBENCH="$(pwd)/jevbench"
|
| 56 |
+
MODEL="$(pwd)/model-31b"
|
| 57 |
+
WORK="$(pwd)/work-31b-run1"
|
| 58 |
+
mkdir "$MODEL" "$WORK"
|
| 59 |
+
TASKS=/jevbench/datasets/public/easy.jsonl,/jevbench/datasets/public/original.jsonl,/jevbench/datasets/public/hard.jsonl
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
2. Install the download tool, fetch the pinned weights, and pull the pinned runtime image:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
python3 -m pip install huggingface_hub
|
| 66 |
+
hf download MeTask-Rain/metask-jev-rain-31B-trained --revision 2feff076f07711fe2fefe7a6fdcedfe4132b773e --local-dir "$MODEL" --exclude 'service/*'
|
| 67 |
+
docker pull vllm/vllm-openai@sha256:a4a4c0437bf7240089da5f08aa370c4aee17ae5290f7a3b468825ee26c4c3a6b
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
3. Start the service:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
docker run -d --name metadecision31b --init --gpus '"device=0"' --network none --read-only --shm-size 8g \
|
| 74 |
+
-v "$PKG":/pkg:ro -v "$MODEL":/model:ro -v "$WORK":/work -v "$JEVBENCH":/jevbench:ro \
|
| 75 |
+
--entrypoint bash vllm/vllm-openai@sha256:a4a4c0437bf7240089da5f08aa370c4aee17ae5290f7a3b468825ee26c4c3a6b \
|
| 76 |
+
/pkg/serve.sh --model /model --state-dir /work/state --run-id run1
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
4. Follow the log until the service is ready:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
docker logs -f metadecision31b
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
When a line starting with `{"status": "ok"` appears, press Ctrl-C (this ends only the log
|
| 86 |
+
display) and check health before step 5:
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
docker exec metadecision31b python3 -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health").read().decode())'
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
If the log ends before that line, the container has exited: the reason is in
|
| 93 |
+
`docker logs metadecision31b` and in `$WORK/state/run1-startup/attempt-*.log`; go to step 6.
|
| 94 |
+
After correcting the cause, use a new `WORK` and restart from step 3.
|
| 95 |
+
|
| 96 |
+
5. Run the official harness in the same container, then summarize:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
docker exec -e PYTHONPATH=/jevbench metadecision31b python3 -B -m jevbench.cli run --adapter typesafe \
|
| 100 |
+
--endpoint http://localhost:8000 --model MetaDecision-31B-Rain --key-env '' --tasks "$TASKS" \
|
| 101 |
+
--results /work/run1/results.jsonl --ledger /work/run1/ledger.jsonl --raw-dir /work/run1/raw \
|
| 102 |
+
--manifest /work/run1/manifest.json --cap-usd 15 --price-in-per-m 0.09 --price-out-per-m 0.34
|
| 103 |
+
docker exec -e PYTHONPATH=/jevbench metadecision31b python3 -B -m jevbench.cli summarize --tasks "$TASKS" \
|
| 104 |
+
--results /work/run1/results.jsonl --ledger /work/run1/ledger.jsonl
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
6. Stop and remove the container:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
docker stop -t 40 metadecision31b && docker rm metadecision31b
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
Each evaluation uses a new `WORK` and a new container. A run name that already exists below the
|
| 114 |
+
state directory is refused at startup ("use a new --state-dir"); with these commands that means a
|
| 115 |
+
new `WORK`.
|
| 116 |
+
|
| 117 |
+
## What to expect
|
| 118 |
+
|
| 119 |
+
- Health line: `"status": "ok"`, `"warnings": []`, `"model_key": "metask-gemma4-31b-lora-w4a16"`,
|
| 120 |
+
`"mode": "routed"`, `"quantization_kernel": "compressed-tensors"`, `"prefix_caching": true`,
|
| 121 |
+
`"supports_slow_session": true`, and `threshold_identity.adapter` equal to `/model/adapter`, the
|
| 122 |
+
verified adapter folder.
|
| 123 |
+
- Startup: `launch.py` verifies the model folder and the adapter, writes the run configuration
|
| 124 |
+
(with the adapter folder filled in) to `$WORK/state/run1-startup/config.runtime.json`, and starts
|
| 125 |
+
vLLM with `enable_lora` and `max_lora_rank=16`; it sets `VLLM_BATCH_INVARIANT=1`,
|
| 126 |
+
`VLLM_USE_FLASHINFER_SAMPLER=0` and `VLLM_WORKER_MULTIPROC_METHOD=spawn`. Each startup attempt
|
| 127 |
+
waits at most 1800 s for readiness: the first lets vLLM select the attention backend; only if the
|
| 128 |
+
engine fails to start does a second attempt request `TRITON_ATTN`. Before serving, the service
|
| 129 |
+
times a few short synthetic drafts to size its drafting chunks.
|
| 130 |
+
- Each decision is first read in one prompt pass. At most 49% of decisions take a second,
|
| 131 |
+
drafting pass (at most 192 generated tokens) before the answer is read again; such a
|
| 132 |
+
decision is planned to finish within its 9 s deadline, counted from the moment the
|
| 133 |
+
service starts answering the request (an engine call in progress is completed, never abandoned;
|
| 134 |
+
DISCLOSURE.md states the rules and the cost fuse). Inputs longer than 6,653 tokens are answered in
|
| 135 |
+
one pass from a context shortened to at most 6,653 tokens by the service's extraction rule.
|
| 136 |
+
- Writes: the service writes files only below `$WORK/state` (plus shared memory in `/dev/shm`) and
|
| 137 |
+
the harness only below `$WORK/run1`. The package and model mounts are read-only. Cache and
|
| 138 |
+
configuration path variables (`HF_HOME`, `XDG_*`, `TRITON_*`, `TORCH_HOME` and similar) must not
|
| 139 |
+
be passed to the container: a value outside `/work/state` is refused at startup. No script of
|
| 140 |
+
this package deletes directories recursively.
|
| 141 |
+
- Responses: an item the service cannot process returns HTTP 422 without answer and without usage;
|
| 142 |
+
after an engine failure, and from the third consecutive item failure on, the service answers
|
| 143 |
+
every `POST /v1/systemone` with HTTP 503 and `/health` reports `"status": "error"`. No substitute
|
| 144 |
+
answer is ever returned. A decision whose drafting pass could not be read before its deadline is
|
| 145 |
+
answered (HTTP 200) with its valid first-pass answer and carries a `warning` note, also listed in
|
| 146 |
+
`partial_errors`.
|
| 147 |
+
|
| 148 |
+
The health response keeps `threshold_identity.quant: "awq"` as a historical configuration alias;
|
| 149 |
+
the actual backend is **compressed-tensors W4A16**, reported by `quantization_kernel`.
|
| 150 |
+
|
| 151 |
+
## Package checks (offline, no GPU or network)
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
cd "$PKG"
|
| 155 |
+
python3 -B pkg_check.py # package integrity, configuration, adapter contract, serving policy
|
| 156 |
+
python3 -B selftest.py --offline # synthetic software tests, including long-input compression
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
`MODEL_MANIFEST.json` pins the weight repository, revision and every model and adapter file;
|
| 160 |
+
`ADAPTER_MANIFEST.json` records the adapter's origin by SHA-256; `FREEZE.json` records the freeze
|
| 161 |
+
of this configuration; `MANIFEST.sha256` binds all package files. `MODEL_CARD.md` describes the
|
| 162 |
+
model.
|
| 163 |
+
|
| 164 |
+
Contact: chenyuwang166@gmail.com
|
service/SERVICE_LOCK.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_files": {
|
| 3 |
+
"adapter_config.json": {
|
| 4 |
+
"sha256": "140bb00c832d117f57d29650dabe50e992b2e5064e908aaa29230b3ce19ca2c4",
|
| 5 |
+
"size": 22953
|
| 6 |
+
},
|
| 7 |
+
"adapter_model.safetensors": {
|
| 8 |
+
"sha256": "2b9568998ffed5d133223ffdff03056be297285c643122db4d45365d0bd86970",
|
| 9 |
+
"size": 494595208
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"arm": "trained",
|
| 13 |
+
"calibration_sha256": "5b60ca8516b93982724145bc2227404c4c53a6f5657b4bf2155108c2f001685f",
|
| 14 |
+
"complete": true,
|
| 15 |
+
"config_file_sha256": "0c84b933ac60e43394dbd748da21cb27bf91fe640cac7674acc3d02b80e8de11",
|
| 16 |
+
"fit_report_sha256": "fc20f2f631c98f043f7c02d4cadbe8122580ee400377eef5420d93621111248b",
|
| 17 |
+
"protocol": "adapter_arm_31b_v3",
|
| 18 |
+
"q": 0.49,
|
| 19 |
+
"serving": {
|
| 20 |
+
"draft": {
|
| 21 |
+
"max_new_tokens": 192,
|
| 22 |
+
"reasoning_words": 50
|
| 23 |
+
},
|
| 24 |
+
"fuse": 0.49,
|
| 25 |
+
"item_deadline_sha256": "3633ad26b201ba9200d6bb606852a80cef33a0c58adc0709f15f4bc8a49d9148",
|
| 26 |
+
"item_rules": {
|
| 27 |
+
"reread_tau": 4.0,
|
| 28 |
+
"slow_types": [
|
| 29 |
+
"choice",
|
| 30 |
+
"noul",
|
| 31 |
+
"score"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"post_calibration_sha256": "02cbf05b38bb627acd416f64e66a5cea6a70ce3a7055f1976fcf54e965396951",
|
| 35 |
+
"quota": 0.49,
|
| 36 |
+
"selection": {
|
| 37 |
+
"freeze_sha256": "6e5325eb9713ed73ed9fc94992b67b1179ea60d6969757af61d5c8860d2bb22e",
|
| 38 |
+
"protocol_sha256": "1c279f3d780a0a522a217c38f74892e9c7019be5012409a9c83bbd9dbc179771",
|
| 39 |
+
"readout_sha256": "8cc627afcf00b3b9ba9edafe004adc8294d45c62bec8a2dc5b3f593cac4ea0a2",
|
| 40 |
+
"selected": "serving rules selected on our own development set",
|
| 41 |
+
"selection_sha256": "cc1070b2d79a088983cdb5c300adc7c1ed5c25232003830a9d1a8611e4df6820"
|
| 42 |
+
},
|
| 43 |
+
"threshold": 6.625023671062799
|
| 44 |
+
},
|
| 45 |
+
"serving_identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 46 |
+
"status": "frozen",
|
| 47 |
+
"variant": "key_value_aligned"
|
| 48 |
+
}
|
service/attention.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Report the attention backend named in the engine log; the requested setting is reported separately."""
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
# vLLM 0.31 logs "Using AttentionBackendEnum.<NAME> backend."; older formats name the backend in prose.
|
| 7 |
+
_PATTERN = re.compile(r'\bUsing\s+(?:AttentionBackendEnum\.([A-Za-z0-9_]+)|([A-Za-z][A-Za-z0-9_ -]*?))\s+backend\b', re.I)
|
| 8 |
+
_ALIASES = {'FLASH_ATTENTION': 'FLASH_ATTN', 'TRITON_ATTENTION': 'TRITON_ATTN',
|
| 9 |
+
'FLASH_INFER': 'FLASHINFER', 'FLASHINFER_ATTENTION': 'FLASHINFER'}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def attention_health():
|
| 13 |
+
"""Backend names found in the engine log of this service process (JEV_ATTENTION_LOG).
|
| 14 |
+
|
| 15 |
+
The requested value comes from JEV_ATTENTION_BACKEND; 'auto' means vLLM selected the backend.
|
| 16 |
+
A request is never reported as the active backend.
|
| 17 |
+
"""
|
| 18 |
+
requested = os.environ.get('JEV_ATTENTION_BACKEND') or 'auto'
|
| 19 |
+
text = ''
|
| 20 |
+
path = os.environ.get('JEV_ATTENTION_LOG')
|
| 21 |
+
if path:
|
| 22 |
+
try:
|
| 23 |
+
text = Path(path).read_text(encoding='utf-8', errors='replace')
|
| 24 |
+
except OSError:
|
| 25 |
+
pass
|
| 26 |
+
text = re.sub(r'\x1b\[[0-9;]*m', '', text)
|
| 27 |
+
names = []
|
| 28 |
+
for enum_name, prose in _PATTERN.findall(text):
|
| 29 |
+
raw = (enum_name or prose).strip().upper().replace('-', '_').replace(' ', '_')
|
| 30 |
+
if not enum_name and 'ATTN' not in raw and 'ATTENTION' not in raw:
|
| 31 |
+
continue # "Using X backend" lines about other kernels
|
| 32 |
+
names.append(_ALIASES.get(raw, raw.removesuffix('_ATTENTION')))
|
| 33 |
+
names = list(dict.fromkeys(names))
|
| 34 |
+
return {'attention_backend': names[-1] if names else None,
|
| 35 |
+
'attention_backends': names,
|
| 36 |
+
'attention_backend_requested': requested,
|
| 37 |
+
'attention_backend_source': 'engine_log' if names else 'unreported'}
|
service/backends.py
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy model backends; importing this module never imports or loads a model.
|
| 2 |
+
|
| 3 |
+
Both backends return unmasked next-token scores. vLLM returns raw log
|
| 4 |
+
probabilities, which differ from logits by one common additive constant; the
|
| 5 |
+
form-set logsumexp and subsequent label temperature are invariant to it.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
import logging
|
| 12 |
+
import json
|
| 13 |
+
import hashlib
|
| 14 |
+
import os
|
| 15 |
+
import re
|
| 16 |
+
import threading
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any, Iterable
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class BackendError(RuntimeError):
|
| 23 |
+
"""An unavailable or incompatible backend, with no approximation fallback."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _candidate_ids(values: Iterable[int]) -> list[int]:
|
| 27 |
+
ids = list(dict.fromkeys(values))
|
| 28 |
+
if not ids or any(isinstance(i, bool) or not isinstance(i, int) or i < 0 for i in ids):
|
| 29 |
+
raise ValueError("candidate_ids must contain nonnegative integer token IDs")
|
| 30 |
+
return ids
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _max_new_tokens(value: int) -> int:
|
| 34 |
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
| 35 |
+
raise ValueError("max_new_tokens must be a positive integer")
|
| 36 |
+
return value
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _check_scores(scores: dict[int, float]) -> dict[int, float]:
|
| 40 |
+
if any(math.isnan(value) or value == math.inf for value in scores.values()):
|
| 41 |
+
raise BackendError("backend returned NaN or positive infinite candidate scores")
|
| 42 |
+
return scores
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def generation_audit(config):
|
| 46 |
+
"""Compare explicit service settings; never silently inherit checkpoint policy."""
|
| 47 |
+
report={'verified':False,'status':'unavailable','differences':{},'intended_overrides':{},'sha256':None}
|
| 48 |
+
model_path=config['model'].get('path')
|
| 49 |
+
path=Path(os.path.expanduser(model_path))/'generation_config.json' if model_path else None
|
| 50 |
+
if path is not None and path.is_file():
|
| 51 |
+
try:
|
| 52 |
+
data=path.read_bytes();reference=json.loads(data)
|
| 53 |
+
if not isinstance(reference,dict):raise ValueError('Expected mapping')
|
| 54 |
+
report['sha256']=hashlib.sha256(data).hexdigest()
|
| 55 |
+
def safe(v):
|
| 56 |
+
if v is None or isinstance(v,bool) or isinstance(v,(int,float)) and math.isfinite(v):return v
|
| 57 |
+
if isinstance(v,list):return [safe(x) for x in v]
|
| 58 |
+
return 'unsupported value'
|
| 59 |
+
for key,value in config['generation'].items():
|
| 60 |
+
if key not in ('seed','reasoning_words','max_new_tokens','wall_timeout_seconds') and key in reference and reference[key]!=value:
|
| 61 |
+
difference={'checkpoint':safe(reference[key]),'configured':safe(value)}
|
| 62 |
+
declared=config.get('generation_reference',{}).get('intended_overrides',{}).get(key,{})
|
| 63 |
+
intended=(isinstance(declared,dict) and declared.get('reason')
|
| 64 |
+
and 'value' in declared and type(declared['value']) is type(value)
|
| 65 |
+
and declared['value']==value and config['generation'].get('do_sample') is False)
|
| 66 |
+
target='intended_overrides' if intended else 'differences'
|
| 67 |
+
if intended:difference['reason']=declared['reason']
|
| 68 |
+
report[target][key]=difference
|
| 69 |
+
report.update(verified=not report['differences'],status='mismatch' if report['differences'] else 'matched_explicit_keys')
|
| 70 |
+
except (OSError,ValueError,TypeError):report['status']='unreadable'
|
| 71 |
+
if not report['verified']:logging.warning('Generation reference %s; explicit configured parameters remain active',report['status'])
|
| 72 |
+
return report
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def install_forward_counter(backend):
|
| 76 |
+
backend.forward_counters={'forward_tokens':0,'prefill_tokens':0,'uncounted_forwards':0}
|
| 77 |
+
def count_forward(module,args,kwargs):
|
| 78 |
+
try:
|
| 79 |
+
ids=kwargs.get('input_ids',args[0] if args else None)
|
| 80 |
+
if ids is None:ids=kwargs.get('inputs_embeds')
|
| 81 |
+
if ids is None:raise ValueError('No input tensor')
|
| 82 |
+
n=int(ids.shape[1]);past=kwargs.get('past_key_values')
|
| 83 |
+
empty=past is None or (callable(getattr(past,'get_seq_length',None)) and past.get_seq_length()==0)
|
| 84 |
+
backend.forward_counters['forward_tokens']+=n
|
| 85 |
+
if empty:backend.forward_counters['prefill_tokens']+=n
|
| 86 |
+
except Exception:
|
| 87 |
+
backend.forward_counters['uncounted_forwards']+=1
|
| 88 |
+
logging.warning('HF forward count unavailable; usage estimated')
|
| 89 |
+
target=backend.model.get_base_model() if callable(getattr(backend.model,'get_base_model',None)) else backend.model
|
| 90 |
+
hook=getattr(target,'register_forward_pre_hook',None)
|
| 91 |
+
backend.stats_available=callable(hook)
|
| 92 |
+
if backend.stats_available:backend.stats_hook=hook(count_forward,with_kwargs=True)
|
| 93 |
+
else:logging.warning('HF forward hook unavailable; usage estimated')
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def runtime_info():
|
| 97 |
+
"""Versions on the numeric path and the visible GPU, reported in /health info.runtime.
|
| 98 |
+
|
| 99 |
+
The GPU name comes from vLLM's platform interface (NVML), so this process creates no CUDA
|
| 100 |
+
context. triton_module is the version of the triton module the process actually imported.
|
| 101 |
+
"""
|
| 102 |
+
import importlib.metadata
|
| 103 |
+
import platform
|
| 104 |
+
import sys
|
| 105 |
+
info = {"python": platform.python_version()}
|
| 106 |
+
for name in ("vllm", "torch", "transformers", "tokenizers", "triton", "compressed-tensors"):
|
| 107 |
+
try:
|
| 108 |
+
info[name] = importlib.metadata.version(name)
|
| 109 |
+
except importlib.metadata.PackageNotFoundError:
|
| 110 |
+
info[name] = None
|
| 111 |
+
info["triton_module"] = getattr(sys.modules.get("triton"), "__version__", None)
|
| 112 |
+
try:
|
| 113 |
+
import torch
|
| 114 |
+
info["torch_cuda"] = torch.version.cuda
|
| 115 |
+
except Exception:
|
| 116 |
+
info["torch_cuda"] = None
|
| 117 |
+
try:
|
| 118 |
+
from vllm.platforms import current_platform
|
| 119 |
+
info["gpu"] = current_platform.get_device_name()
|
| 120 |
+
except Exception:
|
| 121 |
+
info["gpu"] = None
|
| 122 |
+
return info
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class TokenizerMixin:
|
| 126 |
+
tokenizer: Any
|
| 127 |
+
|
| 128 |
+
def scan_digit_tokens(self):
|
| 129 |
+
vocab=self.tokenizer.get_vocab() if hasattr(self.tokenizer,'get_vocab') else {}
|
| 130 |
+
self.digit_start_ids={token for token in set(vocab.values())
|
| 131 |
+
if re.match(r'^\d',self.decode([token]).lstrip())}
|
| 132 |
+
|
| 133 |
+
def encode(self, text: str) -> list[int]:
|
| 134 |
+
return list(self.tokenizer.encode(text, add_special_tokens=False))
|
| 135 |
+
|
| 136 |
+
def decode(self, ids: Iterable[int]) -> str:
|
| 137 |
+
return self.tokenizer.decode(list(ids), skip_special_tokens=True)
|
| 138 |
+
|
| 139 |
+
def prompt_ids(self, system: str, text: str, enable_thinking: bool = False) -> list[int]:
|
| 140 |
+
# The instructions and the context share one user turn, as in all measured runs.
|
| 141 |
+
messages = [{"role": "user", "content": system + "\n\n" + text}]
|
| 142 |
+
try:
|
| 143 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 144 |
+
messages, tokenize=False, add_generation_prompt=True,
|
| 145 |
+
enable_thinking=enable_thinking,
|
| 146 |
+
)
|
| 147 |
+
except TypeError as exc:
|
| 148 |
+
if "enable_thinking" not in str(exc):
|
| 149 |
+
raise
|
| 150 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 151 |
+
messages, tokenize=False, add_generation_prompt=True,
|
| 152 |
+
)
|
| 153 |
+
return self.encode(prompt)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class TransformersBackend(TokenizerMixin):
|
| 157 |
+
"""Direct single-forward readout, and a separate bounded draft generation."""
|
| 158 |
+
|
| 159 |
+
readout_output_tokens = 0
|
| 160 |
+
|
| 161 |
+
def __init__(self, config: dict[str, Any]):
|
| 162 |
+
model_cfg = config["model"]
|
| 163 |
+
self.generation = dict(config["generation"])
|
| 164 |
+
self.generation_reference=generation_audit(config)
|
| 165 |
+
self.marker = config.get("prompt", {}).get("answer_marker", "ANSWER:")
|
| 166 |
+
if model_cfg["quant"] not in {"int8", "bf16"}:
|
| 167 |
+
raise ValueError("model.quant must be int8 or bf16")
|
| 168 |
+
if not model_cfg["path"]:
|
| 169 |
+
raise ValueError("model.path is required before loading a model")
|
| 170 |
+
try:
|
| 171 |
+
import torch
|
| 172 |
+
import transformers
|
| 173 |
+
except ImportError as exc:
|
| 174 |
+
raise BackendError("transformers backend requires torch and transformers") from exc
|
| 175 |
+
self.torch = torch
|
| 176 |
+
torch.manual_seed(self.generation["seed"])
|
| 177 |
+
# expanduser keeps a Hub identifier's slash intact on Windows.
|
| 178 |
+
path = os.path.expanduser(model_cfg["path"])
|
| 179 |
+
cfg = transformers.AutoConfig.from_pretrained(path)
|
| 180 |
+
declared_quant = getattr(cfg, "quantization_config", None)
|
| 181 |
+
if declared_quant:
|
| 182 |
+
if not isinstance(declared_quant, dict):
|
| 183 |
+
declared_quant = declared_quant.to_dict()
|
| 184 |
+
if model_cfg["quant"] == "bf16":
|
| 185 |
+
raise BackendError("bf16 requires an unquantized checkpoint; base config declares quantization")
|
| 186 |
+
if (declared_quant.get("quant_method") != "bitsandbytes"
|
| 187 |
+
or not declared_quant.get("load_in_8bit")
|
| 188 |
+
or declared_quant.get("load_in_4bit")):
|
| 189 |
+
raise BackendError("int8 requires an unquantized base or a BitsAndBytes 8bit checkpoint")
|
| 190 |
+
archs = list(getattr(cfg, "architectures", None) or [])
|
| 191 |
+
if any("Qwen3_5ForConditionalGeneration" in name for name in archs):
|
| 192 |
+
model_cls = getattr(transformers, "Qwen3_5ForConditionalGeneration", None)
|
| 193 |
+
if model_cls is None:
|
| 194 |
+
raise BackendError("installed transformers lacks Qwen3_5ForConditionalGeneration")
|
| 195 |
+
else:
|
| 196 |
+
model_cls = transformers.AutoModelForCausalLM
|
| 197 |
+
kwargs = {
|
| 198 |
+
"dtype": torch.bfloat16,
|
| 199 |
+
"device_map": model_cfg["device_map"],
|
| 200 |
+
"low_cpu_mem_usage": True,
|
| 201 |
+
"attn_implementation": model_cfg["attn_implementation"],
|
| 202 |
+
}
|
| 203 |
+
if model_cfg["quant"] == "int8":
|
| 204 |
+
kwargs["quantization_config"] = transformers.BitsAndBytesConfig(load_in_8bit=True)
|
| 205 |
+
self.tokenizer = transformers.AutoTokenizer.from_pretrained(path)
|
| 206 |
+
self.scan_digit_tokens()
|
| 207 |
+
self.model = model_cls.from_pretrained(path, **kwargs).eval()
|
| 208 |
+
if model_cfg.get("adapter"):
|
| 209 |
+
try:
|
| 210 |
+
from peft import PeftModel
|
| 211 |
+
except ImportError as exc:
|
| 212 |
+
raise BackendError("--adapter requires peft for the transformers backend") from exc
|
| 213 |
+
self.model = PeftModel.from_pretrained(
|
| 214 |
+
self.model, str(Path(model_cfg["adapter"]).expanduser()),
|
| 215 |
+
).eval()
|
| 216 |
+
# A fresh config prevents checkpoint-specific suppression, forced tokens,
|
| 217 |
+
# sampling filters or minimum-length policies from silently changing the
|
| 218 |
+
# configured draft. Preserve special-token metadata, including EOS lists.
|
| 219 |
+
special = {}
|
| 220 |
+
for name in ("bos_token_id", "eos_token_id", "pad_token_id"):
|
| 221 |
+
for source in (getattr(self.model, "generation_config", None),
|
| 222 |
+
getattr(self.model, "config", None), self.tokenizer):
|
| 223 |
+
value = getattr(source, name, None)
|
| 224 |
+
if value is not None:
|
| 225 |
+
special[name] = value
|
| 226 |
+
break
|
| 227 |
+
self.prefix_caching=False
|
| 228 |
+
install_forward_counter(self)
|
| 229 |
+
self.draft_generation_config = transformers.GenerationConfig(**special, **{k:v for k,v in self.generation.items()
|
| 230 |
+
if k not in ("reasoning_words","seed","max_new_tokens","wall_timeout_seconds")})
|
| 231 |
+
eos = special.get("eos_token_id", [])
|
| 232 |
+
self.eos_ids = eos if isinstance(eos, list) else [eos]
|
| 233 |
+
|
| 234 |
+
def engine_stats(self):
|
| 235 |
+
return {'source':'hf_forward_hook','prefix_caching':False,
|
| 236 |
+
'counters':dict(self.forward_counters) if self.stats_available else {}}
|
| 237 |
+
|
| 238 |
+
def open_session(self, ids):
|
| 239 |
+
return CachedSession(self, ids)
|
| 240 |
+
|
| 241 |
+
def slow_session(self, ids, unparsed_policy, meter, max_new_tokens=None):
|
| 242 |
+
count=_max_new_tokens(max_new_tokens or self.generation['max_new_tokens'])
|
| 243 |
+
tensor=self._input(ids)
|
| 244 |
+
meter['prompt']+=len(ids); meter['engine_submitted']+=len(ids)
|
| 245 |
+
meter['engine_input']=meter.get('engine_input',0)+len(ids)
|
| 246 |
+
meter['engine_calls']=meter.get('engine_calls',0)+1
|
| 247 |
+
meter['hf_prefill_tokens']=meter.get('hf_prefill_tokens',0)+len(ids)
|
| 248 |
+
class MeterStreamer:
|
| 249 |
+
first=True
|
| 250 |
+
def put(inner,value):
|
| 251 |
+
if inner.first: inner.first=False; return
|
| 252 |
+
n=value.shape[-1]
|
| 253 |
+
meter['output']+=n; meter['draft']+=n
|
| 254 |
+
meter['engine_generated']=meter.get('engine_generated',0)+n
|
| 255 |
+
if not getattr(inner,'counted',False):
|
| 256 |
+
meter['generate_nonempty_calls']=meter.get('generate_nonempty_calls',0)+1; inner.counted=True
|
| 257 |
+
def end(inner): pass
|
| 258 |
+
streamer=MeterStreamer()
|
| 259 |
+
g=self.generation
|
| 260 |
+
kwargs=dict(input_ids=tensor,attention_mask=self.torch.ones_like(tensor),
|
| 261 |
+
generation_config=self.draft_generation_config,max_new_tokens=count,
|
| 262 |
+
max_time=g.get("wall_timeout_seconds",60),
|
| 263 |
+
do_sample=g['do_sample'],num_beams=1,num_return_sequences=1,
|
| 264 |
+
repetition_penalty=g['repetition_penalty'],use_cache=True,
|
| 265 |
+
return_dict_in_generate=True,stop_strings=[self.marker],tokenizer=self.tokenizer,
|
| 266 |
+
streamer=streamer,pad_token_id=self.tokenizer.pad_token_id or self.tokenizer.eos_token_id)
|
| 267 |
+
if g['do_sample']: kwargs.update(temperature=g['temperature'],top_p=g['top_p'],top_k=g['top_k'])
|
| 268 |
+
check_deadline(meter)
|
| 269 |
+
try:
|
| 270 |
+
with self.torch.inference_mode(): output=self.model.generate(**kwargs)
|
| 271 |
+
except Exception:
|
| 272 |
+
meter['engine_usage_unknown']=True
|
| 273 |
+
raise
|
| 274 |
+
check_deadline(meter)
|
| 275 |
+
generated=output.sequences[0,len(ids):].tolist()
|
| 276 |
+
# Transformers streams sampled IDs even when the next forward raises.
|
| 277 |
+
if meter['draft']!=len(generated): raise BackendError('Generation streamer accounting mismatch')
|
| 278 |
+
parsed=bool(re.search(r'(?m)^[ \t]*'+re.escape(self.marker)+r'$',self.decode(generated)))
|
| 279 |
+
if not parsed and unparsed_policy=='fast': return None,False
|
| 280 |
+
cache=output.past_key_values
|
| 281 |
+
if cache is None or not generated: raise BackendError('Generation returned no reusable cache')
|
| 282 |
+
session=object.__new__(CachedSession)
|
| 283 |
+
session.backend,session.cache,session.length=self,cache,len(ids)+len(generated)-1
|
| 284 |
+
session._log_probs=None
|
| 285 |
+
session.meter=meter
|
| 286 |
+
# Standard generate cache is one sampled token behind sequences.
|
| 287 |
+
session.advance(generated[-1:])
|
| 288 |
+
if not parsed:
|
| 289 |
+
forced=self.encode('\n'+self.marker)
|
| 290 |
+
meter['output']+=len(forced);meter['forced']+=len(forced)
|
| 291 |
+
session.advance(forced)
|
| 292 |
+
return session,parsed
|
| 293 |
+
|
| 294 |
+
def _input(self, ids: Iterable[int]):
|
| 295 |
+
values = list(ids)
|
| 296 |
+
if not values:
|
| 297 |
+
raise ValueError("prompt token IDs cannot be empty")
|
| 298 |
+
embedding = self.model.get_input_embeddings()
|
| 299 |
+
device = embedding.weight.device
|
| 300 |
+
if getattr(device, "type", None) == "meta":
|
| 301 |
+
device = self.model.device
|
| 302 |
+
return self.torch.tensor([values], dtype=self.torch.long, device=device)
|
| 303 |
+
|
| 304 |
+
def next_logits(self, ids: Iterable[int], candidate_ids: Iterable[int]) -> dict[int, float]:
|
| 305 |
+
candidates = _candidate_ids(candidate_ids)
|
| 306 |
+
tensor = self._input(ids)
|
| 307 |
+
with self.torch.inference_mode():
|
| 308 |
+
try:
|
| 309 |
+
output = self.model(input_ids=tensor, use_cache=False, logits_to_keep=1)
|
| 310 |
+
except TypeError as exc:
|
| 311 |
+
if "logits_to_keep" not in str(exc):
|
| 312 |
+
raise
|
| 313 |
+
output = self.model(input_ids=tensor, use_cache=False)
|
| 314 |
+
logits = output.logits[0, -1, :]
|
| 315 |
+
if max(candidates) >= logits.shape[0]:
|
| 316 |
+
raise BackendError("candidate token ID exceeds the model output vocabulary")
|
| 317 |
+
index = self.torch.tensor(candidates, dtype=self.torch.long, device=logits.device)
|
| 318 |
+
selected = logits.index_select(0, index).float().cpu().tolist()
|
| 319 |
+
return _check_scores(dict(zip(candidates, selected)))
|
| 320 |
+
|
| 321 |
+
def generate(self, ids: Iterable[int], max_new_tokens: int) -> list[int]:
|
| 322 |
+
count = _max_new_tokens(max_new_tokens)
|
| 323 |
+
tensor = self._input(ids)
|
| 324 |
+
generation = self.generation
|
| 325 |
+
pad_id = self.tokenizer.pad_token_id
|
| 326 |
+
if pad_id is None:
|
| 327 |
+
pad_id = self.tokenizer.eos_token_id
|
| 328 |
+
kwargs = {
|
| 329 |
+
"input_ids": tensor,
|
| 330 |
+
"attention_mask": self.torch.ones_like(tensor),
|
| 331 |
+
"generation_config": self.draft_generation_config,
|
| 332 |
+
"max_new_tokens": count,
|
| 333 |
+
"do_sample": generation["do_sample"],
|
| 334 |
+
"num_beams": 1,
|
| 335 |
+
"num_return_sequences": 1,
|
| 336 |
+
"repetition_penalty": generation["repetition_penalty"],
|
| 337 |
+
"use_cache": True,
|
| 338 |
+
"return_dict_in_generate": False,
|
| 339 |
+
"pad_token_id": pad_id,
|
| 340 |
+
}
|
| 341 |
+
if generation["do_sample"]:
|
| 342 |
+
kwargs.update(temperature=generation["temperature"],
|
| 343 |
+
top_p=generation["top_p"], top_k=generation["top_k"])
|
| 344 |
+
with self.torch.inference_mode():
|
| 345 |
+
output = self.model.generate(**kwargs)
|
| 346 |
+
return output[0, tensor.shape[1]:].tolist()
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class VLLMBackend(TokenizerMixin):
|
| 350 |
+
"""Exact candidate readout through explicit raw logprob token IDs.
|
| 351 |
+
|
| 352 |
+
The engine produces one probe token to expose the next-token distribution;
|
| 353 |
+
callers must include readout_output_tokens in measured output-token usage.
|
| 354 |
+
Normal probes gather only explicit candidate IDs. Full-vocabulary output
|
| 355 |
+
is reserved for one logged recovery probe if a backend omits candidates.
|
| 356 |
+
This implementation supports bf16 and an explicitly checked AWQ checkpoint.
|
| 357 |
+
It rejects int8 because vLLM's
|
| 358 |
+
BnB loader settings do not establish equivalence to the reference int8 base.
|
| 359 |
+
"""
|
| 360 |
+
|
| 361 |
+
readout_output_tokens = 1
|
| 362 |
+
engine_metering = True
|
| 363 |
+
|
| 364 |
+
@staticmethod
|
| 365 |
+
def _awq_quant_arg(model_cfg):
|
| 366 |
+
"""AWQ profile: pass quantization='awq' for classic AWQ checkpoints; let vLLM auto-detect
|
| 367 |
+
compressed-tensors (W4A16) checkpoints, whose config.json declares quant_method."""
|
| 368 |
+
if model_cfg["quant"]!="awq": return None
|
| 369 |
+
try:
|
| 370 |
+
import json as _json
|
| 371 |
+
cfg=_json.loads(Path(os.path.join(os.path.expanduser(model_cfg["path"]),"config.json")).read_text(encoding="utf-8"))
|
| 372 |
+
qm=((cfg.get("quantization_config") or {}).get("quant_method") or "").lower()
|
| 373 |
+
if qm in ("compressed-tensors","compressed_tensors"): return None
|
| 374 |
+
except Exception:
|
| 375 |
+
pass
|
| 376 |
+
return "awq"
|
| 377 |
+
|
| 378 |
+
def __init__(self, config: dict[str, Any]):
|
| 379 |
+
model_cfg = config["model"]
|
| 380 |
+
if model_cfg["quant"] not in ("bf16","awq"):
|
| 381 |
+
raise BackendError("vllm backend supports bf16 or AWQ; use transformers for int8")
|
| 382 |
+
if not model_cfg["path"]:
|
| 383 |
+
raise ValueError("model.path is required before loading a model")
|
| 384 |
+
options = dict(model_cfg["vllm_options"])
|
| 385 |
+
if config.get("limits"):
|
| 386 |
+
options.setdefault("max_model_len",config["limits"]["physical_max_prompt_tokens"]+2048)
|
| 387 |
+
reserved = {
|
| 388 |
+
"model", "tokenizer", "dtype", "quantization", "quantization_config",
|
| 389 |
+
"max_logprobs", "logprobs_mode", "seed", "enable_lora",
|
| 390 |
+
"generation_config", "override_generation_config", "logits_processors",
|
| 391 |
+
"skip_tokenizer_init", "speculative_config", "enable_prefix_caching", "disable_log_stats",
|
| 392 |
+
"attention_backend",
|
| 393 |
+
}
|
| 394 |
+
overlap = sorted(reserved.intersection(options))
|
| 395 |
+
if overlap:
|
| 396 |
+
raise ValueError("model.vllm_options cannot override readout contract: " + ", ".join(overlap))
|
| 397 |
+
try:
|
| 398 |
+
from vllm import LLM, SamplingParams
|
| 399 |
+
except ImportError as exc:
|
| 400 |
+
raise BackendError("vllm backend requires an importable compatible vllm installation") from exc
|
| 401 |
+
self.SamplingParams = SamplingParams
|
| 402 |
+
self.marker=config.get("prompt",{}).get("answer_marker","ANSWER:")
|
| 403 |
+
self.lock=threading.RLock()
|
| 404 |
+
self.generation = dict(config["generation"])
|
| 405 |
+
self.generation_reference=generation_audit(config)
|
| 406 |
+
self.lora_request = None
|
| 407 |
+
if model_cfg.get("adapter"):
|
| 408 |
+
try:
|
| 409 |
+
from vllm.lora.request import LoRARequest
|
| 410 |
+
self.lora_request = LoRARequest(
|
| 411 |
+
"adapter", 1, str(Path(model_cfg["adapter"]).expanduser()),
|
| 412 |
+
)
|
| 413 |
+
except (ImportError, TypeError, ValueError) as exc:
|
| 414 |
+
raise BackendError("installed vllm does not support the requested LoRA adapter API") from exc
|
| 415 |
+
# Unset means vLLM selects the attention backend (auto). The supervisor sets
|
| 416 |
+
# JEV_ATTENTION_BACKEND=TRITON_ATTN only for its second startup attempt.
|
| 417 |
+
requested_attention = os.environ.get("JEV_ATTENTION_BACKEND")
|
| 418 |
+
if requested_attention:
|
| 419 |
+
options["attention_backend"] = requested_attention
|
| 420 |
+
try:
|
| 421 |
+
# These are correctness invariants, not configurable routing policy.
|
| 422 |
+
self.engine = LLM(
|
| 423 |
+
model=os.path.expanduser(model_cfg["path"]), dtype="bfloat16",
|
| 424 |
+
quantization=self._awq_quant_arg(model_cfg), seed=self.generation["seed"],
|
| 425 |
+
max_logprobs=-1, logprobs_mode="raw_logprobs",
|
| 426 |
+
generation_config="vllm", enable_lora=bool(self.lora_request), disable_log_stats=False,
|
| 427 |
+
enable_prefix_caching=config.get("model",{}).get("enable_prefix_caching",True), **options,
|
| 428 |
+
)
|
| 429 |
+
# quantization=None permits checkpoint auto-detection in vLLM. Do
|
| 430 |
+
# not label that run bf16 if the checkpoint activates quantization.
|
| 431 |
+
active_model_cfg = self.engine.llm_engine.model_config
|
| 432 |
+
if model_cfg["quant"]=="bf16" and active_model_cfg.quantization is not None:
|
| 433 |
+
raise BackendError("bf16 requires an unquantized checkpoint; vllm activated quantization")
|
| 434 |
+
if model_cfg['quant']=='awq' and active_model_cfg.quantization not in ('awq','awq_marlin','compressed-tensors'):
|
| 435 |
+
raise BackendError('AWQ profile requires active AWQ quantization')
|
| 436 |
+
self.quantization_kernel=active_model_cfg.quantization
|
| 437 |
+
cache_cfg=getattr(getattr(self.engine.llm_engine,'vllm_config',None),'cache_config',None)
|
| 438 |
+
self.prefix_caching=getattr(cache_cfg,'enable_prefix_caching',None)
|
| 439 |
+
self.runtime_info=runtime_info()
|
| 440 |
+
self.tokenizer = self.engine.get_tokenizer()
|
| 441 |
+
self.scan_digit_tokens()
|
| 442 |
+
self.probe_params = SamplingParams(
|
| 443 |
+
max_tokens=1, temperature=1.0, top_p=1.0, top_k=-1,
|
| 444 |
+
min_p=0.0, presence_penalty=0.0, frequency_penalty=0.0,
|
| 445 |
+
repetition_penalty=1.0, logprobs=-1, detokenize=False,
|
| 446 |
+
seed=self.generation["seed"],
|
| 447 |
+
)
|
| 448 |
+
except Exception as exc:
|
| 449 |
+
raise BackendError(
|
| 450 |
+
"vllm initialization failed; selected precision/architecture/adapter and full-vocabulary "
|
| 451 |
+
"raw_logprobs support are required. No approximate fallback is used."
|
| 452 |
+
) from exc
|
| 453 |
+
|
| 454 |
+
def engine_stats(self):
|
| 455 |
+
counters={}
|
| 456 |
+
accepted={'prompt_tokens','generation_tokens','prefix_cache_queries','prefix_cache_hits','num_preemptions','request_success'}
|
| 457 |
+
for metric in self.engine.get_metrics():
|
| 458 |
+
name=metric.name.removeprefix('vllm:').removesuffix('_total')
|
| 459 |
+
if name not in accepted: continue
|
| 460 |
+
value=getattr(metric,'value',None)
|
| 461 |
+
if isinstance(value,(int,float)) and not isinstance(value,bool):
|
| 462 |
+
counters[name]=counters.get(name,0)+value
|
| 463 |
+
return {'source':'vllm_metrics','prefix_caching':self.prefix_caching,'counters':counters}
|
| 464 |
+
|
| 465 |
+
def _completion(self, ids: Iterable[int], sampling_params: Any, meter=None, kind="probes"):
|
| 466 |
+
check_deadline(meter)
|
| 467 |
+
values = list(ids)
|
| 468 |
+
if not values:
|
| 469 |
+
raise ValueError("prompt token IDs cannot be empty")
|
| 470 |
+
kwargs = {"use_tqdm": False}
|
| 471 |
+
if self.lora_request is not None:
|
| 472 |
+
kwargs["lora_request"] = self.lora_request
|
| 473 |
+
try:
|
| 474 |
+
if meter is not None: meter['engine_submitted']+=len(values)
|
| 475 |
+
# TokensPrompt is a TypedDict; its runtime representation is this dict.
|
| 476 |
+
with self.lock:
|
| 477 |
+
check_deadline(meter)
|
| 478 |
+
result = self.engine.generate(
|
| 479 |
+
[{"prompt_token_ids": values}], sampling_params, **kwargs,
|
| 480 |
+
)
|
| 481 |
+
except TimeoutError:
|
| 482 |
+
raise
|
| 483 |
+
except Exception as exc:
|
| 484 |
+
if meter is not None: meter['engine_usage_unknown']=True
|
| 485 |
+
raise BackendError("vllm request failed; verify its architecture, adapter and logprobs support") from exc
|
| 486 |
+
if len(result) != 1 or len(result[0].outputs) != 1:
|
| 487 |
+
raise BackendError("vllm did not return exactly one completion")
|
| 488 |
+
if meter is not None:
|
| 489 |
+
n=len(result[0].outputs[0].token_ids)
|
| 490 |
+
meter['output']+=n;meter[kind]+=n
|
| 491 |
+
cached=getattr(result[0],'num_cached_tokens',None)
|
| 492 |
+
if cached is None: meter['cached_unknown']=True
|
| 493 |
+
else: meter['cached']+=int(cached)
|
| 494 |
+
meter['engine_calls']=meter.get('engine_calls',0)+1
|
| 495 |
+
meter['engine_generated']=meter.get('engine_generated',0)+n
|
| 496 |
+
meter['engine_input']=meter.get('engine_input',0)+len(values)-int(cached or 0)
|
| 497 |
+
check_deadline(meter)
|
| 498 |
+
return result[0].outputs[0]
|
| 499 |
+
|
| 500 |
+
def next_logits(self, ids: Iterable[int], candidate_ids: Iterable[int], meter=None) -> dict[int, float]:
|
| 501 |
+
candidates = _candidate_ids(candidate_ids)
|
| 502 |
+
values = list(ids)
|
| 503 |
+
scores = {}
|
| 504 |
+
# v0.31 supports explicit gather from RAW logprobs (before masks).
|
| 505 |
+
# Chunking preserves the same full-vocabulary normalizer for all IDs.
|
| 506 |
+
for start in range(0, len(candidates), 128):
|
| 507 |
+
wanted = candidates[start:start + 128]
|
| 508 |
+
kwargs = dict(max_tokens=1, temperature=1.0, top_p=1.0, top_k=-1,
|
| 509 |
+
repetition_penalty=1.0, detokenize=False,
|
| 510 |
+
seed=self.generation['seed'])
|
| 511 |
+
try:
|
| 512 |
+
params = self.SamplingParams(**kwargs, allowed_token_ids=wanted,
|
| 513 |
+
logprob_token_ids=wanted, logprobs=len(wanted))
|
| 514 |
+
except TypeError:
|
| 515 |
+
params = self.SamplingParams(**kwargs, logprobs=min(50, max(len(wanted), 1)))
|
| 516 |
+
output = self._completion(values, params, meter)
|
| 517 |
+
distribution = output.logprobs[0] if output.logprobs else None
|
| 518 |
+
if len(output.token_ids) != 1:
|
| 519 |
+
raise BackendError('Probe must produce exactly one token')
|
| 520 |
+
if distribution is None or any(t not in distribution for t in wanted):
|
| 521 |
+
logging.warning('Candidate readout incomplete; one full-vocabulary recovery probe')
|
| 522 |
+
if meter is not None:
|
| 523 |
+
meter.setdefault('warnings', []).append('candidate_full_vocab_recovery')
|
| 524 |
+
output = self._completion(values, self.probe_params, meter)
|
| 525 |
+
distribution = output.logprobs[0] if output.logprobs else None
|
| 526 |
+
if len(output.token_ids) != 1 or distribution is None or any(t not in distribution for t in candidates):
|
| 527 |
+
raise BackendError('vllm omitted candidate token logprobs after recovery')
|
| 528 |
+
return _check_scores({t: float(distribution[t].logprob) for t in candidates})
|
| 529 |
+
scores.update({t: float(distribution[t].logprob) for t in wanted})
|
| 530 |
+
return _check_scores(scores)
|
| 531 |
+
|
| 532 |
+
def open_session(self, ids, meter=None):
|
| 533 |
+
return VLLMPrefixSession(self,ids,meter if meter is not None else new_meter())
|
| 534 |
+
|
| 535 |
+
def slow_session(self, ids, unparsed_policy, meter, max_new_tokens=None):
|
| 536 |
+
count=_max_new_tokens(max_new_tokens or self.generation['max_new_tokens'])
|
| 537 |
+
g=self.generation; sampling=g['do_sample']
|
| 538 |
+
params=self.SamplingParams(max_tokens=count,temperature=g['temperature'] if sampling else 0.0,
|
| 539 |
+
top_p=g['top_p'] if sampling else 1.0,top_k=(g['top_k'] or -1) if sampling else -1,
|
| 540 |
+
repetition_penalty=g['repetition_penalty'],seed=g['seed'],
|
| 541 |
+
stop=[self.marker],include_stop_str_in_output=True)
|
| 542 |
+
meter['prompt']+=len(ids)
|
| 543 |
+
out=self._completion(ids,params,meter,'draft')
|
| 544 |
+
gen=list(out.token_ids)
|
| 545 |
+
parsed=(getattr(out,'stop_reason',None)==self.marker and
|
| 546 |
+
getattr(out,'finish_reason',None)=='stop' and
|
| 547 |
+
bool(re.search(r'(?m)^[ \t]*'+re.escape(self.marker)+r'$',self.decode(gen))))
|
| 548 |
+
if not parsed:
|
| 549 |
+
if unparsed_policy=='fast': return None,False
|
| 550 |
+
forced=self.encode('\n'+self.marker);gen+=forced
|
| 551 |
+
meter['output']+=len(forced);meter['forced']+=len(forced)
|
| 552 |
+
return VLLMPrefixSession(self,list(ids)+gen,meter),parsed
|
| 553 |
+
|
| 554 |
+
def generate(self, ids: Iterable[int], max_new_tokens: int) -> list[int]:
|
| 555 |
+
count = _max_new_tokens(max_new_tokens)
|
| 556 |
+
generation = self.generation
|
| 557 |
+
sampling = generation["do_sample"]
|
| 558 |
+
# HF uses top_k=0 for unrestricted sampling; vLLM uses top_k=-1.
|
| 559 |
+
top_k = generation["top_k"] if generation["top_k"] > 0 else -1
|
| 560 |
+
params = self.SamplingParams(
|
| 561 |
+
max_tokens=count, temperature=generation["temperature"] if sampling else 0.0,
|
| 562 |
+
top_p=generation["top_p"] if sampling else 1.0,
|
| 563 |
+
top_k=top_k if sampling else -1,
|
| 564 |
+
repetition_penalty=generation["repetition_penalty"],
|
| 565 |
+
seed=generation["seed"], detokenize=False,
|
| 566 |
+
)
|
| 567 |
+
output = self._completion(ids, params)
|
| 568 |
+
if len(output.token_ids) > count:
|
| 569 |
+
raise BackendError("vllm generated more tokens than max_new_tokens")
|
| 570 |
+
return list(output.token_ids)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def create_backend(config: dict[str, Any]):
|
| 574 |
+
name = config["model"]["backend"]
|
| 575 |
+
if name == "transformers":
|
| 576 |
+
return TransformersBackend(config)
|
| 577 |
+
if name == "vllm":
|
| 578 |
+
return VLLMBackend(config)
|
| 579 |
+
raise ValueError("model.backend must be transformers or vllm")
|
| 580 |
+
|
| 581 |
+
class CachedSession:
|
| 582 |
+
"""Single prefill followed exclusively by KV-backed token continuations.
|
| 583 |
+
|
| 584 |
+
Branches own their cache, including recurrent attention state. Models that
|
| 585 |
+
cannot copy/cache fail into the service's fast path rather than re-prefill.
|
| 586 |
+
"""
|
| 587 |
+
def __init__(self, backend, ids):
|
| 588 |
+
self.backend = backend
|
| 589 |
+
self.cache = None
|
| 590 |
+
self.length = 0
|
| 591 |
+
self.advance(list(ids))
|
| 592 |
+
|
| 593 |
+
def advance(self, ids):
|
| 594 |
+
if not ids:
|
| 595 |
+
return
|
| 596 |
+
backend = self.backend
|
| 597 |
+
meter=getattr(self,'meter',None)
|
| 598 |
+
if meter is not None:
|
| 599 |
+
check_deadline(meter)
|
| 600 |
+
meter['engine_input']+=len(ids);meter['engine_submitted']+=len(ids)
|
| 601 |
+
kwargs = dict(input_ids=backend._input(ids), use_cache=True, logits_to_keep=1)
|
| 602 |
+
if self.cache is not None:
|
| 603 |
+
kwargs['past_key_values'] = self.cache
|
| 604 |
+
try:
|
| 605 |
+
with backend.torch.inference_mode():
|
| 606 |
+
output = backend.model(**kwargs)
|
| 607 |
+
except Exception:
|
| 608 |
+
if meter is not None:meter['engine_usage_unknown']=True
|
| 609 |
+
raise
|
| 610 |
+
self.cache = output.past_key_values
|
| 611 |
+
if self.cache is None:
|
| 612 |
+
raise BackendError('Model did not return a reusable cache')
|
| 613 |
+
self.length += len(ids)
|
| 614 |
+
self._logits = output.logits[0, -1, :].float()
|
| 615 |
+
self._log_probs = None
|
| 616 |
+
|
| 617 |
+
def top_token(self):
|
| 618 |
+
return int(self._logits.argmax().item())
|
| 619 |
+
|
| 620 |
+
def log_probs(self, ids):
|
| 621 |
+
if self._log_probs is None:
|
| 622 |
+
self._log_probs=self._logits.log_softmax(dim=-1)
|
| 623 |
+
index=self.backend.torch.tensor(list(ids),dtype=self.backend.torch.long,device=self._logits.device)
|
| 624 |
+
return _check_scores(dict(zip(ids,self._log_probs.index_select(0,index).cpu().tolist())))
|
| 625 |
+
|
| 626 |
+
def non_digit_logprob(self, digit_ids):
|
| 627 |
+
if self._log_probs is None: self._log_probs=self._logits.log_softmax(dim=-1)
|
| 628 |
+
ids=[i for i in digit_ids if i<self._logits.shape[0]]
|
| 629 |
+
if not ids: return 0.0
|
| 630 |
+
torch=self.backend.torch
|
| 631 |
+
index=torch.tensor(ids,dtype=torch.long,device=self._logits.device)
|
| 632 |
+
# Sum excluded mass on device; return one scalar, never vocab logits.
|
| 633 |
+
mass=self._log_probs.index_select(0,index).exp().sum().clamp(max=1.0)
|
| 634 |
+
return float(torch.log1p(-mass).item())
|
| 635 |
+
|
| 636 |
+
def fork(self):
|
| 637 |
+
import copy
|
| 638 |
+
branch = object.__new__(type(self))
|
| 639 |
+
branch.backend, branch.length = self.backend, self.length
|
| 640 |
+
branch.meter=getattr(self,'meter',None)
|
| 641 |
+
branch.cache = copy.deepcopy(self.cache)
|
| 642 |
+
branch._logits = self._logits
|
| 643 |
+
branch._log_probs = self._log_probs
|
| 644 |
+
return branch
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def check_deadline(meter):
|
| 648 |
+
if meter and (meter.get('cancel_event') is not None and meter['cancel_event'].is_set() or
|
| 649 |
+
meter.get('deadline') is not None and time.monotonic()>=meter['deadline']):
|
| 650 |
+
raise TimeoutError('Slow deadline exceeded')
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def new_meter():
|
| 654 |
+
return dict(prompt=0,output=0,cached=0,engine_submitted=0,draft=0,forced=0,probes=0,
|
| 655 |
+
engine_calls=0,engine_generated=0,engine_input=0,generate_nonempty_calls=0,hf_prefill_tokens=0)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
class VLLMPrefixSession:
|
| 659 |
+
"""Immutable prefix branches; APC owns KV storage and recomputation."""
|
| 660 |
+
handles_meter=True
|
| 661 |
+
def __init__(self,backend,ids,meter):
|
| 662 |
+
self.backend,self.ids,self.meter=backend,list(ids),meter
|
| 663 |
+
self._logits=None
|
| 664 |
+
|
| 665 |
+
@property
|
| 666 |
+
def logits(self):
|
| 667 |
+
if self._logits is None:
|
| 668 |
+
out=self.backend._completion(self.ids,self.backend.probe_params,self.meter)
|
| 669 |
+
if not out.logprobs or out.logprobs[0] is None:
|
| 670 |
+
raise BackendError('Missing full-vocabulary probe distribution')
|
| 671 |
+
self._logits=_check_scores({t:float(lp.logprob) for t,lp in out.logprobs[0].items()})
|
| 672 |
+
return self._logits
|
| 673 |
+
|
| 674 |
+
def log_probs(self,ids):
|
| 675 |
+
return self.backend.next_logits(self.ids, ids, self.meter)
|
| 676 |
+
|
| 677 |
+
def non_digit_logprob(self, digit_ids):
|
| 678 |
+
if not digit_ids: return 0.0
|
| 679 |
+
scores = self.log_probs(sorted(digit_ids))
|
| 680 |
+
mass = min(1.0, math.fsum(math.exp(v) for v in scores.values()))
|
| 681 |
+
return math.log1p(-mass) if mass < 1 else -math.inf
|
| 682 |
+
|
| 683 |
+
def fork(self):
|
| 684 |
+
branch=VLLMPrefixSession(self.backend,self.ids,self.meter)
|
| 685 |
+
branch._logits=self._logits
|
| 686 |
+
return branch
|
| 687 |
+
|
| 688 |
+
def advance(self,ids):
|
| 689 |
+
# These are candidate continuation tokens, separate from engine probes.
|
| 690 |
+
self.ids+=list(ids);self._logits=None
|
| 691 |
+
self.meter['output']+=len(ids);self.meter['probes']+=len(ids)
|
service/bench.py
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sequential, model-free HTTP benchmark client for the /v1/systemone service (maintainer tool).
|
| 2 |
+
|
| 3 |
+
All outputs are created with exclusive mode at an absolute --output-prefix outside the package.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import statistics
|
| 10 |
+
import time
|
| 11 |
+
import urllib.parse
|
| 12 |
+
import urllib.request
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parent
|
| 17 |
+
PUBLIC_FILES = (("easy.jsonl", "easy"), ("original.jsonl", "standard"), ("hard.jsonl", "hard"))
|
| 18 |
+
TYPES = ("noul", "choice", "score")
|
| 19 |
+
ALIASES = {"boolean": "noul", "enum": "choice"}
|
| 20 |
+
QUESTION_NAME = "decision"
|
| 21 |
+
# Local service calls never go through an HTTP proxy.
|
| 22 |
+
_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def canonical_type(value):
|
| 26 |
+
value = ALIASES.get(value, value)
|
| 27 |
+
if value not in TYPES:
|
| 28 |
+
raise ValueError("unsupported question type")
|
| 29 |
+
return value
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _read_rows(path):
|
| 33 |
+
with Path(path).expanduser().open(encoding="utf-8-sig") as source:
|
| 34 |
+
for line_number, line in enumerate(source, 1):
|
| 35 |
+
if not line.strip():
|
| 36 |
+
continue
|
| 37 |
+
try:
|
| 38 |
+
row = json.loads(line)
|
| 39 |
+
except (ValueError, TypeError) as exc:
|
| 40 |
+
raise ValueError(f"invalid JSONL at line {line_number}") from exc
|
| 41 |
+
if not isinstance(row, dict):
|
| 42 |
+
raise ValueError(f"JSONL line {line_number} must be an object")
|
| 43 |
+
yield row
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def normalize_task(task, tier=None, require_expected=False):
|
| 47 |
+
"""Keep the source's label order, including choice tie-breaking order."""
|
| 48 |
+
if not isinstance(task.get("id"), str) or not task["id"]:
|
| 49 |
+
raise ValueError("task id must be a nonempty string")
|
| 50 |
+
if "state" not in task or not isinstance(task.get("question"), dict):
|
| 51 |
+
raise ValueError("task needs state and question")
|
| 52 |
+
question = dict(task["question"])
|
| 53 |
+
qtype = canonical_type(question.get("type"))
|
| 54 |
+
question["request_type"] = question.get("request_type",question["type"])
|
| 55 |
+
question["type"] = qtype
|
| 56 |
+
instructions = question.get("instructions", question.get("description"))
|
| 57 |
+
if not isinstance(instructions, str) or not instructions.strip():
|
| 58 |
+
raise ValueError("question needs nonempty instructions or description")
|
| 59 |
+
question["instructions"] = instructions
|
| 60 |
+
question.pop("description", None)
|
| 61 |
+
criteria = question.get("criteria")
|
| 62 |
+
if qtype == "noul":
|
| 63 |
+
if criteria is not None and not isinstance(criteria, dict):
|
| 64 |
+
raise ValueError("noul criteria must be an object")
|
| 65 |
+
labels = ["no", "yes"]
|
| 66 |
+
elif qtype == "score":
|
| 67 |
+
if not isinstance(criteria, list) or not criteria:
|
| 68 |
+
raise ValueError("score criteria must be a nonempty list")
|
| 69 |
+
labels = [str(index) for index in range(len(criteria))]
|
| 70 |
+
else:
|
| 71 |
+
labels = task.get("labels", question.get("labels"))
|
| 72 |
+
if labels is None and isinstance(criteria, dict):
|
| 73 |
+
labels = list(criteria)
|
| 74 |
+
if labels is None and isinstance(criteria, list):
|
| 75 |
+
labels = criteria
|
| 76 |
+
if not isinstance(labels, list) or not 1 <= len(labels) <= 676:
|
| 77 |
+
raise ValueError("choice requires between 1 and 676 labels")
|
| 78 |
+
labels = list(labels)
|
| 79 |
+
if any(not isinstance(label, str) or not label for label in labels):
|
| 80 |
+
raise ValueError("choice labels must be nonempty strings")
|
| 81 |
+
if len(set(labels)) != len(labels):
|
| 82 |
+
raise ValueError("choice labels must be distinct")
|
| 83 |
+
question["labels"] = labels
|
| 84 |
+
expected = task.get("expected")
|
| 85 |
+
if expected is not None:
|
| 86 |
+
if qtype == "noul" and isinstance(expected, bool):
|
| 87 |
+
expected = "yes" if expected else "no"
|
| 88 |
+
elif qtype == "score":
|
| 89 |
+
expected = str(expected)
|
| 90 |
+
if expected not in labels:
|
| 91 |
+
raise ValueError("expected answer is not a valid label")
|
| 92 |
+
elif require_expected:
|
| 93 |
+
raise ValueError("public source task is missing expected answer")
|
| 94 |
+
result = dict(task)
|
| 95 |
+
result.update(question=question, type=qtype, labels=labels, expected=expected)
|
| 96 |
+
result["tier"] = tier if tier is not None else task.get("tier", "custom")
|
| 97 |
+
return result
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def load_jsonl_tasks(path):
|
| 101 |
+
tasks = [normalize_task(row) for row in _read_rows(path)]
|
| 102 |
+
_require_unique(tasks)
|
| 103 |
+
if not tasks:
|
| 104 |
+
raise ValueError("input contains no tasks")
|
| 105 |
+
return tasks
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _require_unique(rows):
|
| 109 |
+
seen = set()
|
| 110 |
+
for row in rows:
|
| 111 |
+
if row["id"] in seen:
|
| 112 |
+
raise ValueError("duplicate task id")
|
| 113 |
+
seen.add(row["id"])
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def load_public_tasks(index, public, expected_count):
|
| 117 |
+
"""Use only id/type/tier from the index; all task content comes from public."""
|
| 118 |
+
metadata = []
|
| 119 |
+
for cached in _read_rows(index):
|
| 120 |
+
# Do not use cached expected labels, logits, predictions or prompt content.
|
| 121 |
+
item = {key: cached.get(key) for key in ("id", "type", "tier")}
|
| 122 |
+
if not isinstance(item["id"], str) or not item["id"]:
|
| 123 |
+
raise ValueError("index id must be a nonempty string")
|
| 124 |
+
item["type"] = canonical_type(item["type"])
|
| 125 |
+
if item["tier"] not in {tier for _, tier in PUBLIC_FILES}:
|
| 126 |
+
raise ValueError("index tier is not a public tier")
|
| 127 |
+
metadata.append(item)
|
| 128 |
+
_require_unique(metadata)
|
| 129 |
+
if len(metadata) != expected_count:
|
| 130 |
+
raise ValueError("index task count does not match expected_public_count")
|
| 131 |
+
by_id = {}
|
| 132 |
+
for filename, tier in PUBLIC_FILES:
|
| 133 |
+
for row in _read_rows(Path(public).expanduser() / filename):
|
| 134 |
+
if "tier" in row and row["tier"] != tier:
|
| 135 |
+
raise ValueError("public task tier conflicts with its source file")
|
| 136 |
+
task = normalize_task(row, tier=tier, require_expected=True)
|
| 137 |
+
if task["id"] in by_id:
|
| 138 |
+
raise ValueError("duplicate public task id")
|
| 139 |
+
by_id[task["id"]] = task
|
| 140 |
+
selected = []
|
| 141 |
+
for meta in metadata:
|
| 142 |
+
task = by_id.get(meta["id"])
|
| 143 |
+
if task is None:
|
| 144 |
+
raise ValueError("indexed task missing from public sources")
|
| 145 |
+
if task["type"] != meta["type"] or task["tier"] != meta["tier"]:
|
| 146 |
+
raise ValueError("indexed type or tier does not match public source")
|
| 147 |
+
selected.append(task)
|
| 148 |
+
return selected
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def request_payload(task, original_types=False, model=None, label_order=False):
|
| 152 |
+
question=task['question']
|
| 153 |
+
q={'type':question['type'],'instructions':question['instructions']}
|
| 154 |
+
if question.get('criteria') is not None: q['criteria']=question['criteria']
|
| 155 |
+
if original_types:q['type']=question.get('request_type',question['type'])
|
| 156 |
+
if label_order and task['type']=='choice': q['labels']=list(task['labels'])
|
| 157 |
+
return {'state':task['state'],'model':model,'questions':{QUESTION_NAME:q}}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def endpoint_url(endpoint):
|
| 161 |
+
parts = urllib.parse.urlsplit(endpoint)
|
| 162 |
+
if parts.scheme not in {"http", "https"} or not parts.netloc or parts.query or parts.fragment:
|
| 163 |
+
raise ValueError("endpoint must be an HTTP base URL or the systemone route")
|
| 164 |
+
if parts.username or parts.password:
|
| 165 |
+
raise ValueError("endpoint must not contain credentials")
|
| 166 |
+
path = parts.path.rstrip("/")
|
| 167 |
+
if path not in {"", "/v1/systemone"}:
|
| 168 |
+
raise ValueError("endpoint path must be empty or /v1/systemone")
|
| 169 |
+
return urllib.parse.urlunsplit((parts.scheme, parts.netloc, "/v1/systemone", "", ""))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def post_json(endpoint, payload, timeout):
|
| 173 |
+
request = urllib.request.Request(endpoint_url(endpoint),
|
| 174 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 175 |
+
headers={"Content-Type": "application/json"}, method="POST")
|
| 176 |
+
with _OPENER.open(request, timeout=timeout) as response:
|
| 177 |
+
return json.load(response)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def parse_prediction(response, task):
|
| 181 |
+
if not isinstance(response, dict) or not isinstance(response.get("answers"), dict):
|
| 182 |
+
raise ValueError("response must contain answers")
|
| 183 |
+
if not isinstance(response.get("model"), str) or not response["model"]:
|
| 184 |
+
raise ValueError("response must identify its model")
|
| 185 |
+
answer = response["answers"].get(QUESTION_NAME)
|
| 186 |
+
if not isinstance(answer, dict) or canonical_type(answer.get("type")) != task["type"]:
|
| 187 |
+
raise ValueError("response question type mismatch")
|
| 188 |
+
if answer.get("warning") or response.get("partial_errors"):
|
| 189 |
+
raise ValueError("degraded API response")
|
| 190 |
+
probabilities = answer.get("probabilities")
|
| 191 |
+
if task["type"]=="noul" and isinstance(probabilities,dict) and set(probabilities)=={"true","false"}:
|
| 192 |
+
probabilities={"yes":probabilities["true"],"no":probabilities["false"]}
|
| 193 |
+
if not isinstance(probabilities, dict) or set(probabilities) != set(task["labels"]):
|
| 194 |
+
raise ValueError("response probabilities must have exactly the requested labels")
|
| 195 |
+
values = list(probabilities.values())
|
| 196 |
+
if any(isinstance(value, bool) or not isinstance(value, (int, float))
|
| 197 |
+
or not math.isfinite(value) or value < 0 or value > 1 for value in values):
|
| 198 |
+
raise ValueError("response contains invalid probabilities")
|
| 199 |
+
if not math.isclose(sum(values), 1.0, rel_tol=1e-6, abs_tol=1e-6):
|
| 200 |
+
raise ValueError("response probabilities must sum to one")
|
| 201 |
+
# Match official score_task: lexicographically smallest label wins ties.
|
| 202 |
+
return max(sorted(task["labels"]), key=lambda label: probabilities[label]), probabilities
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _token_count(value):
|
| 206 |
+
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _safe_usage_json(value):
|
| 210 |
+
"""Keep malformed optional metadata from making result JSON unwritable."""
|
| 211 |
+
if value is None or isinstance(value, (str, bool, int)):
|
| 212 |
+
return value, False
|
| 213 |
+
if isinstance(value, float):
|
| 214 |
+
return (value, False) if math.isfinite(value) else (None, True)
|
| 215 |
+
if isinstance(value, list):
|
| 216 |
+
parts = [_safe_usage_json(item) for item in value]
|
| 217 |
+
return [item for item, _ in parts], any(changed for _, changed in parts)
|
| 218 |
+
if isinstance(value, dict):
|
| 219 |
+
result, changed = {}, False
|
| 220 |
+
for key, item in value.items():
|
| 221 |
+
if not isinstance(key, str):
|
| 222 |
+
changed = True
|
| 223 |
+
continue
|
| 224 |
+
result[key], item_changed = _safe_usage_json(item)
|
| 225 |
+
changed = changed or item_changed
|
| 226 |
+
return result, changed
|
| 227 |
+
return None, True
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def extract_usage(response):
|
| 231 |
+
usage = response.get("usage") if isinstance(response, dict) else None
|
| 232 |
+
usage = usage if isinstance(usage, dict) else {}
|
| 233 |
+
usage, sanitized = _safe_usage_json(usage)
|
| 234 |
+
questions = usage.get("questions", {})
|
| 235 |
+
detail = questions.get(QUESTION_NAME, {}) if isinstance(questions, dict) else {}
|
| 236 |
+
detail = detail if isinstance(detail, dict) else {}
|
| 237 |
+
prompt = _token_count(usage.get("input_tokens", usage.get("prompt_tokens")))
|
| 238 |
+
completion = _token_count(usage.get("output_tokens", usage.get("completion_tokens")))
|
| 239 |
+
total = _token_count(usage.get("total_tokens"))
|
| 240 |
+
n_new_tokens = _token_count(detail.get("n_new_tokens"))
|
| 241 |
+
complete = prompt is not None and completion is not None and total is not None
|
| 242 |
+
complete = complete and total == prompt + completion
|
| 243 |
+
complete = complete and n_new_tokens is not None
|
| 244 |
+
if usage.get("accounting") is None:complete=complete and n_new_tokens<=completion if complete else False
|
| 245 |
+
complete = complete and not sanitized and not detail.get("usage_estimated", False) and not usage.get("usage_estimated",False)
|
| 246 |
+
path = detail.get("path")
|
| 247 |
+
if not isinstance(path, str) or path not in {"fast", "slow"}:
|
| 248 |
+
path = None
|
| 249 |
+
router_score = detail.get("router_score")
|
| 250 |
+
if isinstance(router_score, bool) or not isinstance(router_score, (int, float)) or not math.isfinite(router_score):
|
| 251 |
+
router_score = None
|
| 252 |
+
return {"usage": usage, "prompt_tokens": prompt, "completion_tokens": completion,
|
| 253 |
+
"total_tokens": total, "usage_complete": bool(complete), "usage_sanitized": sanitized,
|
| 254 |
+
"n_new_tokens": n_new_tokens, "path": path, "router_score": router_score,
|
| 255 |
+
"fast_prompt_tokens":detail.get("fast_prompt_tokens"), "prefix_caching":response.get("prefix_caching"),
|
| 256 |
+
"fast_scores":detail.get("fast_scores",detail.get("raw_label_scores")),
|
| 257 |
+
"slow_scores":detail.get("slow_scores",detail.get("slow_raw_label_scores")),
|
| 258 |
+
"slow_raw_label_scores":detail.get("slow_raw_label_scores"),
|
| 259 |
+
"calibration_block":detail.get("calibration_block"),"threshold_identity":detail.get("threshold_identity"),
|
| 260 |
+
"budget_tier":detail.get("budget_tier"), "cost_fuse":detail.get("cost_fuse"),
|
| 261 |
+
"raw_label_scores":detail.get("raw_label_scores"), "fast_margin":detail.get("fast_margin"), "margin_kind":detail.get("margin_kind")}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def run_benchmark(tasks, endpoint, config, client=post_json, clock=time.perf_counter, writer=None, concurrency=1):
|
| 265 |
+
"""One request per task, no retries; ordered rows, latency includes each call."""
|
| 266 |
+
if type(concurrency) is not int or concurrency<1: raise ValueError('concurrency must be positive')
|
| 267 |
+
if concurrency>1:
|
| 268 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 269 |
+
def one(task): return run_benchmark([task],endpoint,config,client,clock)[0]
|
| 270 |
+
with ThreadPoolExecutor(max_workers=concurrency) as pool:
|
| 271 |
+
rows=list(pool.map(one,tasks))
|
| 272 |
+
for row in rows: row["concurrency"]=concurrency
|
| 273 |
+
if writer is not None:
|
| 274 |
+
for row in rows: writer.write(json.dumps(row,ensure_ascii=False,allow_nan=False)+'\n')
|
| 275 |
+
writer.flush()
|
| 276 |
+
return rows
|
| 277 |
+
rows = []
|
| 278 |
+
timeout = config["bench"]["timeout_seconds"]
|
| 279 |
+
for task in tasks:
|
| 280 |
+
row = {"request_order":"labels" if config.get("bench",{}).get("label_order",False) else "criteria", "id": task["id"], "type": task["type"], "tier": task.get("tier"),
|
| 281 |
+
"benchmark_set":task.get("benchmark_set"), "concurrency":concurrency, "expected": task.get("expected"), "labels": task["labels"], "pred": None,
|
| 282 |
+
"probabilities": None, "ok": False, "error": None, "model": None}
|
| 283 |
+
row.update(extract_usage({}))
|
| 284 |
+
start = clock()
|
| 285 |
+
try:
|
| 286 |
+
response = client(endpoint, request_payload(task,config.get("bench",{}).get("original_types",False),config["model"]["name"],config.get("bench",{}).get("label_order",False)), timeout)
|
| 287 |
+
except Exception as exc:
|
| 288 |
+
# Exception text can include an endpoint or credentials; never save it.
|
| 289 |
+
row["error"] = {"type": type(exc).__name__, "message": "request failed"}
|
| 290 |
+
row["latency_seconds"] = clock() - start
|
| 291 |
+
else:
|
| 292 |
+
row["latency_seconds"] = clock() - start
|
| 293 |
+
row.update(extract_usage(response))
|
| 294 |
+
if isinstance(response, dict) and isinstance(response.get("model"), str):
|
| 295 |
+
row["model"] = response["model"]
|
| 296 |
+
try:
|
| 297 |
+
row["pred"], row["probabilities"] = parse_prediction(response, task)
|
| 298 |
+
row["ok"] = True
|
| 299 |
+
except (ValueError, KeyError, TypeError) as exc:
|
| 300 |
+
row["error"] = {"type": type(exc).__name__, "message": "invalid API response"}
|
| 301 |
+
row["correct"] = bool(row["ok"] and row["pred"] == row["expected"]) if row["expected"] is not None else None
|
| 302 |
+
rows.append(row)
|
| 303 |
+
if writer is not None:
|
| 304 |
+
writer.write(json.dumps(row, ensure_ascii=False, allow_nan=False) + "\n")
|
| 305 |
+
writer.flush()
|
| 306 |
+
return rows
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def percentile(values, fraction):
|
| 310 |
+
if not values:
|
| 311 |
+
return None
|
| 312 |
+
ordered = sorted(values)
|
| 313 |
+
rank = (len(ordered) - 1) * fraction
|
| 314 |
+
low, high = math.floor(rank), math.ceil(rank)
|
| 315 |
+
return ordered[low] + (ordered[high] - ordered[low]) * (rank - low)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def summarize(rows, config, expected_count=None):
|
| 319 |
+
count = len(rows)
|
| 320 |
+
expected_count = count if expected_count is None else expected_count
|
| 321 |
+
successes = sum(bool(row["ok"]) for row in rows)
|
| 322 |
+
complete = count > 0 and count == expected_count and successes == count
|
| 323 |
+
usage_complete = count > 0 and all(row["usage_complete"] for row in rows)
|
| 324 |
+
routing_complete = count > 0 and all(row["path"] in {"fast", "slow"} for row in rows)
|
| 325 |
+
new_tokens_complete = count > 0 and all(row["n_new_tokens"] is not None for row in rows)
|
| 326 |
+
latencies = [row["latency_seconds"] for row in rows]
|
| 327 |
+
p50 = statistics.median(latencies) if latencies else None
|
| 328 |
+
price_key = config["pricing"]["model_key"]
|
| 329 |
+
observed_models = sorted({row["model"] for row in rows if isinstance(row.get("model"), str) and row["model"]})
|
| 330 |
+
price_model_matches = count > 0 and all(config["pricing"].get("submission_models",{}).get(row.get("model"),row.get("model")) == price_key for row in rows)
|
| 331 |
+
prices = config["pricing"]["models"].get(price_key, {})
|
| 332 |
+
input_price = prices.get("input_per_million")
|
| 333 |
+
output_price = prices.get("output_per_million")
|
| 334 |
+
price_known = all(isinstance(value, (int, float)) and not isinstance(value, bool)
|
| 335 |
+
and math.isfinite(value) and value >= 0 for value in (input_price, output_price))
|
| 336 |
+
total_prompt = sum(row["prompt_tokens"] for row in rows) if usage_complete else None
|
| 337 |
+
total_completion = sum(row["completion_tokens"] for row in rows) if usage_complete else None
|
| 338 |
+
cost = (total_prompt * input_price + total_completion * output_price) / 1_000_000 if usage_complete and price_known and price_model_matches else None
|
| 339 |
+
cost_per_1000 = cost / count * 1000 if cost is not None else None
|
| 340 |
+
by_type = {}
|
| 341 |
+
for qtype in TYPES:
|
| 342 |
+
subset = [row for row in rows if row["type"] == qtype]
|
| 343 |
+
labeled = [row for row in subset if row["expected"] is not None]
|
| 344 |
+
correct = sum(row["correct"] is True for row in labeled)
|
| 345 |
+
by_type[qtype] = {"count": len(subset), "labeled_count": len(labeled), "correct": correct,
|
| 346 |
+
"accuracy": correct / len(subset) if subset and len(labeled) == len(subset) else None}
|
| 347 |
+
limits = config["limits"]
|
| 348 |
+
scale=config.get('bench',{}).get('latency_scale',1)
|
| 349 |
+
offset=config.get('bench',{}).get('latency_offset_seconds',0)
|
| 350 |
+
scaled_p50=p50*scale+offset if p50 is not None else None
|
| 351 |
+
margin=config.get('pricing',{}).get('margin',0)
|
| 352 |
+
if not isinstance(margin,(float,int)) or not 0<=margin<1: raise ValueError('pricing.margin must be in [0,1)')
|
| 353 |
+
cost_limit=limits['cost_per_1000_decisions_usd']*(1-margin)
|
| 354 |
+
eligible = None
|
| 355 |
+
if complete and usage_complete and routing_complete and new_tokens_complete and cost_per_1000 is not None:
|
| 356 |
+
eligible = scaled_p50 <= limits["median_latency_seconds"] and cost_per_1000 <= cost_limit
|
| 357 |
+
slow_share = sum(row["path"] == "slow" for row in rows) / count if routing_complete else None
|
| 358 |
+
fast_q = 0.5 / (1 - slow_share) if slow_share is not None and slow_share < 1 else None
|
| 359 |
+
fast_latencies = [row["latency_seconds"] for row in rows if row["path"] == "fast" and row["ok"]]
|
| 360 |
+
path_stats = {}
|
| 361 |
+
for path in ("fast", "slow"):
|
| 362 |
+
subset = [row for row in rows if row["path"] == path and row["ok"]]
|
| 363 |
+
times = [row["latency_seconds"] for row in subset]
|
| 364 |
+
path_stats[path] = {"count":len(subset), "p50":percentile(times, .5),
|
| 365 |
+
"p75":percentile(times,.75),"mean":statistics.mean(times) if times else None,
|
| 366 |
+
"scaled_p75":percentile(times,.75)*scale+offset if times else None,
|
| 367 |
+
"scaled_mean":statistics.mean(times)*scale+offset if times else None,
|
| 368 |
+
"p90":percentile(times, .9), "p99":percentile(times, .99),
|
| 369 |
+
'average_prompt_tokens':statistics.mean(r['prompt_tokens'] for r in subset) if subset and all(r['prompt_tokens'] is not None for r in subset) else None,
|
| 370 |
+
'average_completion_tokens':statistics.mean(r['completion_tokens'] for r in subset) if subset and all(r['completion_tokens'] is not None for r in subset) else None}
|
| 371 |
+
tail = config.get("bench", {}).get("long_tail", {"count":23, "prompt_tokens":79000, "total_decisions":1624})
|
| 372 |
+
tail_kept = min(tail["prompt_tokens"], limits.get("max_prompt_tokens", 16000))
|
| 373 |
+
tail_cost = tail["count"] * tail_kept * input_price / 1e6 if price_known else None
|
| 374 |
+
projection = {"count":tail["count"], "original_prompt_tokens_each":tail["prompt_tokens"],
|
| 375 |
+
"kept_prompt_tokens_each":tail_kept, "path":"fast", "output_tokens_each":0,
|
| 376 |
+
"estimated_cost_usd":tail_cost,
|
| 377 |
+
"untruncated_fast_cost_usd":tail["count"] * tail["prompt_tokens"] * input_price / 1e6 if price_known else None,
|
| 378 |
+
"assumption":"fast-only long-tail projection; final physical guard can discard protected content",
|
| 379 |
+
"projected_cost_per_1000_decisions_usd":
|
| 380 |
+
((tail["total_decisions"]-tail["count"]) * cost/count + tail_cost) / tail["total_decisions"] * 1000
|
| 381 |
+
if cost is not None and count and tail_cost is not None else None}
|
| 382 |
+
cost_basis = {'A':cost_per_1000,'B':None,'C':None}
|
| 383 |
+
accounting_details = [row.get('usage',{}).get('prompt_tokens_details') for row in rows]
|
| 384 |
+
if cost is not None and count and all(isinstance(d,dict) and
|
| 385 |
+
all(type(d.get(k)) is int and d[k]>=0 for k in ('cached_tokens','engine_submitted_tokens'))
|
| 386 |
+
and d['cached_tokens']<=d['engine_submitted_tokens'] for d in accounting_details):
|
| 387 |
+
submitted=sum(d['engine_submitted_tokens'] for d in accounting_details)
|
| 388 |
+
cached=sum(d['cached_tokens'] for d in accounting_details)
|
| 389 |
+
cost_basis['B']=(submitted*input_price+total_completion*output_price)/1e6/count*1000
|
| 390 |
+
if all(d.get('cached_tokens_known',True) for d in accounting_details):
|
| 391 |
+
cost_basis['C']=((submitted-cached)*input_price+total_completion*output_price)/1e6/count*1000
|
| 392 |
+
for name,legacy in [('engine','C'),('each_once','A'),('submitted','B')]:
|
| 393 |
+
values=[r.get('usage',{}).get('alternatives',{}).get(name) for r in rows]
|
| 394 |
+
if cost is not None and count and all(isinstance(v,dict) for v in values):
|
| 395 |
+
cost_basis[legacy]=sum(v['input_tokens']*input_price+v['output_tokens']*output_price for v in values)/1e6/count*1000
|
| 396 |
+
cost_basis[name]=cost_basis[legacy]
|
| 397 |
+
tail_tiers={}
|
| 398 |
+
for budget in tail.get('projection_budgets',[]):
|
| 399 |
+
extra=tail['count']*budget*input_price/1e6 if price_known else None
|
| 400 |
+
tail_tiers[str(budget)]={'count':tail['count'],'prompt_tokens_each':budget,
|
| 401 |
+
'fast_only_estimated_cost_usd':extra,
|
| 402 |
+
'projected_cost_per_1000_by_basis':{k:((tail['total_decisions']-tail['count'])*v/1000+extra)
|
| 403 |
+
/tail['total_decisions']*1000 if v is not None and extra is not None else None for k,v in cost_basis.items()}}
|
| 404 |
+
speed_rows=[r for r in rows if r.get('benchmark_set')=='open-set' and r.get('tier') in ('standard','judge') and r.get('concurrency',1)==1]
|
| 405 |
+
speed={'count':len(speed_rows),'scope':'explicit open-set standard+judge serial',
|
| 406 |
+
'scaled_p50':None,'scaled_p95':None,'complete':bool(speed_rows) and all(r['ok'] for r in speed_rows)}
|
| 407 |
+
if speed['complete']:
|
| 408 |
+
speed.update(scaled_p50=percentile([r['latency_seconds'] for r in speed_rows],.5)*scale+offset,
|
| 409 |
+
scaled_p95=percentile([r['latency_seconds'] for r in speed_rows],.95)*scale+offset)
|
| 410 |
+
q_latency=percentile(fast_latencies,fast_q) if fast_q is not None and fast_q<=1 else None
|
| 411 |
+
budget_stats={}
|
| 412 |
+
for tier in ('normal','hard','long'):
|
| 413 |
+
subset=[r for r in rows if r.get('budget_tier')==tier]
|
| 414 |
+
labeled=[r for r in subset if r.get('correct') is not None]
|
| 415 |
+
known=bool(subset) and price_known and all(r['usage_complete'] for r in subset)
|
| 416 |
+
budget_stats[tier]={'count':len(subset),'accuracy':sum(r['correct'] for r in labeled)/len(labeled) if labeled else None,
|
| 417 |
+
'cost_usd':sum(r['prompt_tokens']*input_price+r['completion_tokens']*output_price for r in subset)/1e6 if known else None}
|
| 418 |
+
by_tier={}
|
| 419 |
+
for tier in sorted({str(r.get('tier')) for r in rows}):
|
| 420 |
+
subset=[r for r in rows if str(r.get('tier'))==tier];labeled=[r for r in subset if r.get('correct') is not None]
|
| 421 |
+
by_tier[tier]={'count':len(subset),'accuracy':sum(r['correct'] for r in labeled)/len(labeled) if labeled else None}
|
| 422 |
+
p_projection=project_cost(rows,config,config.get('bench',{}).get('projection_prompt_tokens'))
|
| 423 |
+
accuracy_only=config.get('bench',{}).get('accuracy_only',False)
|
| 424 |
+
if accuracy_only:eligible=None
|
| 425 |
+
return {"accuracy_only":accuracy_only,"by_tier":by_tier,"P_projection":p_projection,
|
| 426 |
+
"cost_gate":('UNKNOWN' if cost_per_1000 is None or accuracy_only else 'PASS' if cost_per_1000<=.85*cost_limit else 'WARN' if cost_per_1000<=cost_limit else 'FAIL'),
|
| 427 |
+
"p75_latency_seconds":percentile(latencies,.75),"mean_latency_seconds":statistics.mean(latencies) if latencies else None,
|
| 428 |
+
"scaled_p75_latency_seconds":percentile(latencies,.75)*scale+offset if latencies else None,
|
| 429 |
+
"scaled_mean_latency_seconds":statistics.mean(latencies)*scale+offset if latencies else None,
|
| 430 |
+
"by_budget_tier":budget_stats,"official_speed_subset":speed,"fast_required_quantile_meets_raw_limit":q_latency<=limits.get('raw_median_latency_seconds',.54) if q_latency is not None else None,
|
| 431 |
+
"count": count, "expected_count": expected_count, "successful_count": successes,
|
| 432 |
+
"failed_count": count - successes, "complete": complete,
|
| 433 |
+
"by_path":path_stats, "long_tail_projection":projection,
|
| 434 |
+
"cost_per_1000_by_basis":cost_basis, "long_tail_by_budget":tail_tiers,
|
| 435 |
+
"fast_required_quantile":fast_q,
|
| 436 |
+
"fast_required_quantile_latency_seconds":percentile(fast_latencies, fast_q) if fast_q is not None and fast_q <= 1 else None,
|
| 437 |
+
"fast_quantile_assumption":"all slow decisions exceed median target; successful fast samples only",
|
| 438 |
+
"successful_p50_latency_seconds":percentile([r["latency_seconds"] for r in rows if r["ok"]], .5),
|
| 439 |
+
"latency_scale":scale,"latency_offset_seconds":offset,"scaled_p50_latency_seconds":scaled_p50,
|
| 440 |
+
"pricing_margin":margin,"cost_limit_after_margin":cost_limit,
|
| 441 |
+
"cost_headroom_usd_per_1000":cost_limit-cost_per_1000 if cost_per_1000 is not None else None,
|
| 442 |
+
"latency_scope": "all_attempts_including_failures", "p50_latency_seconds": p50,
|
| 443 |
+
"p99_latency_seconds": percentile(latencies, 0.99),
|
| 444 |
+
"slow_count": sum(row["path"] == "slow" for row in rows),
|
| 445 |
+
"slow_share": sum(row["path"] == "slow" for row in rows) / count if routing_complete else None,
|
| 446 |
+
"usage_complete": usage_complete, "routing_complete": routing_complete,
|
| 447 |
+
"average_prompt_tokens": total_prompt / count if usage_complete else None,
|
| 448 |
+
"average_completion_tokens": total_completion / count if usage_complete else None,
|
| 449 |
+
"average_total_tokens": (total_prompt + total_completion) / count if usage_complete else None,
|
| 450 |
+
"average_new_tokens": sum(row["n_new_tokens"] for row in rows) / count if new_tokens_complete else None,
|
| 451 |
+
"pricing_model_key": price_key, "observed_models": observed_models,
|
| 452 |
+
"price_model_matches": price_model_matches, "reference_prices": prices,
|
| 453 |
+
"estimated_total_cost_usd": cost, "estimated_cost_per_1000_decisions_usd": cost_per_1000,
|
| 454 |
+
"by_type": by_type, "limits": limits, "local_reference_limits_met": eligible,
|
| 455 |
+
"qualification_note": "Local measurements only; evaluator hardware measurements determine admission."}
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def project_cost(rows,config,P):
|
| 459 |
+
if P is None:return None
|
| 460 |
+
if isinstance(P,bool) or not isinstance(P,(int,float)) or not math.isfinite(P) or P<=0:raise ValueError('P must be positive')
|
| 461 |
+
prices=config['pricing']['models'][config['pricing']['model_key']]
|
| 462 |
+
known=all(isinstance(prices.get(k),(int,float)) and prices[k]>=0 for k in ('input_per_million','output_per_million'))
|
| 463 |
+
result={'P':P,'by_basis':{},'assumption':'APC: additive input overhead; no APC: proportional input; output unchanged; long tail added'}
|
| 464 |
+
cap=config['limits']['cost_per_1000_decisions_usd']*(1-config['pricing']['margin'])
|
| 465 |
+
tail=config['bench'].get('long_tail',{'count':23,'total_decisions':1624})
|
| 466 |
+
for basis in ('engine','each_once','submitted'):
|
| 467 |
+
values=[];outputs=[];valid=bool(rows) and known and all(r.get('usage_complete') for r in rows)
|
| 468 |
+
modes=set()
|
| 469 |
+
if valid:
|
| 470 |
+
for r in rows:
|
| 471 |
+
usage=r.get('usage',{}).get('alternatives',{}).get(basis);f=r.get('fast_prompt_tokens')
|
| 472 |
+
if not usage or not isinstance(f,(int,float)) or f<=0:valid=False;break
|
| 473 |
+
apc=r.get('prefix_caching')
|
| 474 |
+
if basis=='engine' and apc is None:valid=False;break
|
| 475 |
+
additive=basis=='engine' and apc is True
|
| 476 |
+
values.append(max(0,P+usage['input_tokens']-f) if additive else usage['input_tokens']*P/f)
|
| 477 |
+
outputs.append(usage['output_tokens']);modes.add('additive' if additive else 'ratio')
|
| 478 |
+
cost=None
|
| 479 |
+
if valid:
|
| 480 |
+
regular=(statistics.mean(values)*prices['input_per_million']+statistics.mean(outputs)*prices['output_per_million'])/1e6
|
| 481 |
+
long_input=config['limits']['max_prompt_tokens'];probe=1 if config['model']['backend']=='vllm' else 0
|
| 482 |
+
long_cost=(long_input*prices['input_per_million']+probe*prices['output_per_million'])/1e6
|
| 483 |
+
cost=((tail['total_decisions']-tail['count'])*regular+tail['count']*long_cost)/tail['total_decisions']*1000
|
| 484 |
+
result['by_basis'][basis]={'cost_per_1000':cost,'method':sorted(modes),
|
| 485 |
+
'gate':'UNKNOWN' if cost is None else 'PASS' if cost<=.85*cap else 'WARN' if cost<=cap else 'FAIL'}
|
| 486 |
+
return result
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def margin_threshold_for_share(margins,target):
|
| 490 |
+
if not margins or not 0<target<1 or any(not math.isfinite(v) or v<0 for v in margins):
|
| 491 |
+
raise ValueError('Need finite nonnegative margins and target in (0,1)')
|
| 492 |
+
ordered=sorted(margins); k=math.ceil(target*len(ordered)); lower=ordered[k-1]
|
| 493 |
+
higher=next((v for v in ordered[k:] if v>lower),None)
|
| 494 |
+
# JSON-safe equivalent of infinity when all remaining margins are tied.
|
| 495 |
+
threshold=lower+(higher-lower)/2 if higher is not None else math.nextafter(lower,math.inf)
|
| 496 |
+
if not math.isfinite(threshold): raise ValueError("No finite threshold can include the boundary tie")
|
| 497 |
+
return threshold,sum(v<threshold for v in margins)/len(margins)
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
def binomial_ci(successes,n):
|
| 501 |
+
if not n: return None
|
| 502 |
+
z=1.959963984540054; p=successes/n; d=1+z*z/n
|
| 503 |
+
center=(p+z*z/(2*n))/d
|
| 504 |
+
radius=z*math.sqrt(p*(1-p)/n+z*z/(4*n*n))/d
|
| 505 |
+
return [max(0,center-radius),min(1,center+radius)]
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def margin_auroc(rows):
|
| 509 |
+
positive=[-r['fast_margin'] for r in rows if r.get('correct') is False]
|
| 510 |
+
negative=[-r['fast_margin'] for r in rows if r.get('correct') is True]
|
| 511 |
+
if not positive or not negative: return None
|
| 512 |
+
return sum((a>b)+.5*(a==b) for a in positive for b in negative)/(len(positive)*len(negative))
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def calibrate_margin(rows,target,health):
|
| 516 |
+
if health.get('mode')!='fast_only': raise ValueError('Margin calibration requires fast_only service')
|
| 517 |
+
if not rows or any(r.get('request_order')!='criteria' or not r['ok'] or not r['usage_complete'] or r.get('path')!='fast'
|
| 518 |
+
or isinstance(r.get('fast_margin'),bool) or not isinstance(r.get('fast_margin'),(int,float))
|
| 519 |
+
or not math.isfinite(r['fast_margin']) for r in rows):
|
| 520 |
+
raise ValueError('Every calibration task must succeed with complete margin and usage')
|
| 521 |
+
kinds={r.get('margin_kind') for r in rows}
|
| 522 |
+
if len(kinds)!=1 or next(iter(kinds)) not in ('logratio','probdiff'):
|
| 523 |
+
raise ValueError('Inconsistent margin kind')
|
| 524 |
+
threshold,achieved=margin_threshold_for_share([r['fast_margin'] for r in rows],target)
|
| 525 |
+
def stats(subset):
|
| 526 |
+
n=len(subset); selected=sum(r['fast_margin']<threshold for r in subset)
|
| 527 |
+
result={'n':n,'selected':selected,'share':selected/n if n else None,
|
| 528 |
+
'binomial_ci95':binomial_ci(selected,n),'auroc_fast_error':margin_auroc(subset)}
|
| 529 |
+
for side in ('below','above'):
|
| 530 |
+
labeled=[r for r in subset if r.get('correct') is not None and (r['fast_margin']<threshold)==(side=='below')]
|
| 531 |
+
result[side+'_fast_accuracy']=sum(r['correct'] for r in labeled)/len(labeled) if labeled else None
|
| 532 |
+
return result
|
| 533 |
+
identity=health.get('threshold_identity',{})
|
| 534 |
+
if any(k not in identity for k in ('model_key','quant','adapter','prompt_sha','backend','margin_kind','max_new_tokens','max_prompt_tokens','request_contract')):
|
| 535 |
+
raise ValueError('Health lacks threshold identity')
|
| 536 |
+
return dict(identity,request_contract='typesafe.criteria.v1',margin_kind=next(iter(kinds)),target=target,threshold=threshold,
|
| 537 |
+
achieved=achieved,n=len(rows),placeholder=False,overall=stats(rows),
|
| 538 |
+
by_type={k:stats([r for r in rows if r['type']==k]) for k in sorted({r['type'] for r in rows})},
|
| 539 |
+
by_tier={str(k):stats([r for r in rows if r['tier']==k]) for k in sorted({r['tier'] for r in rows},key=str)})
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def compare_order_accuracy(criteria_rows,labels_rows):
|
| 543 |
+
left={r['id']:r for r in criteria_rows};right={r['id']:r for r in labels_rows}
|
| 544 |
+
ids=[i for i in left if i in right and left[i]['type']=='choice' and left[i].get('expected') is not None]
|
| 545 |
+
if not ids or any(not left[i]['ok'] or not right[i]['ok'] for i in ids):
|
| 546 |
+
return {'count':len(ids),'criteria_accuracy':None,'labels_accuracy':None,'labels_minus_criteria':None}
|
| 547 |
+
a=sum(left[i]['correct'] for i in ids)/len(ids);b=sum(right[i]['correct'] for i in ids)/len(ids)
|
| 548 |
+
return {'count':len(ids),'criteria_accuracy':a,'labels_accuracy':b,'labels_minus_criteria':b-a}
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def calibrate_temperature(rows):
|
| 552 |
+
# Only replay raw scores from the official request order; never calibrate
|
| 553 |
+
# already floored/temperature-scaled probabilities.
|
| 554 |
+
if not rows or any(not r['ok'] or r.get('request_order')!='criteria' or not isinstance(r.get('raw_label_scores'),dict) or r.get('expected') is None for r in rows):
|
| 555 |
+
raise ValueError('Temperature fit needs successful labeled criteria-order raw scores')
|
| 556 |
+
result={}
|
| 557 |
+
grid=[2**(i/8) for i in range(-32,41)]
|
| 558 |
+
for kind in TYPES:
|
| 559 |
+
subset=[r for r in rows if r['type']==kind]
|
| 560 |
+
if not subset: result[kind]={'count':0,'fitted':False};continue
|
| 561 |
+
if kind=='noul':
|
| 562 |
+
correct=sum(max(r['raw_label_scores'],key=r['raw_label_scores'].get)==r['expected'] for r in subset)
|
| 563 |
+
result[kind]={'count':len(subset),'p_cal':max(.5,min(1-1e-6,correct/len(subset))),'fitted':True};continue
|
| 564 |
+
def loss(t):
|
| 565 |
+
total=0
|
| 566 |
+
for r in subset:
|
| 567 |
+
scores=r['raw_label_scores'];top=max(scores.values())
|
| 568 |
+
total+=math.log(sum(math.exp((v-top)/t) for v in scores.values()))-(scores[r['expected']]-top)/t
|
| 569 |
+
return total/len(subset)
|
| 570 |
+
t=min(grid,key=loss);result[kind]={'count':len(subset),'temperature':t,'nll':loss(t),'fitted':True}
|
| 571 |
+
return {'request_contract':'typesafe.criteria.v1','by_type':result,'status':'candidate; held-out validation required'}
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def fit_calibration(fast_rows, routed_rows, model_key):
|
| 575 |
+
import copy
|
| 576 |
+
if not fast_rows or not routed_rows:raise ValueError('Both fast and routed rows are required')
|
| 577 |
+
for rows in (fast_rows,routed_rows):
|
| 578 |
+
if len({r.get('id') for r in rows})!=len(rows):raise ValueError('Duplicate calibration IDs')
|
| 579 |
+
for r in rows:
|
| 580 |
+
identity=r.get('threshold_identity') or {}
|
| 581 |
+
if identity.get('model_key')!=model_key:raise ValueError('Calibration model identity mismatch')
|
| 582 |
+
if not r.get('ok') or r.get('request_order')!='criteria' or r.get('expected') is None:
|
| 583 |
+
raise ValueError('Calibration requires successful labeled criteria-order rows')
|
| 584 |
+
keys=('model_key','quant','adapter','backend','prompt_sha','margin_kind','max_prompt_tokens','request_contract')
|
| 585 |
+
identity=fast_rows[0]['threshold_identity']
|
| 586 |
+
if any(any(r['threshold_identity'].get(k)!=identity.get(k) for k in keys) for r in fast_rows+routed_rows):
|
| 587 |
+
raise ValueError('Mixed calibration identities')
|
| 588 |
+
if any(r.get('path')!='fast' or r.get('calibration_block') not in ('fast',None) for r in fast_rows):
|
| 589 |
+
raise ValueError('fast-run must contain only fast decisions')
|
| 590 |
+
def prepared(rows,field):
|
| 591 |
+
result=[]
|
| 592 |
+
for r in rows:
|
| 593 |
+
scores=r.get(field)
|
| 594 |
+
if scores is None and field=='fast_scores':scores=r.get('raw_label_scores')
|
| 595 |
+
if not isinstance(scores,dict) or r['expected'] not in scores or any(isinstance(v,bool) or not isinstance(v,(int,float)) or not math.isfinite(v) for v in scores.values()):
|
| 596 |
+
raise ValueError('Missing or invalid pre-temperature label scores')
|
| 597 |
+
result.append(dict(r,raw_label_scores=scores))
|
| 598 |
+
return result
|
| 599 |
+
sources={'fast':prepared(fast_rows,'fast_scores'),
|
| 600 |
+
'slow':prepared([r for r in routed_rows if r.get('calibration_block')=='slow'],'slow_scores'),
|
| 601 |
+
'fallback':prepared([r for r in routed_rows if r.get('calibration_block')=='fallback'],'fast_scores')}
|
| 602 |
+
blocks={}
|
| 603 |
+
for name,rows in sources.items():
|
| 604 |
+
fit=calibrate_temperature(rows)['by_type'] if rows else {}
|
| 605 |
+
block={'placeholder':False,'status':'candidate; held-out validation required',
|
| 606 |
+
'temperature_by_kind':{'choice':1.,'score':1.,'noul':'one_bin'},'noul_calibration':{'p_cal':.5},'by_type':{}}
|
| 607 |
+
for kind in TYPES:
|
| 608 |
+
item=fit.get(kind,{'count':0,'fitted':False});n=item['count']
|
| 609 |
+
meta=dict(item,status='insufficient' if n<20 else 'candidate')
|
| 610 |
+
if name=='fallback' and n<20:
|
| 611 |
+
source=blocks['fast'];meta['inherited_from']='fast'
|
| 612 |
+
if kind=='noul':block['noul_calibration']=copy.deepcopy(source['noul_calibration'])
|
| 613 |
+
else:block['temperature_by_kind'][kind]=source['temperature_by_kind'][kind]
|
| 614 |
+
elif item.get('fitted'):
|
| 615 |
+
if kind=='noul':block['noul_calibration']['p_cal']=item['p_cal']
|
| 616 |
+
else:block['temperature_by_kind'][kind]=item['temperature']
|
| 617 |
+
if n<20:block['placeholder']=True
|
| 618 |
+
block['by_type'][kind]=meta
|
| 619 |
+
blocks[name]=block
|
| 620 |
+
return {'status':'candidate; no production overwrite; validate held-out data',
|
| 621 |
+
'models':{model_key:dict(blocks,placeholder=any(b['placeholder'] for b in blocks.values()),
|
| 622 |
+
fitted_for=identity)}}
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def read_run_rows(prefix):
|
| 626 |
+
if __package__:from .core import external_path
|
| 627 |
+
else:from core import external_path
|
| 628 |
+
path=external_path(prefix)
|
| 629 |
+
if path.suffix!='.jsonl':path=path.with_name(path.name+'.jsonl')
|
| 630 |
+
return [json.loads(line) for line in path.read_text(encoding='utf-8-sig').splitlines() if line.strip()]
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def write_candidate(path,value):
|
| 634 |
+
if __package__:from .core import external_output
|
| 635 |
+
else:from core import external_output
|
| 636 |
+
target=external_output(path)
|
| 637 |
+
with target.open('x',encoding='utf-8') as out:json.dump(value,out,ensure_ascii=False,indent=2,allow_nan=False)
|
| 638 |
+
return target
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def apply_margin_candidate(config, report, tier=None):
|
| 642 |
+
import copy
|
| 643 |
+
if __package__:from .core import apply_profile,threshold_identity,apply_budget_tier
|
| 644 |
+
else:from core import apply_profile,threshold_identity,apply_budget_tier
|
| 645 |
+
result=copy.deepcopy(config);key=report.get('model_key')
|
| 646 |
+
result['model']['model_key']=key;apply_profile(result);apply_budget_tier(result,tier)
|
| 647 |
+
identity=threshold_identity(result)
|
| 648 |
+
keys=('model_key','quant','adapter','backend','prompt_sha','margin_kind','max_prompt_tokens','request_contract')
|
| 649 |
+
if report.get('placeholder',True) or any(report.get(k)!=identity[k] for k in keys):raise ValueError('Margin candidate identity mismatch')
|
| 650 |
+
value=report['threshold']
|
| 651 |
+
if not isinstance(value,(int,float)) or not math.isfinite(value) or value<0:raise ValueError('Invalid threshold')
|
| 652 |
+
fit={k:report[k] for k in keys};fit.update(placeholder=False,max_new_tokens=identity['max_new_tokens'],reasoning_words=identity['reasoning_words'])
|
| 653 |
+
target=result['profiles'][key]['routing']
|
| 654 |
+
target.update(margin_threshold=value,margin_threshold_fitted_for=fit)
|
| 655 |
+
result['routing'].update(margin_threshold=value,margin_threshold_fitted_for=fit)
|
| 656 |
+
return result
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def get_health(endpoint,timeout,path="/health"):
|
| 660 |
+
parts=urllib.parse.urlsplit(endpoint)
|
| 661 |
+
url=urllib.parse.urlunsplit((parts.scheme,parts.netloc,path,'',''))
|
| 662 |
+
with _OPENER.open(url,timeout=timeout) as response:
|
| 663 |
+
return json.load(response)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
def output_paths(prefix, calibration_only=False):
|
| 667 |
+
if __package__:from .core import external_output
|
| 668 |
+
else:from core import external_output
|
| 669 |
+
candidate = external_output(prefix)
|
| 670 |
+
if calibration_only:
|
| 671 |
+
target=candidate.with_name(candidate.name+'.margin_calibration.json')
|
| 672 |
+
if target.exists(): raise FileExistsError('Calibration output exists')
|
| 673 |
+
return target
|
| 674 |
+
rows_path = candidate.with_name(candidate.name + ".jsonl")
|
| 675 |
+
summary_path = candidate.with_name(candidate.name + ".summary.json")
|
| 676 |
+
if rows_path.exists() or summary_path.exists():
|
| 677 |
+
raise FileExistsError("output already exists; choose a new prefix")
|
| 678 |
+
return rows_path, summary_path
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
def main(argv=None):
|
| 682 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 683 |
+
parser.add_argument("--config", type=Path, default=ROOT / "config.json")
|
| 684 |
+
source = parser.add_mutually_exclusive_group(required=False)
|
| 685 |
+
source.add_argument("--public", type=Path)
|
| 686 |
+
source.add_argument("--data", type=Path)
|
| 687 |
+
parser.add_argument("--index", type=Path)
|
| 688 |
+
parser.add_argument("--endpoint", help="Runtime HTTP endpoint; never written to output")
|
| 689 |
+
parser.add_argument("--output-prefix", help="Absolute path prefix outside the package (required for runs)")
|
| 690 |
+
parser.add_argument("--price-model", help="Key in config.pricing.models")
|
| 691 |
+
parser.add_argument('--original-types',action='store_true')
|
| 692 |
+
parser.add_argument('--concurrency',type=int,nargs='+')
|
| 693 |
+
parser.add_argument('--calibrate-margin',action='store_true')
|
| 694 |
+
parser.add_argument('--engine-stats',action='store_true')
|
| 695 |
+
parser.add_argument('--compare-label-order',action='store_true')
|
| 696 |
+
parser.add_argument('--target-slow-share',type=float)
|
| 697 |
+
parser.add_argument('--P',type=float)
|
| 698 |
+
parser.add_argument('--fit-calibration',action='store_true')
|
| 699 |
+
parser.add_argument('--fast-run',type=Path)
|
| 700 |
+
parser.add_argument('--routed-run',type=Path)
|
| 701 |
+
parser.add_argument('--output',type=Path)
|
| 702 |
+
parser.add_argument('--model-key')
|
| 703 |
+
parser.add_argument('--apply-margin',type=Path)
|
| 704 |
+
parser.add_argument('--tier',choices=('normal','hard','long'))
|
| 705 |
+
args = parser.parse_args(argv)
|
| 706 |
+
config_path = args.config.expanduser().resolve()
|
| 707 |
+
config = json.loads(config_path.read_text(encoding="utf-8-sig"))
|
| 708 |
+
if args.fit_calibration:
|
| 709 |
+
if not all((args.fast_run,args.routed_run,args.output,args.model_key)):parser.error('Fit needs fast-run, routed-run, output and model-key')
|
| 710 |
+
write_candidate(args.output,fit_calibration(read_run_rows(args.fast_run),read_run_rows(args.routed_run),args.model_key))
|
| 711 |
+
return 0
|
| 712 |
+
if args.apply_margin:
|
| 713 |
+
if not args.output:parser.error('Apply margin needs a new output config')
|
| 714 |
+
write_candidate(args.output,apply_margin_candidate(config,json.loads(args.apply_margin.read_text(encoding='utf-8')),args.tier))
|
| 715 |
+
return 0
|
| 716 |
+
if not args.endpoint or not (args.public or args.data):parser.error('Benchmark needs endpoint and public or data')
|
| 717 |
+
if not args.output_prefix:parser.error('Benchmark needs --output-prefix (absolute, outside the package)')
|
| 718 |
+
if args.P is not None:config['bench']['projection_prompt_tokens']=args.P
|
| 719 |
+
if args.original_types: config['bench']['original_types']=True
|
| 720 |
+
if args.price_model:
|
| 721 |
+
config["pricing"]["model_key"] = args.price_model
|
| 722 |
+
if args.public:
|
| 723 |
+
index = args.index or Path(config["bench"]["index"])
|
| 724 |
+
if not index.is_absolute():
|
| 725 |
+
index = config_path.parent / index
|
| 726 |
+
tasks = load_public_tasks(index, args.public, config["bench"]["expected_public_count"])
|
| 727 |
+
else:
|
| 728 |
+
if args.index:
|
| 729 |
+
parser.error("--index requires --public")
|
| 730 |
+
tasks = load_jsonl_tasks(args.data)
|
| 731 |
+
levels=args.concurrency or config['bench']['concurrency']
|
| 732 |
+
if any(n<1 for n in levels): parser.error('Concurrency must be positive')
|
| 733 |
+
if args.calibrate_margin and config['bench'].get('original_types'):parser.error('Calibration requires the official request types')
|
| 734 |
+
if args.calibrate_margin and levels!=[1]: parser.error('Margin calibration is serial')
|
| 735 |
+
if args.calibrate_margin and args.engine_stats:parser.error('Use a separate serial run for engine reconciliation')
|
| 736 |
+
endpoint = endpoint_url(args.endpoint)
|
| 737 |
+
paths = output_paths(args.output_prefix,calibration_only=args.calibrate_margin)
|
| 738 |
+
health=get_health(endpoint,config['bench']['timeout_seconds'])
|
| 739 |
+
if __package__: from .core import apply_profile
|
| 740 |
+
else: from core import apply_profile
|
| 741 |
+
config['model']['model_key']=health['model_key']
|
| 742 |
+
apply_profile(config)
|
| 743 |
+
if args.tier:
|
| 744 |
+
if __package__:from .core import apply_budget_tier
|
| 745 |
+
else:from core import apply_budget_tier
|
| 746 |
+
apply_budget_tier(config,args.tier)
|
| 747 |
+
if args.price_model: config['pricing']['model_key']=args.price_model
|
| 748 |
+
if args.calibrate_margin:
|
| 749 |
+
if health.get('mode')!='fast_only': parser.error('Requires fast_only service')
|
| 750 |
+
target=args.target_slow_share if args.target_slow_share is not None else config['bench']['target_slow_share']
|
| 751 |
+
calibration_path=paths
|
| 752 |
+
# Existing-file check precedes inference; exclusive creation prevents races.
|
| 753 |
+
rows=run_benchmark(tasks,endpoint,config)
|
| 754 |
+
report=calibrate_margin(rows,target,health)
|
| 755 |
+
report['temperature_fit']=calibrate_temperature(rows)
|
| 756 |
+
if args.compare_label_order:
|
| 757 |
+
import copy
|
| 758 |
+
other=copy.deepcopy(config);other['bench']['label_order']=True
|
| 759 |
+
report['order_accuracy']=compare_order_accuracy(rows,run_benchmark(tasks,endpoint,other))
|
| 760 |
+
with calibration_path.open('x',encoding='utf-8') as output:
|
| 761 |
+
json.dump(report,output,ensure_ascii=False,indent=2,allow_nan=False)
|
| 762 |
+
print(json.dumps(report,ensure_ascii=False,indent=2,allow_nan=False))
|
| 763 |
+
return 0
|
| 764 |
+
stats_path=paths[0].with_name(paths[0].stem+'.engine_stats.json')
|
| 765 |
+
if args.engine_stats and stats_path.exists():raise FileExistsError('Engine snapshot exists')
|
| 766 |
+
# Claim both paths before requests, using exclusive creation for preservation.
|
| 767 |
+
with paths[0].open("x", encoding="utf-8") as writer, paths[1].open("x", encoding="utf-8") as summary_writer:
|
| 768 |
+
summaries={}
|
| 769 |
+
snapshots={"health":health,"by_concurrency":{}}
|
| 770 |
+
stats_path=paths[0].with_name(paths[0].stem+".engine_stats.json")
|
| 771 |
+
if args.engine_stats and stats_path.exists(): raise FileExistsError("Engine snapshot exists")
|
| 772 |
+
for n in dict.fromkeys(levels):
|
| 773 |
+
if args.engine_stats: before=get_health(endpoint,config['bench']['timeout_seconds'],'/engine_stats')
|
| 774 |
+
rows=run_benchmark(tasks,endpoint,config,concurrency=n)
|
| 775 |
+
if args.engine_stats: snapshots['by_concurrency'][str(n)]={'before':before,'after':get_health(endpoint,config['bench']['timeout_seconds'],'/engine_stats')}
|
| 776 |
+
for row in rows:
|
| 777 |
+
row['concurrency']=n
|
| 778 |
+
row['prefix_caching']=health.get('prefix_caching')
|
| 779 |
+
writer.write(json.dumps(row,ensure_ascii=False,allow_nan=False)+'\n')
|
| 780 |
+
writer.flush()
|
| 781 |
+
summaries[str(n)]=summarize(rows,config,expected_count=len(tasks))
|
| 782 |
+
if args.compare_label_order:
|
| 783 |
+
import copy
|
| 784 |
+
other=copy.deepcopy(config);other['bench']['label_order']=True
|
| 785 |
+
summaries[str(n)]['order_accuracy']=compare_order_accuracy(rows,run_benchmark(tasks,endpoint,other,concurrency=n))
|
| 786 |
+
if args.engine_stats:
|
| 787 |
+
with stats_path.open('x',encoding='utf-8') as output: json.dump(snapshots,output,indent=2,allow_nan=False)
|
| 788 |
+
summary=dict(summaries[str(levels[0])],by_concurrency=summaries)
|
| 789 |
+
summary_writer.write(json.dumps(summary, ensure_ascii=False, indent=2, allow_nan=False) + "\n")
|
| 790 |
+
print(json.dumps(summary, ensure_ascii=False, indent=2, allow_nan=False))
|
| 791 |
+
return 0 if all(v["complete"] for v in summary["by_concurrency"].values()) else 1
|
| 792 |
+
|
| 793 |
+
|
| 794 |
+
if __name__ == "__main__":
|
| 795 |
+
raise SystemExit(main())
|
service/calibration.json
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"models": {
|
| 3 |
+
"metask-gemma4-31b-lora-w4a16": {
|
| 4 |
+
"fallback": {
|
| 5 |
+
"by_type": {
|
| 6 |
+
"choice": {
|
| 7 |
+
"count": 139,
|
| 8 |
+
"fitted": true,
|
| 9 |
+
"inherited": true,
|
| 10 |
+
"inherited_from": "fast",
|
| 11 |
+
"nll": 0.268809886282054,
|
| 12 |
+
"temperature": 1.2968395546510096
|
| 13 |
+
},
|
| 14 |
+
"noul": {
|
| 15 |
+
"constant_source": "service parameter (fit_report.json noul_constant)",
|
| 16 |
+
"count": 74,
|
| 17 |
+
"fitted": true,
|
| 18 |
+
"fitted_p_cal": 0.9459459459459459,
|
| 19 |
+
"inherited": true,
|
| 20 |
+
"inherited_from": "fast",
|
| 21 |
+
"p_cal": 0.898
|
| 22 |
+
},
|
| 23 |
+
"score": {
|
| 24 |
+
"count": 18,
|
| 25 |
+
"fitted": true,
|
| 26 |
+
"inherited": true,
|
| 27 |
+
"inherited_from": "fast",
|
| 28 |
+
"nll": 0.26993267192031306,
|
| 29 |
+
"temperature": 1.189207115002721
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"noul_calibration": {
|
| 33 |
+
"p_cal": 0.898
|
| 34 |
+
},
|
| 35 |
+
"placeholder": false,
|
| 36 |
+
"status": "temperatures: public231 in-sample fast fit (unchanged); yes/no constant: service parameter, see fit_report.json noul_constant",
|
| 37 |
+
"temperature_by_kind": {
|
| 38 |
+
"choice": 1.2968395546510096,
|
| 39 |
+
"noul": "one_bin",
|
| 40 |
+
"score": 1.189207115002721
|
| 41 |
+
}
|
| 42 |
+
},
|
| 43 |
+
"fast": {
|
| 44 |
+
"by_type": {
|
| 45 |
+
"choice": {
|
| 46 |
+
"count": 139,
|
| 47 |
+
"fitted": true,
|
| 48 |
+
"nll": 0.268809886282054,
|
| 49 |
+
"temperature": 1.2968395546510096
|
| 50 |
+
},
|
| 51 |
+
"noul": {
|
| 52 |
+
"constant_source": "service parameter (fit_report.json noul_constant)",
|
| 53 |
+
"count": 74,
|
| 54 |
+
"fitted": true,
|
| 55 |
+
"fitted_p_cal": 0.9459459459459459,
|
| 56 |
+
"p_cal": 0.898
|
| 57 |
+
},
|
| 58 |
+
"score": {
|
| 59 |
+
"count": 18,
|
| 60 |
+
"fitted": true,
|
| 61 |
+
"nll": 0.26993267192031306,
|
| 62 |
+
"temperature": 1.189207115002721
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"noul_calibration": {
|
| 66 |
+
"p_cal": 0.898
|
| 67 |
+
},
|
| 68 |
+
"placeholder": false,
|
| 69 |
+
"status": "temperatures: public231 in-sample fast fit (unchanged); yes/no constant: service parameter, see fit_report.json noul_constant",
|
| 70 |
+
"temperature_by_kind": {
|
| 71 |
+
"choice": 1.2968395546510096,
|
| 72 |
+
"noul": "one_bin",
|
| 73 |
+
"score": 1.189207115002721
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"fitted_for": {
|
| 77 |
+
"adapter": "model-subdir:adapter",
|
| 78 |
+
"backend": "vllm",
|
| 79 |
+
"margin_kind": "logratio",
|
| 80 |
+
"max_new_tokens": 256,
|
| 81 |
+
"max_prompt_tokens": 6653,
|
| 82 |
+
"model_key": "metask-gemma4-31b-lora-w4a16",
|
| 83 |
+
"prompt_sha": "e18ed7a06eb52f1b351458f384710a2c240aaf2b4f64d916b776c6d18ca3f938",
|
| 84 |
+
"quant": "awq",
|
| 85 |
+
"reasoning_words": 160,
|
| 86 |
+
"request_contract": "typesafe.criteria.v1"
|
| 87 |
+
},
|
| 88 |
+
"placeholder": false,
|
| 89 |
+
"slow": {
|
| 90 |
+
"by_type": {
|
| 91 |
+
"choice": {
|
| 92 |
+
"applied_rule": "slow samples of a type < 30: the fast parameter of that type (temperature or yes/no confidence) is copied unchanged",
|
| 93 |
+
"count": 0,
|
| 94 |
+
"fast_count": 139,
|
| 95 |
+
"fitted": false,
|
| 96 |
+
"inherited": true,
|
| 97 |
+
"inherited_from": "fast",
|
| 98 |
+
"status": "inherited"
|
| 99 |
+
},
|
| 100 |
+
"noul": {
|
| 101 |
+
"applied_rule": "slow samples of a type < 30: the fast parameter of that type (temperature or yes/no confidence) is copied unchanged",
|
| 102 |
+
"constant_source": "service parameter (fit_report.json noul_constant)",
|
| 103 |
+
"count": 0,
|
| 104 |
+
"fast_count": 74,
|
| 105 |
+
"fitted": false,
|
| 106 |
+
"fitted_p_cal": 0.9459459459459459,
|
| 107 |
+
"inherited": true,
|
| 108 |
+
"inherited_from": "fast",
|
| 109 |
+
"p_cal": 0.898,
|
| 110 |
+
"status": "inherited"
|
| 111 |
+
},
|
| 112 |
+
"score": {
|
| 113 |
+
"applied_rule": "slow samples of a type < 30: the fast parameter of that type (temperature or yes/no confidence) is copied unchanged",
|
| 114 |
+
"count": 0,
|
| 115 |
+
"fast_count": 18,
|
| 116 |
+
"fitted": false,
|
| 117 |
+
"inherited": true,
|
| 118 |
+
"inherited_from": "fast",
|
| 119 |
+
"status": "inherited"
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"held_out_validated": false,
|
| 123 |
+
"noul_calibration": {
|
| 124 |
+
"p_cal": 0.898
|
| 125 |
+
},
|
| 126 |
+
"placeholder": false,
|
| 127 |
+
"status": "temperatures: public231 in-sample; slow types with fewer than 30 slow samples inherit fast (unchanged); yes/no constant: service parameter, see fit_report.json noul_constant",
|
| 128 |
+
"temperature_by_kind": {
|
| 129 |
+
"choice": 1.2968395546510096,
|
| 130 |
+
"noul": "one_bin",
|
| 131 |
+
"score": 1.189207115002721
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
service/config.json
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bench": {
|
| 3 |
+
"concurrency": [
|
| 4 |
+
1
|
| 5 |
+
],
|
| 6 |
+
"expected_public_count": 231,
|
| 7 |
+
"latency_offset_seconds": 0.15,
|
| 8 |
+
"latency_scale": 2.0,
|
| 9 |
+
"long_tail": {
|
| 10 |
+
"count": 23,
|
| 11 |
+
"projection_budgets": [
|
| 12 |
+
8192
|
| 13 |
+
],
|
| 14 |
+
"prompt_tokens": 79000,
|
| 15 |
+
"total_decisions": 1624
|
| 16 |
+
},
|
| 17 |
+
"original_types": false,
|
| 18 |
+
"output_prefix": "31b-exploration",
|
| 19 |
+
"target_slow_share": 0.49,
|
| 20 |
+
"timeout_seconds": 120
|
| 21 |
+
},
|
| 22 |
+
"calibration_file": "calibration.json",
|
| 23 |
+
"generation": {
|
| 24 |
+
"bad_words_ids": null,
|
| 25 |
+
"begin_suppress_tokens": null,
|
| 26 |
+
"do_sample": false,
|
| 27 |
+
"forced_bos_token_id": null,
|
| 28 |
+
"forced_decoder_ids": null,
|
| 29 |
+
"forced_eos_token_id": null,
|
| 30 |
+
"length_penalty": 1.0,
|
| 31 |
+
"max_new_tokens": 192,
|
| 32 |
+
"min_length": 0,
|
| 33 |
+
"min_new_tokens": 0,
|
| 34 |
+
"no_repeat_ngram_size": 0,
|
| 35 |
+
"reasoning_words": 50,
|
| 36 |
+
"renormalize_logits": false,
|
| 37 |
+
"repetition_penalty": 1.0,
|
| 38 |
+
"seed": 0,
|
| 39 |
+
"suppress_tokens": null,
|
| 40 |
+
"temperature": 1.0,
|
| 41 |
+
"top_k": 0,
|
| 42 |
+
"top_p": 1.0,
|
| 43 |
+
"wall_timeout_seconds": 60
|
| 44 |
+
},
|
| 45 |
+
"generation_reference": {
|
| 46 |
+
"intended_overrides": {
|
| 47 |
+
"do_sample": {
|
| 48 |
+
"reason": "Frozen greedy decoding; checkpoint sampling policy is intentionally overridden.",
|
| 49 |
+
"value": false
|
| 50 |
+
},
|
| 51 |
+
"suppress_tokens": {
|
| 52 |
+
"reason": "All reported measurements were taken without checkpoint token suppression; drafts only, fast-path candidate scoring unaffected.",
|
| 53 |
+
"value": null
|
| 54 |
+
},
|
| 55 |
+
"temperature": {
|
| 56 |
+
"reason": "Frozen greedy decoding; checkpoint sampling policy is intentionally overridden.",
|
| 57 |
+
"value": 1.0
|
| 58 |
+
},
|
| 59 |
+
"top_k": {
|
| 60 |
+
"reason": "Frozen greedy decoding; checkpoint sampling policy is intentionally overridden.",
|
| 61 |
+
"value": 0
|
| 62 |
+
},
|
| 63 |
+
"top_p": {
|
| 64 |
+
"reason": "Frozen greedy decoding; checkpoint sampling policy is intentionally overridden.",
|
| 65 |
+
"value": 1.0
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"interface": {
|
| 70 |
+
"boolean_keys": "request"
|
| 71 |
+
},
|
| 72 |
+
"item_deadline": {
|
| 73 |
+
"chunk_tokens": 32,
|
| 74 |
+
"ewma_alpha": 0.3,
|
| 75 |
+
"item_seconds": 9.0,
|
| 76 |
+
"min_chunk_tokens": 8,
|
| 77 |
+
"readout_reserve_init_s": 1.0,
|
| 78 |
+
"readout_reserve_min_s": 0.3,
|
| 79 |
+
"readout_safety": 1.5,
|
| 80 |
+
"response_reserve_s": 0.35,
|
| 81 |
+
"token_safety": 1.25,
|
| 82 |
+
"warmup_repeats": 3,
|
| 83 |
+
"warmup_tokens": 33
|
| 84 |
+
},
|
| 85 |
+
"item_rules": {
|
| 86 |
+
"reread_tau": 4.0,
|
| 87 |
+
"slow_types": [
|
| 88 |
+
"choice",
|
| 89 |
+
"noul",
|
| 90 |
+
"score"
|
| 91 |
+
]
|
| 92 |
+
},
|
| 93 |
+
"limits": {
|
| 94 |
+
"compression_reserve_tokens": 32,
|
| 95 |
+
"cost_per_1000_decisions_usd": 0.0646,
|
| 96 |
+
"estimated_chars_per_token": 4,
|
| 97 |
+
"max_prompt_tokens": 6653,
|
| 98 |
+
"median_latency_seconds": 1.0,
|
| 99 |
+
"oom_retry_prompt_tokens": 4096,
|
| 100 |
+
"physical_max_prompt_tokens": 6653,
|
| 101 |
+
"precompression_trigger_chars_per_token": 8,
|
| 102 |
+
"raw_median_latency_seconds": 0.425,
|
| 103 |
+
"state_chunk_tokens": 200,
|
| 104 |
+
"state_head_tokens": 512,
|
| 105 |
+
"state_tail_tokens": 512,
|
| 106 |
+
"truncation_head_frac": 0.6
|
| 107 |
+
},
|
| 108 |
+
"model": {
|
| 109 |
+
"adapter": "model-subdir:adapter",
|
| 110 |
+
"attn_implementation": "sdpa",
|
| 111 |
+
"backend": "vllm",
|
| 112 |
+
"device_map": {
|
| 113 |
+
"": 0
|
| 114 |
+
},
|
| 115 |
+
"enable_prefix_caching": true,
|
| 116 |
+
"model_key": "metask-gemma4-31b-lora-w4a16",
|
| 117 |
+
"name": "MetaDecision-31B-Rain",
|
| 118 |
+
"path": "",
|
| 119 |
+
"quant": "awq",
|
| 120 |
+
"version": "MetaDecision-31B-Rain",
|
| 121 |
+
"vllm_expected_version": "0.31.0",
|
| 122 |
+
"vllm_gpu_memory_utilization": 0.94,
|
| 123 |
+
"vllm_max_model_len": 6656,
|
| 124 |
+
"vllm_options": {
|
| 125 |
+
"gpu_memory_utilization": 0.94,
|
| 126 |
+
"max_lora_rank": 16,
|
| 127 |
+
"max_model_len": 6656,
|
| 128 |
+
"max_num_seqs": 1
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"package": {
|
| 132 |
+
"finalized": true,
|
| 133 |
+
"nonformal_fitted": true,
|
| 134 |
+
"sample_only": false,
|
| 135 |
+
"version": "MetaDecision-31B-Rain"
|
| 136 |
+
},
|
| 137 |
+
"pipeline": {
|
| 138 |
+
"arm": "trained",
|
| 139 |
+
"model_identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 140 |
+
"protocol": "adapter_arm_31b_v3"
|
| 141 |
+
},
|
| 142 |
+
"post_calibration": {
|
| 143 |
+
"choice": {
|
| 144 |
+
"config": {
|
| 145 |
+
"debias": false,
|
| 146 |
+
"temperature": false
|
| 147 |
+
},
|
| 148 |
+
"params": {}
|
| 149 |
+
},
|
| 150 |
+
"noul": {
|
| 151 |
+
"config": {
|
| 152 |
+
"floor": null,
|
| 153 |
+
"temperature": "type"
|
| 154 |
+
},
|
| 155 |
+
"params": {
|
| 156 |
+
"T": 0.5
|
| 157 |
+
}
|
| 158 |
+
},
|
| 159 |
+
"score": {
|
| 160 |
+
"config": {
|
| 161 |
+
"mode_weight": 1.0,
|
| 162 |
+
"temperature": false
|
| 163 |
+
},
|
| 164 |
+
"params": {}
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"pricing": {
|
| 168 |
+
"margin": 0.0,
|
| 169 |
+
"model_key": "31B",
|
| 170 |
+
"models": {
|
| 171 |
+
"31B": {
|
| 172 |
+
"input_per_million": 0.059819178473256074,
|
| 173 |
+
"output_per_million": 0.059819178473256074
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
"submission_models": {
|
| 177 |
+
"MetaDecision-31B-Rain": "31B",
|
| 178 |
+
"metask-gemma4-31b-lora-w4a16": "31B",
|
| 179 |
+
"metask-jev-rain-31B-trained": "31B"
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
"profiles": {
|
| 183 |
+
"metask-gemma4-31b-lora-w4a16": {
|
| 184 |
+
"bench": {
|
| 185 |
+
"target_slow_share": 0.49
|
| 186 |
+
},
|
| 187 |
+
"generation": {
|
| 188 |
+
"bad_words_ids": null,
|
| 189 |
+
"begin_suppress_tokens": null,
|
| 190 |
+
"do_sample": false,
|
| 191 |
+
"forced_bos_token_id": null,
|
| 192 |
+
"forced_decoder_ids": null,
|
| 193 |
+
"forced_eos_token_id": null,
|
| 194 |
+
"length_penalty": 1.0,
|
| 195 |
+
"max_new_tokens": 192,
|
| 196 |
+
"min_length": 0,
|
| 197 |
+
"min_new_tokens": 0,
|
| 198 |
+
"no_repeat_ngram_size": 0,
|
| 199 |
+
"reasoning_words": 50,
|
| 200 |
+
"renormalize_logits": false,
|
| 201 |
+
"repetition_penalty": 1.0,
|
| 202 |
+
"seed": 0,
|
| 203 |
+
"suppress_tokens": null,
|
| 204 |
+
"temperature": 1.0,
|
| 205 |
+
"top_k": 0,
|
| 206 |
+
"top_p": 1.0,
|
| 207 |
+
"wall_timeout_seconds": 60
|
| 208 |
+
},
|
| 209 |
+
"limits": {
|
| 210 |
+
"max_prompt_tokens": 6653,
|
| 211 |
+
"physical_max_prompt_tokens": 6653
|
| 212 |
+
},
|
| 213 |
+
"model": {
|
| 214 |
+
"backend": "vllm",
|
| 215 |
+
"name": "MetaDecision-31B-Rain",
|
| 216 |
+
"quant": "awq"
|
| 217 |
+
},
|
| 218 |
+
"pricing": {
|
| 219 |
+
"model_key": "31B"
|
| 220 |
+
},
|
| 221 |
+
"routing": {
|
| 222 |
+
"cost_fuse": {
|
| 223 |
+
"arm": 0.9,
|
| 224 |
+
"enabled": true,
|
| 225 |
+
"release": 0.85,
|
| 226 |
+
"warmup": 100
|
| 227 |
+
},
|
| 228 |
+
"fuse": 0.49,
|
| 229 |
+
"long_item_slow_policy": "fast_only",
|
| 230 |
+
"margin_kind": "logratio",
|
| 231 |
+
"margin_threshold": 6.625023671062799,
|
| 232 |
+
"margin_threshold_fitted_for": {
|
| 233 |
+
"achieved_share": 0.5207547169811321,
|
| 234 |
+
"adapter": "model-subdir:adapter",
|
| 235 |
+
"backend": "vllm",
|
| 236 |
+
"fitted_on": "our own independently built development set (fit split): fast-path margins of the earlier service routing, eligible_items rule (the lowest-margin share of the decisions that can take the drafting pass)",
|
| 237 |
+
"margin_kind": "logratio",
|
| 238 |
+
"max_new_tokens": 192,
|
| 239 |
+
"max_prompt_tokens": 6653,
|
| 240 |
+
"model_key": "metask-gemma4-31b-lora-w4a16",
|
| 241 |
+
"n": 530,
|
| 242 |
+
"placeholder": false,
|
| 243 |
+
"prompt_sha": "e18ed7a06eb52f1b351458f384710a2c240aaf2b4f64d916b776c6d18ca3f938",
|
| 244 |
+
"quant": "awq",
|
| 245 |
+
"reasoning_words": 50,
|
| 246 |
+
"request_contract": "typesafe.criteria.v1"
|
| 247 |
+
},
|
| 248 |
+
"mode": "routed",
|
| 249 |
+
"quota": 0.49,
|
| 250 |
+
"slow_max_prompt_tokens": 840,
|
| 251 |
+
"target_slow_share": 0.49,
|
| 252 |
+
"tier": "normal",
|
| 253 |
+
"tiers": {
|
| 254 |
+
"hard": {
|
| 255 |
+
"fast_only": false,
|
| 256 |
+
"max_new_tokens": 192,
|
| 257 |
+
"reasoning_words": 50
|
| 258 |
+
},
|
| 259 |
+
"long": {
|
| 260 |
+
"fast_only": true,
|
| 261 |
+
"max_prompt_tokens": 6653
|
| 262 |
+
},
|
| 263 |
+
"normal": {
|
| 264 |
+
"fast_only": false,
|
| 265 |
+
"max_prompt_tokens": 6653
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"unparsed_policy": "force_answer"
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
},
|
| 272 |
+
"prompt": {
|
| 273 |
+
"answer_marker": "ANSWER:",
|
| 274 |
+
"cot_instructions": {
|
| 275 |
+
"choice": "First reason step by step in at most {reasoning_words} words, tracking any intermediate state explicitly. Then output a final line `ANSWER: <letter>`.",
|
| 276 |
+
"noul": "First reason step by step in at most {reasoning_words} words, tracking any intermediate state explicitly. Then output a final line of the form `ANSWER: yes` or `ANSWER: no`.",
|
| 277 |
+
"score": "First reason step by step in at most {reasoning_words} words, tracking any intermediate state explicitly. Then output a final line `ANSWER: <number>`."
|
| 278 |
+
},
|
| 279 |
+
"enable_thinking": false,
|
| 280 |
+
"system": "You are a careful, decisive judge. Read the context, apply every rule, definition, threshold, exception and date it contains, and answer the question. Reply with exactly one word and nothing else."
|
| 281 |
+
},
|
| 282 |
+
"routing": {
|
| 283 |
+
"cost_fuse": {
|
| 284 |
+
"arm": 0.9,
|
| 285 |
+
"enabled": true,
|
| 286 |
+
"release": 0.85,
|
| 287 |
+
"warmup": 100
|
| 288 |
+
},
|
| 289 |
+
"fuse": 0.49,
|
| 290 |
+
"long_item_slow_policy": "fast_only",
|
| 291 |
+
"margin_kind": "logratio",
|
| 292 |
+
"margin_threshold": 6.625023671062799,
|
| 293 |
+
"margin_threshold_fitted_for": {
|
| 294 |
+
"achieved_share": 0.5207547169811321,
|
| 295 |
+
"adapter": "model-subdir:adapter",
|
| 296 |
+
"backend": "vllm",
|
| 297 |
+
"fitted_on": "our own independently built development set (fit split): fast-path margins of the earlier service routing, eligible_items rule (the lowest-margin share of the decisions that can take the drafting pass)",
|
| 298 |
+
"margin_kind": "logratio",
|
| 299 |
+
"max_new_tokens": 192,
|
| 300 |
+
"max_prompt_tokens": 6653,
|
| 301 |
+
"model_key": "metask-gemma4-31b-lora-w4a16",
|
| 302 |
+
"n": 530,
|
| 303 |
+
"placeholder": false,
|
| 304 |
+
"prompt_sha": "e18ed7a06eb52f1b351458f384710a2c240aaf2b4f64d916b776c6d18ca3f938",
|
| 305 |
+
"quant": "awq",
|
| 306 |
+
"reasoning_words": 50,
|
| 307 |
+
"request_contract": "typesafe.criteria.v1"
|
| 308 |
+
},
|
| 309 |
+
"mode": "routed",
|
| 310 |
+
"quota": 0.49,
|
| 311 |
+
"resume": false,
|
| 312 |
+
"run_id": null,
|
| 313 |
+
"slow_max_prompt_tokens": 840,
|
| 314 |
+
"state_dir": null,
|
| 315 |
+
"target_slow_share": 0.49,
|
| 316 |
+
"tier": "normal",
|
| 317 |
+
"tiers": {
|
| 318 |
+
"hard": {
|
| 319 |
+
"fast_only": false,
|
| 320 |
+
"max_new_tokens": 192,
|
| 321 |
+
"reasoning_words": 50
|
| 322 |
+
},
|
| 323 |
+
"long": {
|
| 324 |
+
"fast_only": true,
|
| 325 |
+
"max_prompt_tokens": 6653
|
| 326 |
+
},
|
| 327 |
+
"normal": {
|
| 328 |
+
"fast_only": false,
|
| 329 |
+
"max_prompt_tokens": 6653
|
| 330 |
+
}
|
| 331 |
+
},
|
| 332 |
+
"unparsed_policy": "force_answer"
|
| 333 |
+
},
|
| 334 |
+
"server": {
|
| 335 |
+
"bind": "127.0.0.1",
|
| 336 |
+
"max_questions": 64,
|
| 337 |
+
"max_request_bytes": 16777216,
|
| 338 |
+
"port": 8000,
|
| 339 |
+
"socket_timeout_seconds": 120,
|
| 340 |
+
"warmup": true,
|
| 341 |
+
"warmup_slow_tokens": 2
|
| 342 |
+
},
|
| 343 |
+
"usage": {
|
| 344 |
+
"basis": "engine"
|
| 345 |
+
}
|
| 346 |
+
}
|
service/core.py
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendering, candidate aggregation and service logic, without model imports."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import re
|
| 7 |
+
import string
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import threading
|
| 11 |
+
if __package__:
|
| 12 |
+
from .backends import new_meter, check_deadline
|
| 13 |
+
from .quota import QuotaStateError
|
| 14 |
+
else:
|
| 15 |
+
from backends import new_meter, check_deadline
|
| 16 |
+
from quota import QuotaStateError
|
| 17 |
+
|
| 18 |
+
ROOT = Path(__file__).resolve().parent
|
| 19 |
+
YES_FORMS = ["yes", "Yes", "YES", " yes", " Yes", " YES", "true", "True", " true", " True", "Y", " Y"]
|
| 20 |
+
NO_FORMS = ["no", "No", "NO", " no", " No", " NO", "false", "False", " false", " False", "N", " N"]
|
| 21 |
+
ALIASES = {"boolean": "noul", "enum": "choice", "noul": "noul", "choice": "choice", "score": "score"}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ItemFailure(RuntimeError):
|
| 25 |
+
"""Inference for one item failed; the service answers HTTP 422 and keeps running."""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ServiceFailure(RuntimeError):
|
| 29 |
+
"""Service-level failure; the service answers HTTP 503."""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# The third consecutive item failure is treated as service-level (HTTP 503, service
|
| 33 |
+
# marked failed), so the harness stop rule for consecutive infrastructure errors applies.
|
| 34 |
+
ITEM_FAILURE_LIMIT = 3
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def unrecoverable_engine_error(exc):
|
| 38 |
+
"""True if the __cause__/__context__ graph of exc holds an unrecoverable vLLM engine error.
|
| 39 |
+
|
| 40 |
+
Classes are matched by name, so the check needs no vLLM import: EngineDeadError, or a
|
| 41 |
+
vLLM VLLMServerError subclass other than the per-request EngineGenerateError.
|
| 42 |
+
"""
|
| 43 |
+
pending, seen = [exc], set()
|
| 44 |
+
while pending:
|
| 45 |
+
current = pending.pop()
|
| 46 |
+
if current is None or id(current) in seen:
|
| 47 |
+
continue
|
| 48 |
+
seen.add(id(current))
|
| 49 |
+
classes = type(current).__mro__
|
| 50 |
+
if isinstance(current, ServiceFailure) or any(cls.__name__ == 'EngineDeadError' for cls in classes):
|
| 51 |
+
return True
|
| 52 |
+
vllm_classes = {cls.__name__ for cls in classes
|
| 53 |
+
if cls.__module__ == 'vllm' or cls.__module__.startswith('vllm.')}
|
| 54 |
+
if 'VLLMServerError' in vllm_classes and 'EngineGenerateError' not in vllm_classes:
|
| 55 |
+
return True
|
| 56 |
+
pending.extend((current.__cause__, current.__context__))
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def external_path(path):
|
| 61 |
+
"""Resolve an absolute path that is neither inside the package nor an ancestor of it."""
|
| 62 |
+
candidate = Path(path)
|
| 63 |
+
if not candidate.is_absolute():
|
| 64 |
+
raise ValueError("Path must be absolute: " + str(path))
|
| 65 |
+
result = candidate.resolve()
|
| 66 |
+
if result == ROOT or result.is_relative_to(ROOT) or ROOT.is_relative_to(result):
|
| 67 |
+
raise ValueError("Path must be outside the package and must not contain it: " + str(result))
|
| 68 |
+
return result
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def external_output(path):
|
| 72 |
+
"""Validated output location outside the package; callers create files with mode 'x'."""
|
| 73 |
+
result = external_path(path)
|
| 74 |
+
result.parent.mkdir(parents=True, exist_ok=True)
|
| 75 |
+
return result
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def load_json(path):
|
| 79 |
+
def reject(value):
|
| 80 |
+
raise ValueError("Non-finite JSON value")
|
| 81 |
+
return json.loads(Path(path).read_text(encoding="utf-8"), parse_constant=reject)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_config(path=None):
|
| 85 |
+
config = load_json(path or ROOT / "config.json")
|
| 86 |
+
apply_profile(config)
|
| 87 |
+
validate_config(config)
|
| 88 |
+
return config
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def apply_profile(config):
|
| 92 |
+
import copy
|
| 93 |
+
profile=config.get('profiles',{}).get(config['model']['model_key'])
|
| 94 |
+
if profile is None: raise ValueError('No profile for model_key')
|
| 95 |
+
for section,values in profile.items():
|
| 96 |
+
config.setdefault(section,{}).update(copy.deepcopy(values))
|
| 97 |
+
return apply_budget_tier(config)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def apply_budget_tier(config, tier=None):
|
| 101 |
+
r=config['routing'];tier=tier or r.get('tier','normal')
|
| 102 |
+
if tier not in ('normal','hard','long'):raise ValueError('Unknown quota tier')
|
| 103 |
+
r['tier']=tier
|
| 104 |
+
tiers=r.get('tiers',{})
|
| 105 |
+
values=tiers.get(tier,{})
|
| 106 |
+
for key in ('reasoning_words','max_new_tokens'):
|
| 107 |
+
if key in tiers.get('hard',{}):config['generation'][key]=tiers['hard'][key]
|
| 108 |
+
if 'max_prompt_tokens' in values:config['limits']['max_prompt_tokens']=values['max_prompt_tokens']
|
| 109 |
+
for key in ('reasoning_words','max_new_tokens'):
|
| 110 |
+
if key in values:config['generation'][key]=values[key]
|
| 111 |
+
# Forced long is a runtime admission switch, preserving fast identity.
|
| 112 |
+
return config
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def finite_number(value, name, *, low=None, high=None, positive=False):
|
| 116 |
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
| 117 |
+
raise ValueError(f"{name} must be a finite number")
|
| 118 |
+
if (low is not None and value < low) or (high is not None and value > high) or (positive and value <= 0):
|
| 119 |
+
raise ValueError(f"{name} is outside its allowed range")
|
| 120 |
+
return value
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def validate_config(c):
|
| 124 |
+
if __package__: from .quota import cost_settings
|
| 125 |
+
else: from quota import cost_settings
|
| 126 |
+
cost_settings(c)
|
| 127 |
+
if c['model']['model_key'] not in c.get('profiles',{}):raise ValueError('No profile for model_key')
|
| 128 |
+
if c.get('usage',{}).get('basis','engine') not in ('engine','each_once','submitted'):
|
| 129 |
+
raise ValueError('Invalid usage basis')
|
| 130 |
+
m, r, g, p, s = (c[k] for k in ("model", "routing", "generation", "prompt", "server"))
|
| 131 |
+
if m["backend"] not in ("transformers", "vllm") or m["quant"] not in ("int8", "bf16", "awq"):
|
| 132 |
+
raise ValueError("Unsupported backend or precision")
|
| 133 |
+
if m['backend']=='vllm':
|
| 134 |
+
neutral={'suppress_tokens':None,'begin_suppress_tokens':None,'bad_words_ids':None,
|
| 135 |
+
'forced_bos_token_id':None,'forced_eos_token_id':None,'forced_decoder_ids':None,
|
| 136 |
+
'no_repeat_ngram_size':0,'min_new_tokens':0,'min_length':0,'length_penalty':1.0,'renormalize_logits':False}
|
| 137 |
+
if any(g.get(k,v)!=v for k,v in neutral.items()):raise ValueError('Unsupported vLLM decode override')
|
| 138 |
+
if r["mode"] not in ("routed", "fast_only", "uniform"):
|
| 139 |
+
raise ValueError("Unsupported routing mode")
|
| 140 |
+
tiers=r.get('tiers',{})
|
| 141 |
+
if tiers:
|
| 142 |
+
if set(tiers)!={'normal','hard','long'} or r.get('tier','normal') not in tiers:raise ValueError('Require normal/hard/long tiers')
|
| 143 |
+
if tiers['long'].get('fast_only') is not True:raise ValueError('Long tier must be fast only')
|
| 144 |
+
for tier in ('normal','long'):
|
| 145 |
+
value=tiers[tier].get('max_prompt_tokens')
|
| 146 |
+
if type(value) is not int or not 0<value<=c['limits']['physical_max_prompt_tokens']:raise ValueError('Invalid tier input budget')
|
| 147 |
+
for key in ('reasoning_words','max_new_tokens'):
|
| 148 |
+
if type(tiers['hard'].get(key)) is not int or tiers['hard'][key]<=0:raise ValueError('Invalid hard draft budget')
|
| 149 |
+
finite_number(r["margin_threshold"], "margin_threshold", low=0)
|
| 150 |
+
if r["margin_kind"]=="probdiff" and r["margin_threshold"]>1:
|
| 151 |
+
raise ValueError("probdiff threshold must be <=1")
|
| 152 |
+
if r["margin_kind"] not in ("logratio", "probdiff"):
|
| 153 |
+
raise ValueError("Invalid margin_kind")
|
| 154 |
+
if r.get('long_item_slow_policy','fast_only') not in ('fast_only','allow'):
|
| 155 |
+
raise ValueError('Invalid long item policy')
|
| 156 |
+
if r["unparsed_policy"] not in ("fast", "force_answer"):
|
| 157 |
+
raise ValueError("Invalid unparsed_policy")
|
| 158 |
+
finite_number(r["quota"], "quota", low=0, high=1)
|
| 159 |
+
finite_number(r["fuse"], "fuse", low=0, high=1)
|
| 160 |
+
if r["quota"] > r["fuse"]:
|
| 161 |
+
raise ValueError("quota must not exceed fuse")
|
| 162 |
+
if r["mode"] == "uniform" and (r["quota"] != 1 or r["fuse"] != 1):
|
| 163 |
+
raise ValueError("uniform requires explicit quota=1 and fuse=1; it is an unbounded draft experiment")
|
| 164 |
+
for key in ("reasoning_words", "max_new_tokens"):
|
| 165 |
+
if type(g[key]) is not int or g[key] <= 0:
|
| 166 |
+
raise ValueError(f"{key} must be a positive integer")
|
| 167 |
+
if type(g["seed"]) is not int or type(g["do_sample"]) is not bool:
|
| 168 |
+
raise ValueError("Invalid generation seed or do_sample")
|
| 169 |
+
finite_number(g.get("wall_timeout_seconds",60),"slow wall timeout",positive=True)
|
| 170 |
+
finite_number(g["temperature"], "generation temperature", positive=True)
|
| 171 |
+
finite_number(g["top_p"], "top_p", positive=True, high=1)
|
| 172 |
+
finite_number(g["repetition_penalty"], "repetition_penalty", positive=True)
|
| 173 |
+
if type(g["top_k"]) is not int or g["top_k"] < 0:
|
| 174 |
+
raise ValueError("top_k must be a nonnegative integer")
|
| 175 |
+
for key in ("max_request_bytes", "max_questions", "port"):
|
| 176 |
+
if type(s[key]) is not int or s[key] <= 0:
|
| 177 |
+
raise ValueError(f"{key} must be a positive integer")
|
| 178 |
+
if s["port"] > 65535:
|
| 179 |
+
raise ValueError("Invalid port")
|
| 180 |
+
finite_number(s["socket_timeout_seconds"], "socket timeout", positive=True)
|
| 181 |
+
if not isinstance(p["answer_marker"], str) or not p["answer_marker"].strip() or "\n" in p["answer_marker"]:
|
| 182 |
+
raise ValueError("answer_marker must be a nonempty single line")
|
| 183 |
+
if type(p["enable_thinking"]) is not bool or not isinstance(p["system"], str):
|
| 184 |
+
raise ValueError("Invalid prompt settings")
|
| 185 |
+
for template in p["cot_instructions"].values():
|
| 186 |
+
template.format(reasoning_words=g["reasoning_words"])
|
| 187 |
+
for value in (c["limits"]["max_prompt_tokens"], r["slow_max_prompt_tokens"]):
|
| 188 |
+
if type(value) is not int or value <= 0:
|
| 189 |
+
raise ValueError("Token limits must be positive integers")
|
| 190 |
+
limits=c['limits']
|
| 191 |
+
for name in ('physical_max_prompt_tokens','oom_retry_prompt_tokens','state_chunk_tokens','precompression_trigger_chars_per_token','estimated_chars_per_token'):
|
| 192 |
+
if type(limits[name]) is not int or limits[name]<=0: raise ValueError('Invalid compression limit: '+name)
|
| 193 |
+
for name in ('state_head_tokens','state_tail_tokens','compression_reserve_tokens'):
|
| 194 |
+
if type(limits[name]) is not int or limits[name]<0: raise ValueError('Invalid compression limit: '+name)
|
| 195 |
+
if limits['physical_max_prompt_tokens']<limits['max_prompt_tokens']:
|
| 196 |
+
raise ValueError('Physical maximum must cover soft prompt budget')
|
| 197 |
+
finite_number(c["limits"]["truncation_head_frac"], "head fraction", low=0, high=1)
|
| 198 |
+
if c.get("interface", {}).get("boolean_keys", "request") not in ("request", "true_false", "yes_no"):
|
| 199 |
+
raise ValueError("Unknown boolean key style")
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class UnsupportedRequest(ValueError):
|
| 203 |
+
"""Structurally valid request outside the supported semantic domain (422)."""
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def choice_codes(count):
|
| 207 |
+
if count<=26:return list(string.ascii_uppercase[:count])
|
| 208 |
+
if count>676:raise UnsupportedRequest('At most 676 choices supported')
|
| 209 |
+
return [a+b for a in string.ascii_uppercase for b in string.ascii_uppercase][:count]
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def normalize_question(question):
|
| 213 |
+
if not isinstance(question, dict):
|
| 214 |
+
raise ValueError("Each question must be an object")
|
| 215 |
+
qtype = ALIASES.get(question.get("type"))
|
| 216 |
+
if qtype is None:
|
| 217 |
+
raise UnsupportedRequest("Unsupported question type")
|
| 218 |
+
instructions = question.get("instructions", question.get("description"))
|
| 219 |
+
if not isinstance(instructions, str) or not instructions.strip():
|
| 220 |
+
raise ValueError("Question needs instructions or description")
|
| 221 |
+
criteria = question.get("criteria")
|
| 222 |
+
q = {"request_type": question["type"], "type": qtype, "instructions": instructions, "criteria": criteria}
|
| 223 |
+
labels = None
|
| 224 |
+
if qtype == "noul":
|
| 225 |
+
if criteria is not None and not isinstance(criteria, dict):
|
| 226 |
+
raise ValueError("noul criteria must be an object")
|
| 227 |
+
elif qtype == "score":
|
| 228 |
+
if not isinstance(criteria,list): raise ValueError("score criteria must be an ordered list")
|
| 229 |
+
if not criteria: raise UnsupportedRequest("score criteria is empty")
|
| 230 |
+
else:
|
| 231 |
+
labels = question.get("labels")
|
| 232 |
+
if labels is None:
|
| 233 |
+
labels = list(criteria) if isinstance(criteria, (dict, list)) else None
|
| 234 |
+
if not isinstance(labels,list): raise ValueError("choice needs labels or ordered criteria")
|
| 235 |
+
if not 1<=len(labels)<=676: raise UnsupportedRequest("choice needs 1..676 labels")
|
| 236 |
+
if any(not isinstance(x, str) or not x for x in labels) or len(set(labels)) != len(labels):
|
| 237 |
+
raise ValueError("choice labels must be distinct nonempty strings")
|
| 238 |
+
if criteria is not None and not isinstance(criteria, (dict, list)):
|
| 239 |
+
raise ValueError("choice criteria must be an object or list")
|
| 240 |
+
return q, labels
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def normalize_request(payload, max_questions):
|
| 244 |
+
if not isinstance(payload, dict) or "state" not in payload:
|
| 245 |
+
raise ValueError("Request needs state and questions")
|
| 246 |
+
questions = payload.get("questions")
|
| 247 |
+
if not isinstance(questions, dict) or not 1 <= len(questions) <= max_questions:
|
| 248 |
+
raise ValueError("questions must be a nonempty object within max_questions")
|
| 249 |
+
tasks = []
|
| 250 |
+
for name, question in questions.items():
|
| 251 |
+
if not isinstance(name, str) or not name:
|
| 252 |
+
raise ValueError("Question names must be nonempty strings")
|
| 253 |
+
q, labels = normalize_question(question)
|
| 254 |
+
tasks.append((name, {"state": payload["state"], "question": q, "labels": labels}))
|
| 255 |
+
return tasks
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def letter_forms(c):
|
| 259 |
+
return [c, " " + c, c.lower(), " " + c.lower(), c + ".", "(" + c]
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def digit_forms(d):
|
| 263 |
+
return [d, " " + d, d + "."]
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def render(task):
|
| 267 |
+
# Source-faithful copy of cache_hidden_v2.render; do not deduplicate forms.
|
| 268 |
+
q = task["question"]; qtype = q["type"]; crit = q.get("criteria")
|
| 269 |
+
state = task["state"] if isinstance(task["state"], str) else json.dumps(task["state"], ensure_ascii=False)
|
| 270 |
+
cq = "Context:\n" + state + "\n\nQuestion: " + q["instructions"]
|
| 271 |
+
if qtype == "noul":
|
| 272 |
+
labels = ["no", "yes"]; c = crit or {}; groups = {"yes": YES_FORMS, "no": NO_FORMS}
|
| 273 |
+
parts = [cq] + (["Answer yes if: " + str(c.get("true", "Yes")) + "\nAnswer no if: " + str(c.get("false", "No"))] if (c.get("true") or c.get("false")) else []) + ["Answer with one word: yes or no."]
|
| 274 |
+
elif qtype == "score":
|
| 275 |
+
labels = [str(i) for i in range(len(crit))]; groups = {l: digit_forms(l) for l in labels}
|
| 276 |
+
parts = [cq, "Levels:\n" + "\n".join(str(i) + ": " + str(t) for i, t in enumerate(crit)), "Answer with one number from 0 to " + str(len(crit) - 1) + "."]
|
| 277 |
+
else:
|
| 278 |
+
labels = list(task["labels"]); rubric = crit if isinstance(crit, dict) else {}; codes = choice_codes(len(labels))
|
| 279 |
+
groups = {l: letter_forms(c) for c, l in zip(codes, labels)}
|
| 280 |
+
parts = [cq, "Options:\n" + "\n".join(c + ". " + str(l) + ((": " + str(rubric[l])) if rubric.get(l) else "") for c, l in zip(codes, labels)), "Answer with the letter of the best option."]
|
| 281 |
+
return labels, "\n\n".join(parts), groups
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def parse_answer(generation, qtype="noul", labels=None):
|
| 285 |
+
"""Audit only: keep the explicit-last and standalone fallbacks of the measured parser."""
|
| 286 |
+
if qtype == "noul":
|
| 287 |
+
matches = re.findall(r"ANSWER:\s*(yes|no)", generation, re.I) or re.findall(r"\b(yes|no)\b", generation, re.I)
|
| 288 |
+
return matches[-1].lower() if matches else None
|
| 289 |
+
if qtype == "choice":
|
| 290 |
+
mapping = dict(zip(choice_codes(len(labels or [])), labels or []))
|
| 291 |
+
explicit, fallback = r"ANSWER:\s*([A-Z]{1,2})\b", r"\b([A-Z]{1,2})\b"
|
| 292 |
+
elif qtype == "score":
|
| 293 |
+
mapping = {str(label): str(label) for label in (labels or [])}
|
| 294 |
+
explicit, fallback = r"ANSWER:\s*(\d+)", r"(?<![\w.+-])(\d+)(?!\w|\.\d)"
|
| 295 |
+
else:
|
| 296 |
+
raise ValueError("Unsupported question type")
|
| 297 |
+
matches = re.findall(explicit, generation)
|
| 298 |
+
if matches and matches[-1] in mapping:
|
| 299 |
+
return mapping[matches[-1]]
|
| 300 |
+
matches = [symbol for symbol in re.findall(fallback, generation) if symbol in mapping]
|
| 301 |
+
return mapping[matches[-1]] if matches else None
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def readout_prefix(generation, marker):
|
| 305 |
+
matches = list(re.finditer(r"(?m)^[ \t]*" + re.escape(marker), generation))
|
| 306 |
+
if matches:
|
| 307 |
+
return generation[:matches[-1].end()], False
|
| 308 |
+
return generation.rstrip() + "\n" + marker, True
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def logsumexp(values):
|
| 312 |
+
top = max(values)
|
| 313 |
+
if top == -math.inf: return top
|
| 314 |
+
return top + math.log(sum(math.exp(x - top) for x in values))
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def candidate_ids(backend, groups):
|
| 318 |
+
# Keep multiplicity even when two textual forms map to the same token id.
|
| 319 |
+
result = {}
|
| 320 |
+
for label, forms in groups.items():
|
| 321 |
+
tokens = [backend.encode(form) for form in forms]
|
| 322 |
+
result[label] = [ids[0] for ids in tokens if len(ids) == 1]
|
| 323 |
+
if not any(result.values()):
|
| 324 |
+
raise ValueError("Tokenizer has no single-token candidate forms")
|
| 325 |
+
return result
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def aggregate(logits, groups):
|
| 329 |
+
for token in {t for ids in groups.values() for t in ids}:
|
| 330 |
+
if token not in logits or not math.isfinite(logits[token]):
|
| 331 |
+
raise RuntimeError("Backend omitted a candidate or returned a non-finite logit")
|
| 332 |
+
return {label: logsumexp([logits[t] for t in ids]) if ids else -1e9 for label, ids in groups.items()}
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def validate_calibration(calibration, model_key, path="fast"):
|
| 336 |
+
model = calibration.get("models", {}).get(model_key)
|
| 337 |
+
if model is None:
|
| 338 |
+
raise ValueError("Calibration lacks selected model_key")
|
| 339 |
+
block = model.get(path)
|
| 340 |
+
if block is None:
|
| 341 |
+
raise ValueError("Calibration lacks selected route")
|
| 342 |
+
for kind in ("choice", "score"):
|
| 343 |
+
finite_number(block["temperature_by_kind"][kind], kind + " temperature", positive=True)
|
| 344 |
+
if block["temperature_by_kind"]["noul"] != "one_bin":
|
| 345 |
+
raise ValueError("noul calibration must be one_bin")
|
| 346 |
+
finite_number(block["noul_calibration"]["p_cal"], "p_cal", low=0, high=1)
|
| 347 |
+
return block
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def probabilities(labels, logits, qtype, block):
|
| 351 |
+
if qtype == "noul":
|
| 352 |
+
# Deterministic tie: earliest in render's label order, namely no.
|
| 353 |
+
winner = max(labels, key=logits.__getitem__)
|
| 354 |
+
p_cal = min(1 - 1e-6, max(0.5, block["noul_calibration"]["p_cal"]))
|
| 355 |
+
return {label: p_cal if label == winner else 1 - p_cal for label in labels}
|
| 356 |
+
temperature = block["temperature_by_kind"][qtype]
|
| 357 |
+
# Subtract before dividing to avoid overflow from small positive T.
|
| 358 |
+
top = max(logits.values())
|
| 359 |
+
weights = {label: math.exp((logits[label] - top) / temperature) for label in labels}
|
| 360 |
+
total = sum(weights.values())
|
| 361 |
+
floored = {label: max(1e-6, weights[label] / total) for label in labels}
|
| 362 |
+
return {label: value / sum(floored.values()) for label, value in floored.items()}
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# Rule sections are protected conservatively. Unknown text outside a recognized
|
| 366 |
+
# section is narrative; explicit rule/policy/definition/criteria subtrees are sacred.
|
| 367 |
+
_RULE = re.compile(r'(?im)^\s*(?:R\d+\s*[::]|(?:rules?|polic(?:y|ies)|definitions?|criteria|规则|規則|定义|定義|政策|判定标准)\s*[::])')
|
| 368 |
+
|
| 369 |
+
_RULE_KEY = re.compile(r'^\s*(?:rules?|polic(?:y|ies)|definitions?|criteria|application_rule_set|规则|規則|定义|定義|政策|标准|判定标准)\s*$', re.I)
|
| 370 |
+
|
| 371 |
+
def contains_rule_subtree(value):
|
| 372 |
+
if isinstance(value,str):return bool(_RULE.search(value))
|
| 373 |
+
if isinstance(value,dict):return any(_RULE_KEY.fullmatch(str(k)) or contains_rule_subtree(v) for k,v in value.items())
|
| 374 |
+
if isinstance(value,list):return any(contains_rule_subtree(v) for v in value)
|
| 375 |
+
return False
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
OMISSION = '\n[... state middle omitted ...]\n'
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def lexical_terms(text):
|
| 382 |
+
words = re.findall(r'[a-zA-Z_][\w-]*|\d+(?:\.\d+)?|[\u4e00-\u9fff]', text.lower())
|
| 383 |
+
# Character bigrams preserve Chinese query signal without a segmenter.
|
| 384 |
+
words += re.findall(r'(?=([\u4e00-\u9fff]{2}))', text)
|
| 385 |
+
return set(words)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def shrink_state(state, keep_chars, head_fraction=0.6, *, backend=None, query='',
|
| 389 |
+
head_tokens=512, tail_tokens=512, chunk_tokens=200):
|
| 390 |
+
"""Extract query-relevant chunks in original order, protecting explicit rules.
|
| 391 |
+
|
| 392 |
+
The budget is tokens when a backend is supplied, otherwise characters.
|
| 393 |
+
A contiguous omitted range produces exactly one marker.
|
| 394 |
+
"""
|
| 395 |
+
encode = backend.encode if backend else lambda x: list(x)
|
| 396 |
+
decode = backend.decode if backend else lambda x: ''.join(x)
|
| 397 |
+
query_terms = lexical_terms(query)
|
| 398 |
+
important = {w for w in query_terms if any(c.isdigit() for c in w)}
|
| 399 |
+
important |= {w.lower() for w in re.findall(r'\b(?:[A-Z][a-z]+|[A-Z][A-Z0-9_]+)\b',query)}
|
| 400 |
+
def text_shrink(text, budget):
|
| 401 |
+
if len(encode(text)) <= budget:
|
| 402 |
+
return text
|
| 403 |
+
# Slice original paragraphs including their delimiters; never make a
|
| 404 |
+
# delimiter into an independent compressible paragraph.
|
| 405 |
+
spans=[]; rule_section=False
|
| 406 |
+
for match in re.finditer(r'.+?(?:\n[ \t]*\n|\Z)',text,re.S):
|
| 407 |
+
part=match.group()
|
| 408 |
+
heading=re.match(r'^\s*(?:#{1,6}\s*)?([^\n::]{1,60})[::]\s*(?:\n|$)',part)
|
| 409 |
+
if heading:
|
| 410 |
+
title=heading.group(1)
|
| 411 |
+
if _RULE_KEY.search(title): rule_section=True
|
| 412 |
+
elif re.fullmatch(r'(?i)narrative|events?|history|state|context|data|observations?|背景|叙述|事件|状态|记录',title.strip()): rule_section=False
|
| 413 |
+
protected=rule_section or bool(_RULE.search(part))
|
| 414 |
+
if spans and spans[-1][1]==protected: spans[-1]=(spans[-1][0]+part,protected)
|
| 415 |
+
else: spans.append((part,protected))
|
| 416 |
+
merged=spans
|
| 417 |
+
encoded=[(part,keep,encode(part)) for part,keep in merged]
|
| 418 |
+
total=sum(len(ids) for _,_,ids in encoded)
|
| 419 |
+
head_end=min(head_tokens,total);tail_start=max(head_end,total-tail_tokens)
|
| 420 |
+
blocks=[];cursor=0
|
| 421 |
+
for part,protected,ids in encoded:
|
| 422 |
+
if protected:
|
| 423 |
+
blocks.append((part,len(ids),True));cursor+=len(ids)
|
| 424 |
+
continue
|
| 425 |
+
# Sentence/newline boundaries first, word boundaries for oversized
|
| 426 |
+
# sentences. Never decode an arbitrary token slice into a block.
|
| 427 |
+
sentences=re.findall(r'.*?(?:[.!?。!?](?=\s|$)|\n|$)',part,re.S)
|
| 428 |
+
pieces=[]
|
| 429 |
+
for sentence in sentences:
|
| 430 |
+
if not sentence:continue
|
| 431 |
+
if len(encode(sentence))<=chunk_tokens:pieces.append(sentence)
|
| 432 |
+
else:
|
| 433 |
+
words=re.findall(r'\s+|\S+\s*',sentence)
|
| 434 |
+
chunk=''
|
| 435 |
+
for word in words:
|
| 436 |
+
if chunk and len(encode(chunk+word))>chunk_tokens:
|
| 437 |
+
pieces.append(chunk);chunk=''
|
| 438 |
+
chunk+=word
|
| 439 |
+
if chunk:pieces.append(chunk)
|
| 440 |
+
chunk=''
|
| 441 |
+
def add(value):
|
| 442 |
+
nonlocal cursor
|
| 443 |
+
size=len(encode(value))
|
| 444 |
+
edge=cursor<head_end or cursor+size>tail_start
|
| 445 |
+
blocks.append((value,size,edge));cursor+=size
|
| 446 |
+
for piece in pieces:
|
| 447 |
+
if chunk and len(encode(chunk+piece))>chunk_tokens:
|
| 448 |
+
add(chunk);chunk=''
|
| 449 |
+
chunk+=piece
|
| 450 |
+
if chunk:add(chunk)
|
| 451 |
+
selected={i for i,(_,_,keep) in enumerate(blocks) if keep}
|
| 452 |
+
marker_cost=len(encode(OMISSION))
|
| 453 |
+
used=sum(blocks[i][1] for i in selected)+marker_cost*(len(selected)+1)
|
| 454 |
+
ranked=[]
|
| 455 |
+
for i,(part,size,keep) in enumerate(blocks):
|
| 456 |
+
terms=lexical_terms(part); overlap=query_terms & terms
|
| 457 |
+
score=(len(overlap)+2*len(important & terms))/max(1,math.sqrt(len(terms)))
|
| 458 |
+
if not keep: ranked.append((-score,i))
|
| 459 |
+
for _,i in sorted(ranked):
|
| 460 |
+
if used+blocks[i][1]+marker_cost <= budget:
|
| 461 |
+
selected.add(i); used+=blocks[i][1]+marker_cost
|
| 462 |
+
def join(indices):
|
| 463 |
+
out=[]; omitted=False
|
| 464 |
+
for i,(part,_,_) in enumerate(blocks):
|
| 465 |
+
if i in indices:
|
| 466 |
+
if omitted: out.append(OMISSION)
|
| 467 |
+
out.append(part); omitted=False
|
| 468 |
+
else: omitted=True
|
| 469 |
+
if omitted: out.append(OMISSION)
|
| 470 |
+
return ''.join(out)
|
| 471 |
+
result=join(selected)
|
| 472 |
+
# Recover conservative omission-marker reserves using adjacent blocks.
|
| 473 |
+
while len(encode(result))<.8*budget:
|
| 474 |
+
neighbors=sorted({j for i in selected for j in (i-1,i+1) if 0<=j<len(blocks) and j not in selected})
|
| 475 |
+
added=False
|
| 476 |
+
for j in neighbors:
|
| 477 |
+
candidate=join(selected|{j})
|
| 478 |
+
if len(encode(candidate))<=budget:
|
| 479 |
+
selected.add(j);result=candidate;added=True
|
| 480 |
+
if not added:break
|
| 481 |
+
if len(encode(result)) <= budget: return result
|
| 482 |
+
# Protected material survives the soft budget. For an unusually small
|
| 483 |
+
# budget shrink only narrative edges globally, retaining the rules.
|
| 484 |
+
if any(keep for _,keep in merged):
|
| 485 |
+
protected_text=''.join(part for part,keep in merged if keep)
|
| 486 |
+
allowance=max(0,budget-len(encode(protected_text))-marker_cost*(len(merged)+1))
|
| 487 |
+
if not allowance: return protected_text
|
| 488 |
+
n=sum(len(encode(part)) for part,keep in merged if not keep)
|
| 489 |
+
h=int(allowance*head_fraction);t=allowance-h
|
| 490 |
+
out=[];offset=0;omitted=False
|
| 491 |
+
for part,keep in merged:
|
| 492 |
+
if keep:
|
| 493 |
+
if omitted:out.append(OMISSION);omitted=False
|
| 494 |
+
out.append(part);continue
|
| 495 |
+
ids=encode(part)
|
| 496 |
+
head=ids[:max(0,min(len(ids),h-offset))]
|
| 497 |
+
tail=ids[max(0,n-t-offset):] if offset+len(ids)>n-t else []
|
| 498 |
+
if head:
|
| 499 |
+
if omitted:out.append(OMISSION);omitted=False
|
| 500 |
+
out.append(decode(head))
|
| 501 |
+
if len(head)+len(tail)<len(ids):omitted=True
|
| 502 |
+
if tail:
|
| 503 |
+
if omitted:out.append(OMISSION);omitted=False
|
| 504 |
+
out.append(decode(tail))
|
| 505 |
+
offset+=len(ids)
|
| 506 |
+
if omitted:out.append(OMISSION)
|
| 507 |
+
return ''.join(out)
|
| 508 |
+
ids=encode(text); available=max(0,budget-marker_cost)
|
| 509 |
+
h=int(available*head_fraction); t=available-h
|
| 510 |
+
return decode(ids[:h])+OMISSION+(decode(ids[-t:]) if t else '')
|
| 511 |
+
|
| 512 |
+
if isinstance(state,str): return text_shrink(state,max(0,keep_chars))
|
| 513 |
+
if isinstance(state,dict):
|
| 514 |
+
protected={k:v for k,v in state.items() if _RULE_KEY.search(str(k)) or
|
| 515 |
+
(not isinstance(v,(dict,list)) and len(str(v))<=200)}
|
| 516 |
+
rest=[k for k in state if k not in protected]
|
| 517 |
+
remaining=max(0,keep_chars-len(encode(json.dumps(protected,ensure_ascii=False))))
|
| 518 |
+
sizes={k:len(encode(json.dumps(state[k],ensure_ascii=False))) for k in rest}
|
| 519 |
+
total=max(1,sum(sizes.values()))
|
| 520 |
+
return {k:v if k in protected else shrink_state(v,int(remaining*sizes[k]/total),head_fraction,
|
| 521 |
+
backend=backend,query=query,head_tokens=head_tokens,tail_tokens=tail_tokens,chunk_tokens=chunk_tokens)
|
| 522 |
+
for k,v in state.items()}
|
| 523 |
+
if isinstance(state,list):
|
| 524 |
+
if len(encode(json.dumps(state,ensure_ascii=False)))<=keep_chars: return state
|
| 525 |
+
if not state: return state
|
| 526 |
+
# Budget whole entries, protect explicit rule entries; preserve both ends.
|
| 527 |
+
selected={0,len(state)-1}|{i for i,v in enumerate(state) if contains_rule_subtree(v)}
|
| 528 |
+
ranked=sorted(range(1,len(state)-1),key=lambda i:(-len(lexical_terms(str(state[i])) & query_terms),i))
|
| 529 |
+
used=sum(len(encode(json.dumps(state[i],ensure_ascii=False))) for i in selected)
|
| 530 |
+
for i in ranked:
|
| 531 |
+
size=len(encode(json.dumps(state[i],ensure_ascii=False)))
|
| 532 |
+
if i not in selected and used+size+len(OMISSION)<keep_chars: selected.add(i); used+=size
|
| 533 |
+
protected_entries={i for i in selected if isinstance(state[i],str) and _RULE.search(state[i])}
|
| 534 |
+
protected_size=sum(len(encode(json.dumps(state[i],ensure_ascii=False))) for i in protected_entries)
|
| 535 |
+
narrative_size=max(1,sum(len(encode(json.dumps(state[i],ensure_ascii=False))) for i in selected-protected_entries))
|
| 536 |
+
free=max(0,keep_chars-protected_size-len(selected)*len(OMISSION))
|
| 537 |
+
result=[]; omitted=False
|
| 538 |
+
for i,v in enumerate(state):
|
| 539 |
+
if i in selected:
|
| 540 |
+
if omitted: result.append(OMISSION.strip())
|
| 541 |
+
if i not in protected_entries and len(str(v))>200:
|
| 542 |
+
size=len(encode(json.dumps(v,ensure_ascii=False)))
|
| 543 |
+
v=shrink_state(v,int(free*size/narrative_size),head_fraction,backend=backend,
|
| 544 |
+
query=query,head_tokens=head_tokens,tail_tokens=tail_tokens,chunk_tokens=chunk_tokens)
|
| 545 |
+
result.append(v);omitted=False
|
| 546 |
+
else: omitted=True
|
| 547 |
+
return result
|
| 548 |
+
return state
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def prepare_task(task, backend, config):
|
| 552 |
+
def prompts(value):
|
| 553 |
+
labels,text,groups=render(value)
|
| 554 |
+
instruction=config['prompt']['cot_instructions'][value['question']['type']].format(
|
| 555 |
+
reasoning_words=config['generation']['reasoning_words']).replace('ANSWER:',config['prompt']['answer_marker'])
|
| 556 |
+
system,think=config['prompt']['system'],config['prompt']['enable_thinking']
|
| 557 |
+
return labels,groups,backend.prompt_ids(system,text,think),backend.prompt_ids(system,text+'\n'+instruction,think)
|
| 558 |
+
limits=config['limits']; budget=limits['max_prompt_tokens'];normal_budget=budget
|
| 559 |
+
long_budget=min(budget,config['routing'].get('tiers',{}).get('long',{}).get('max_prompt_tokens',budget))
|
| 560 |
+
query=task['question']['instructions']+' '+json.dumps(task['question'].get('criteria'),ensure_ascii=False)+' '+str(task.get('labels') or '')
|
| 561 |
+
raw=task['state'] if isinstance(task['state'],str) else json.dumps(task['state'],ensure_ascii=False)
|
| 562 |
+
precompressed=len(raw)>limits['precompression_trigger_chars_per_token']*budget
|
| 563 |
+
source=task
|
| 564 |
+
if precompressed:budget=long_budget
|
| 565 |
+
if precompressed:
|
| 566 |
+
ratio=limits['estimated_chars_per_token']
|
| 567 |
+
source=dict(task,state=shrink_state(task['state'],budget*ratio,limits['truncation_head_frac'],query=query,
|
| 568 |
+
head_tokens=limits['state_head_tokens']*ratio,tail_tokens=limits['state_tail_tokens']*ratio,
|
| 569 |
+
chunk_tokens=limits['state_chunk_tokens']*ratio))
|
| 570 |
+
original=prompts(source); original_count=len(original[2])
|
| 571 |
+
long_input=precompressed or original_count>normal_budget
|
| 572 |
+
if long_input:budget=long_budget
|
| 573 |
+
current,prepared=source,original
|
| 574 |
+
refilled=False
|
| 575 |
+
if precompressed and original_count<.8*budget:
|
| 576 |
+
fixed=len(prompts(dict(task,state=''))[2])
|
| 577 |
+
refill=shrink_state(task['state'],max(0,budget-fixed-limits['compression_reserve_tokens']),
|
| 578 |
+
limits['truncation_head_frac'],backend=backend,query=query,
|
| 579 |
+
head_tokens=limits['state_head_tokens'],tail_tokens=limits['state_tail_tokens'],chunk_tokens=limits['state_chunk_tokens'])
|
| 580 |
+
candidate=dict(task,state=refill);candidate_prompts=prompts(candidate)
|
| 581 |
+
if len(candidate_prompts[2])<=budget:
|
| 582 |
+
current,prepared=candidate,candidate_prompts;refilled=True
|
| 583 |
+
if original_count>budget:
|
| 584 |
+
fixed=len(prompts(dict(task,state=''))[2])
|
| 585 |
+
query=task['question']['instructions']+' '+json.dumps(task['question'].get('criteria'),ensure_ascii=False)+' '+str(task.get('labels') or '')
|
| 586 |
+
allowance=max(0,budget-fixed-limits['compression_reserve_tokens'])
|
| 587 |
+
for _ in range(3):
|
| 588 |
+
current=dict(task,state=shrink_state(source['state'],allowance,limits['truncation_head_frac'],
|
| 589 |
+
backend=backend,query=query,head_tokens=limits['state_head_tokens'],
|
| 590 |
+
tail_tokens=limits['state_tail_tokens'],chunk_tokens=limits['state_chunk_tokens']))
|
| 591 |
+
prepared=prompts(current); size=len(prepared[2])
|
| 592 |
+
if size<=budget: break
|
| 593 |
+
allowance=max(0,allowance-(size-budget)-limits['compression_reserve_tokens'])
|
| 594 |
+
# Compression cannot remove sacred rule/question/option material. The
|
| 595 |
+
# final physical guard always bounds tensors, with explicit metadata.
|
| 596 |
+
kept=len(prepared[2]); physical=limits['physical_max_prompt_tokens']
|
| 597 |
+
hard_cut=kept>physical
|
| 598 |
+
if hard_cut:
|
| 599 |
+
def cut(ids):
|
| 600 |
+
h=int(physical*limits['truncation_head_frac']);t=physical-h
|
| 601 |
+
return ids if len(ids)<=physical else ids[:h]+ids[-t:]
|
| 602 |
+
prepared=prepared[:2]+(cut(prepared[2]),cut(prepared[3]))
|
| 603 |
+
return current,prepared,{'original_prompt_tokens':original_count if not precompressed else None,
|
| 604 |
+
'original_prompt_tokens_estimate':math.ceil(len(raw)/limits['estimated_chars_per_token']) if precompressed else original_count,
|
| 605 |
+
'long_input':long_input,'character_precompressed':precompressed,'adjacent_refill':refilled,
|
| 606 |
+
'kept_prompt_tokens':len(prepared[2]),
|
| 607 |
+
'slow_prompt_exceeds_budget':len(prepared[3])>min(budget,config['routing']['slow_max_prompt_tokens']),
|
| 608 |
+
'truncated':current['state']!=task['state'] or hard_cut,
|
| 609 |
+
'protected_content_exceeds_budget':kept>budget,'physical_truncated':hard_cut}
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def sequence_scores(session, backend, groups, meter=None):
|
| 613 |
+
"""Full-vocabulary chain probabilities; copies branch KV, never prompt prefill.
|
| 614 |
+
|
| 615 |
+
Keep every form (including repeated tokenizations) with its original weight.
|
| 616 |
+
"""
|
| 617 |
+
cache = {(): session}
|
| 618 |
+
continuation_tokens = 0
|
| 619 |
+
normalized = {}
|
| 620 |
+
path_scores = {(): 0.0}
|
| 621 |
+
def score(ids):
|
| 622 |
+
nonlocal continuation_tokens
|
| 623 |
+
prefix = ()
|
| 624 |
+
for token in ids:
|
| 625 |
+
check_deadline(meter)
|
| 626 |
+
child = prefix + (token,)
|
| 627 |
+
if child not in path_scores:
|
| 628 |
+
parent = cache[prefix]
|
| 629 |
+
if prefix not in normalized:
|
| 630 |
+
wanted=sorted({encoded[len(prefix)] for forms in groups.values() for form in forms
|
| 631 |
+
if (encoded:=backend.encode(form))[:len(prefix)]==list(prefix) and len(encoded)>len(prefix)})
|
| 632 |
+
if hasattr(parent,'log_probs'):
|
| 633 |
+
normalized[prefix]=parent.log_probs(wanted)
|
| 634 |
+
else:
|
| 635 |
+
logits=parent.logits; lse=logsumexp(list(logits.values()))
|
| 636 |
+
normalized[prefix]={t:logits[t]-lse for t in wanted if t in logits}
|
| 637 |
+
value=normalized[prefix].get(token)
|
| 638 |
+
if value is None or not math.isfinite(value): raise RuntimeError('Missing candidate token')
|
| 639 |
+
path_scores[child]=path_scores[prefix]+value
|
| 640 |
+
prefix = child
|
| 641 |
+
if prefix not in cache and len(prefix) < len(ids):
|
| 642 |
+
branch = cache[prefix[:-1]].fork()
|
| 643 |
+
if meter is not None and not getattr(branch, 'handles_meter', False):
|
| 644 |
+
meter['output'] += 1
|
| 645 |
+
meter['probes'] += 1
|
| 646 |
+
if getattr(branch,'meter',None) is None:
|
| 647 |
+
meter['engine_input']=meter.get('engine_input',0)+1; meter['engine_submitted']+=1
|
| 648 |
+
branch.advance([token])
|
| 649 |
+
continuation_tokens += 1
|
| 650 |
+
cache[prefix] = branch
|
| 651 |
+
return path_scores[prefix]
|
| 652 |
+
digit_ids=getattr(backend,'digit_start_ids',None)
|
| 653 |
+
if digit_ids is None:
|
| 654 |
+
# Synthetic tokenizer fallback only; real backends scan the vocabulary.
|
| 655 |
+
digit_ids={ids[0] for d in string.digits if len(ids:=backend.encode(d))==1}
|
| 656 |
+
terminals={}
|
| 657 |
+
def termination(ids):
|
| 658 |
+
nonlocal continuation_tokens
|
| 659 |
+
key=tuple(ids)
|
| 660 |
+
if key not in terminals:
|
| 661 |
+
if key not in cache:
|
| 662 |
+
branch=cache[key[:-1]].fork()
|
| 663 |
+
if meter is not None and not getattr(branch,'handles_meter',False):
|
| 664 |
+
meter['output']+=1;meter['probes']+=1
|
| 665 |
+
if getattr(branch,'meter',None) is None:
|
| 666 |
+
meter['engine_input']=meter.get('engine_input',0)+1;meter['engine_submitted']+=1
|
| 667 |
+
branch.advance([key[-1]]);continuation_tokens+=1;cache[key]=branch
|
| 668 |
+
se=cache[key]
|
| 669 |
+
if hasattr(se,'non_digit_logprob'):
|
| 670 |
+
value=se.non_digit_logprob(digit_ids)
|
| 671 |
+
else:
|
| 672 |
+
logits=se.logits; z=logsumexp(list(logits.values()))
|
| 673 |
+
allowed=[v for t,v in logits.items() if t not in digit_ids]
|
| 674 |
+
value=logsumexp(allowed)-z if allowed else -math.inf
|
| 675 |
+
terminals[key]=value
|
| 676 |
+
return terminals[key]
|
| 677 |
+
result={}
|
| 678 |
+
for label,forms in groups.items():
|
| 679 |
+
is_prefix=label.isdigit() and any(other!=label and other.startswith(label) for other in groups)
|
| 680 |
+
values=[]
|
| 681 |
+
for form in forms:
|
| 682 |
+
ids=backend.encode(form)
|
| 683 |
+
if not ids: continue
|
| 684 |
+
value=score(ids)
|
| 685 |
+
if is_prefix and form.strip().isdigit(): value+=termination(ids)
|
| 686 |
+
values.append(value)
|
| 687 |
+
result[label]=logsumexp(values)
|
| 688 |
+
return result, continuation_tokens
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def slow_readout(backend, ids, groups, config, meter):
|
| 692 |
+
"""One autoregressive session: one prompt prefill, cached draft/marker/readout."""
|
| 693 |
+
if hasattr(backend,'slow_session'):
|
| 694 |
+
session,parsed=backend.slow_session(ids,config['routing']['unparsed_policy'],meter)
|
| 695 |
+
if session is None: return None,meter['draft'],False,False,0
|
| 696 |
+
# Marginalize textual whitespace forms; no full-vocabulary CPU copy.
|
| 697 |
+
scores,probes=sequence_scores(session,backend,groups,meter)
|
| 698 |
+
return scores,meter['draft']+meter['forced'],not parsed,False,probes
|
| 699 |
+
meter['prompt'] += len(ids)
|
| 700 |
+
meter['engine_submitted'] += len(ids)
|
| 701 |
+
meter['engine_input']=meter.get('engine_input',0)+len(ids)
|
| 702 |
+
meter['engine_calls']=meter.get('engine_calls',0)+1
|
| 703 |
+
session = backend.open_session(ids)
|
| 704 |
+
import random
|
| 705 |
+
rng = random.Random(config['generation']['seed'])
|
| 706 |
+
generated = []
|
| 707 |
+
marker = config['prompt']['answer_marker']
|
| 708 |
+
found = False
|
| 709 |
+
for _ in range(config['generation']['max_new_tokens']):
|
| 710 |
+
check_deadline(meter)
|
| 711 |
+
logits = dict(session.logits)
|
| 712 |
+
g = config['generation']
|
| 713 |
+
for seen in set(generated):
|
| 714 |
+
value = logits[seen]
|
| 715 |
+
logits[seen] = value * g['repetition_penalty'] if value < 0 else value / g['repetition_penalty']
|
| 716 |
+
token = max(logits, key=logits.get)
|
| 717 |
+
if g['do_sample']:
|
| 718 |
+
ordered = sorted(logits, key=logits.get, reverse=True)
|
| 719 |
+
if g['top_k']:
|
| 720 |
+
ordered = ordered[:g['top_k']]
|
| 721 |
+
weights = [math.exp((logits[t]-logits[token])/g['temperature']) for t in ordered]
|
| 722 |
+
mass, cutoff = 0, sum(weights)*g['top_p']
|
| 723 |
+
end = 0
|
| 724 |
+
while end < len(weights) and mass < cutoff:
|
| 725 |
+
mass += weights[end]
|
| 726 |
+
end += 1
|
| 727 |
+
token = rng.choices(ordered[:end], weights=weights[:end])[0]
|
| 728 |
+
generated.append(token)
|
| 729 |
+
meter['output'] += 1
|
| 730 |
+
meter['draft'] += 1
|
| 731 |
+
meter['engine_generated']=meter.get('engine_generated',0)+1
|
| 732 |
+
session.advance([token])
|
| 733 |
+
text = backend.decode(generated)
|
| 734 |
+
if re.search(r'(?m)^[ \t]*' + re.escape(marker) + r'$', text):
|
| 735 |
+
found = True
|
| 736 |
+
break
|
| 737 |
+
if token in getattr(backend, 'eos_ids', ()):
|
| 738 |
+
break
|
| 739 |
+
if not found and config['routing']['unparsed_policy'] == 'fast':
|
| 740 |
+
return None, len(generated), False, False, 0
|
| 741 |
+
if not found:
|
| 742 |
+
suffix = backend.encode('\n' + marker)
|
| 743 |
+
meter['output'] += len(suffix)
|
| 744 |
+
meter['forced'] += len(suffix)
|
| 745 |
+
meter['engine_input']=meter.get('engine_input',0)+len(suffix);meter['engine_submitted']+=len(suffix)
|
| 746 |
+
session.advance(suffix)
|
| 747 |
+
generated.extend(suffix)
|
| 748 |
+
# Whitespace is a form branch, never a top-1 destructive advance.
|
| 749 |
+
whitespace = False
|
| 750 |
+
scores, probes = sequence_scores(session, backend, groups, meter)
|
| 751 |
+
return scores, len(generated), not found, whitespace, probes
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
def label_margin(labels, scores, kind="logratio"):
|
| 755 |
+
"""Aggregated label distribution at T=1, before any calibration/floor."""
|
| 756 |
+
if len(labels) < 2:
|
| 757 |
+
return math.inf
|
| 758 |
+
values = sorted((scores[label] for label in labels), reverse=True)
|
| 759 |
+
if kind == 'logratio':
|
| 760 |
+
return values[0] - values[1]
|
| 761 |
+
if kind != 'probdiff':
|
| 762 |
+
raise ValueError('Invalid margin kind')
|
| 763 |
+
weights = [math.exp(v-values[0]) for v in values]
|
| 764 |
+
return (weights[0]-weights[1])/sum(weights)
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def fast_readout(backend, ids, labels, groups, meter):
|
| 768 |
+
meter["prompt"]+=len(ids)
|
| 769 |
+
if not getattr(backend,"engine_metering",False):
|
| 770 |
+
meter["engine_submitted"]+=len(ids);meter['engine_input']+=len(ids)
|
| 771 |
+
meter['hf_prefill_tokens']+=len(ids)
|
| 772 |
+
try:
|
| 773 |
+
tokens = candidate_ids(backend, groups)
|
| 774 |
+
except ValueError:
|
| 775 |
+
tokens = {label: [] for label in labels}
|
| 776 |
+
probes = 0
|
| 777 |
+
if any(not values for values in tokens.values()):
|
| 778 |
+
session=backend.open_session(ids,meter=meter) if getattr(backend,'engine_metering',False) else backend.open_session(ids)
|
| 779 |
+
if hasattr(session,'cache'): session.meter=meter
|
| 780 |
+
scores, probes = sequence_scores(session, backend, groups, meter)
|
| 781 |
+
else:
|
| 782 |
+
wanted = sorted({t for values in tokens.values() for t in values})
|
| 783 |
+
logits=backend.next_logits(ids,wanted,meter=meter) if getattr(backend,'engine_metering',False) else backend.next_logits(ids,wanted)
|
| 784 |
+
scores = aggregate(logits, tokens)
|
| 785 |
+
if not getattr(backend,'engine_metering',False):
|
| 786 |
+
meter['output']+=backend.readout_output_tokens;meter['probes']+=backend.readout_output_tokens
|
| 787 |
+
meter['engine_generated']+=backend.readout_output_tokens
|
| 788 |
+
return scores, probes + backend.readout_output_tokens
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
def release_failed_session(backend):
|
| 792 |
+
import gc
|
| 793 |
+
gc.collect()
|
| 794 |
+
torch=getattr(backend,'torch',None)
|
| 795 |
+
cuda=getattr(torch,'cuda',None)
|
| 796 |
+
if cuda is not None:
|
| 797 |
+
try: cuda.empty_cache()
|
| 798 |
+
except Exception: pass
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def fast_attempt_with_retry(backend,task,config,ids,labels,groups,meter):
|
| 802 |
+
try:
|
| 803 |
+
scores,out=fast_readout(backend,ids,labels,groups,meter)
|
| 804 |
+
return scores,out,None
|
| 805 |
+
except Exception as exc:
|
| 806 |
+
if unrecoverable_engine_error(exc):
|
| 807 |
+
raise
|
| 808 |
+
cuda=getattr(getattr(backend,'torch',None),'cuda',None)
|
| 809 |
+
oom_type=getattr(cuda,'OutOfMemoryError',MemoryError)
|
| 810 |
+
if not isinstance(exc,(MemoryError,oom_type)) and 'out of memory' not in str(exc).lower():
|
| 811 |
+
raise
|
| 812 |
+
# Leave the failed forward traceback before clearing allocator state.
|
| 813 |
+
release_failed_session(backend)
|
| 814 |
+
import copy
|
| 815 |
+
reduced=copy.deepcopy(config)
|
| 816 |
+
budget=min(config['limits']['oom_retry_prompt_tokens'],max(1,config['limits']['max_prompt_tokens']//2))
|
| 817 |
+
reduced['limits']['max_prompt_tokens']=budget
|
| 818 |
+
reduced['limits']['physical_max_prompt_tokens']=budget
|
| 819 |
+
_,(_,new_groups,new_ids,_),length=prepare_task(task,backend,reduced)
|
| 820 |
+
scores,out=fast_readout(backend,new_ids,labels,new_groups,meter)
|
| 821 |
+
return scores,out,length
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
def slow_attempt(backend,ids,groups,config,meter):
|
| 825 |
+
# Catch in this frame, then leave except before collecting. The failed
|
| 826 |
+
# readout/generate traceback (and its KV references) no longer exists.
|
| 827 |
+
try:
|
| 828 |
+
return slow_readout(backend,ids,groups,config,meter)
|
| 829 |
+
except Exception as exc:
|
| 830 |
+
if unrecoverable_engine_error(exc):
|
| 831 |
+
raise ServiceFailure('unrecoverable engine failure') from exc
|
| 832 |
+
timed_out=isinstance(exc,TimeoutError)
|
| 833 |
+
release_failed_session(backend)
|
| 834 |
+
if timed_out: raise TimeoutError('Slow deadline exceeded')
|
| 835 |
+
raise RuntimeError('Slow session failed')
|
| 836 |
+
|
| 837 |
+
|
| 838 |
+
def bounded_slow_attempt(backend,ids,groups,config,meter,model_lock, *, on_service_failure=None):
|
| 839 |
+
"""Bound response wait; an in-flight synchronous engine call drains under lock.
|
| 840 |
+
|
| 841 |
+
No worker may start another engine call after cancellation. Timeout usage is
|
| 842 |
+
explicitly estimated and reconciliation rejects the run. An unrecoverable engine
|
| 843 |
+
error is reported through on_service_failure even after the response timed out.
|
| 844 |
+
"""
|
| 845 |
+
import queue,time
|
| 846 |
+
result=queue.Queue(maxsize=1)
|
| 847 |
+
timeout=config['generation'].get('wall_timeout_seconds',60)
|
| 848 |
+
meter['cancel_event']=threading.Event();meter['deadline']=time.monotonic()+timeout
|
| 849 |
+
def work():
|
| 850 |
+
try:
|
| 851 |
+
with model_lock:
|
| 852 |
+
check_deadline(meter)
|
| 853 |
+
value=slow_attempt(backend,ids,groups,config,meter)
|
| 854 |
+
check_deadline(meter)
|
| 855 |
+
result.put((True,value))
|
| 856 |
+
except Exception as exc:
|
| 857 |
+
fatal=unrecoverable_engine_error(exc)
|
| 858 |
+
if fatal and on_service_failure is not None:
|
| 859 |
+
on_service_failure()
|
| 860 |
+
result.put((False,ServiceFailure if fatal else type(exc)))
|
| 861 |
+
worker=threading.Thread(target=work,daemon=True);worker.start()
|
| 862 |
+
try: ok,value=result.get(timeout=timeout)
|
| 863 |
+
except queue.Empty:
|
| 864 |
+
meter['cancel_event'].set();meter['engine_usage_unknown']=True
|
| 865 |
+
raise TimeoutError('Slow wall timeout; in-flight call draining')
|
| 866 |
+
if not ok:
|
| 867 |
+
if value is ServiceFailure: raise ServiceFailure('unrecoverable engine failure')
|
| 868 |
+
if value is TimeoutError: raise TimeoutError('Slow wall timeout')
|
| 869 |
+
raise RuntimeError('Slow session failed')
|
| 870 |
+
return value
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
def threshold_identity(config):
|
| 874 |
+
import hashlib
|
| 875 |
+
import inspect
|
| 876 |
+
# The renderer source binds label layout, and settings bind both prompts.
|
| 877 |
+
canonical=json.dumps({'prompt':{k:config['prompt'][k] for k in ('system','enable_thinking')},'render':inspect.getsource(render),'choice_codes':inspect.getsource(choice_codes),
|
| 878 |
+
'generation':{k:v for k,v in config['generation'].items() if k not in ('max_new_tokens','reasoning_words','wall_timeout_seconds')}},sort_keys=True,ensure_ascii=False)
|
| 879 |
+
m=config['model']
|
| 880 |
+
return dict(model_key=m['model_key'],quant=m['quant'],adapter=m.get('adapter'),
|
| 881 |
+
prompt_sha=hashlib.sha256(canonical.encode('utf-8')).hexdigest(),backend=m['backend'],
|
| 882 |
+
margin_kind=config['routing']['margin_kind'],max_new_tokens=config['generation']['max_new_tokens'],
|
| 883 |
+
reasoning_words=config['generation']['reasoning_words'],
|
| 884 |
+
max_prompt_tokens=config['limits']['max_prompt_tokens'],request_contract='typesafe.criteria.v1')
|
| 885 |
+
|
| 886 |
+
|
| 887 |
+
def validate_threshold(config,allow_placeholder=False):
|
| 888 |
+
identity=threshold_identity(config)
|
| 889 |
+
fit=config['routing'].get('margin_threshold_fitted_for',{})
|
| 890 |
+
reasons=[]
|
| 891 |
+
if fit.get('placeholder',True): reasons.append('placeholder')
|
| 892 |
+
if any(fit.get(k)!=identity[k] for k in ('model_key','quant','adapter','prompt_sha','margin_kind','max_prompt_tokens','request_contract')):
|
| 893 |
+
reasons.append('identity mismatch')
|
| 894 |
+
if fit.get('backend')!=identity['backend']: reasons.append('backend mismatch')
|
| 895 |
+
if reasons and config['routing']['mode']=='routed' and not allow_placeholder:
|
| 896 |
+
raise ValueError('Unfitted margin threshold: '+', '.join(reasons))
|
| 897 |
+
return ['margin threshold degraded: '+', '.join(reasons)] if reasons else []
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
class Service:
|
| 901 |
+
def __init__(self, config, calibration, backend, counter, *, allow_placeholder_threshold=False):
|
| 902 |
+
validate_config(config)
|
| 903 |
+
threshold_warnings=validate_threshold(config,allow_placeholder_threshold)
|
| 904 |
+
self.model_lock = threading.RLock()
|
| 905 |
+
self._failure_lock = threading.Lock()
|
| 906 |
+
self.item_failure_streak = 0
|
| 907 |
+
self.dead = False
|
| 908 |
+
self.config, self.backend, self.counter = config, backend, counter
|
| 909 |
+
counter.configure_cost(config)
|
| 910 |
+
key = config['model']['model_key']
|
| 911 |
+
self.calibration = {path: validate_calibration(calibration, key, path) for path in ('fast', 'slow')}
|
| 912 |
+
model = calibration['models'][key]
|
| 913 |
+
self.warnings = ['calibration placeholder: refit required'] if (
|
| 914 |
+
model.get('placeholder') or any(b.get('placeholder') for b in self.calibration.values())) else []
|
| 915 |
+
self.warnings.extend(threshold_warnings)
|
| 916 |
+
if not config['routing'].get('cost_fuse',{}).get('enabled',True):
|
| 917 |
+
self.warnings.append('cost fuse explicitly disabled')
|
| 918 |
+
if 'fallback' in model:
|
| 919 |
+
self.calibration['fallback']=validate_calibration(calibration,key,'fallback')
|
| 920 |
+
else:
|
| 921 |
+
self.calibration['fallback']=self.calibration['fast']
|
| 922 |
+
self.warnings.append('fallback calibration missing: using fast block')
|
| 923 |
+
|
| 924 |
+
def _mark_dead(self):
|
| 925 |
+
with self._failure_lock:
|
| 926 |
+
self.dead = True
|
| 927 |
+
|
| 928 |
+
def is_dead(self):
|
| 929 |
+
"""Service failed: an unrecoverable engine error, the engine's own dead flag, or the
|
| 930 |
+
third consecutive item failure. Reading the flag performs no engine call."""
|
| 931 |
+
if self.dead:
|
| 932 |
+
return True
|
| 933 |
+
resource = self.backend
|
| 934 |
+
for name in ('engine', 'llm_engine', 'engine_core', 'resources'):
|
| 935 |
+
resource = getattr(resource, name, None)
|
| 936 |
+
if getattr(resource, 'engine_dead', False) is True:
|
| 937 |
+
self._mark_dead()
|
| 938 |
+
return self.dead
|
| 939 |
+
|
| 940 |
+
def _raise_if_service_failure(self, exc):
|
| 941 |
+
if unrecoverable_engine_error(exc) or self.is_dead():
|
| 942 |
+
self._mark_dead()
|
| 943 |
+
raise ServiceFailure('unrecoverable engine failure') from exc
|
| 944 |
+
|
| 945 |
+
def _item_failed(self):
|
| 946 |
+
with self._failure_lock:
|
| 947 |
+
self.item_failure_streak += 1
|
| 948 |
+
if self.item_failure_streak >= ITEM_FAILURE_LIMIT:
|
| 949 |
+
self.dead = True
|
| 950 |
+
return self.item_failure_streak
|
| 951 |
+
|
| 952 |
+
def _item_succeeded(self):
|
| 953 |
+
with self._failure_lock:
|
| 954 |
+
self.item_failure_streak = 0
|
| 955 |
+
|
| 956 |
+
def health(self):
|
| 957 |
+
dead = self.is_dead()
|
| 958 |
+
warnings = list(self.warnings)
|
| 959 |
+
if dead:
|
| 960 |
+
warnings.append('service failure')
|
| 961 |
+
reference=getattr(self.backend,'generation_reference',None)
|
| 962 |
+
if reference and not reference['verified']:warnings.append('generation reference '+reference['status'])
|
| 963 |
+
if hasattr(self.backend,'forward_counters') and not getattr(self.backend,'stats_available',False):warnings.append('HF forward hook unavailable; usage estimated')
|
| 964 |
+
if getattr(self.backend,'forward_counters',{}).get('uncounted_forwards',0):warnings.append('HF uncounted forwards; usage estimated')
|
| 965 |
+
if getattr(self.counter, 'memory_only', False):
|
| 966 |
+
warnings.append('quota persistence unavailable: memory counts, fast only')
|
| 967 |
+
return {'status': 'error' if dead else 'warning' if warnings else 'ok', 'warnings': warnings,
|
| 968 |
+
'info': {'intended_overrides': (reference or {}).get('intended_overrides',{})},
|
| 969 |
+
'model_key': self.config['model']['model_key'], 'mode':self.config['routing']['mode'],
|
| 970 |
+
'quota_tier':self.config['routing'].get('tier','normal'), 'budget_tiers':self.config['routing'].get('tiers',{}),
|
| 971 |
+
'threshold_identity':threshold_identity(self.config),
|
| 972 |
+
'cost_fuse':self.counter.cost_fuse.snapshot(self.config['routing']['quota']),
|
| 973 |
+
'accuracy_only':self.config.get('bench',{}).get('accuracy_only',False),
|
| 974 |
+
'generation_reference':({k:v for k,v in reference.items() if k!='intended_overrides'} if reference else reference),'quantization_kernel':getattr(self.backend,'quantization_kernel',None),
|
| 975 |
+
'prefix_caching':getattr(self.backend,'prefix_caching',None),
|
| 976 |
+
'supports_slow_session':callable(getattr(self.backend,'slow_session',None)) or callable(getattr(self.backend,'open_session',None))}
|
| 977 |
+
|
| 978 |
+
def engine_stats(self):
|
| 979 |
+
with self.model_lock:
|
| 980 |
+
return self.backend.engine_stats()
|
| 981 |
+
|
| 982 |
+
def warmup(self):
|
| 983 |
+
ids = self.backend.prompt_ids(self.config['prompt']['system'], 'Ready?', False)
|
| 984 |
+
self.backend.next_logits(ids, [self.backend.encode('yes')[0]])
|
| 985 |
+
self.backend.generate(ids, 1)
|
| 986 |
+
meter=new_meter()
|
| 987 |
+
with self.model_lock:
|
| 988 |
+
if callable(getattr(self.backend,'slow_session',None)):
|
| 989 |
+
session,_=self.backend.slow_session(ids,'force_answer',meter,
|
| 990 |
+
max_new_tokens=self.config['server']['warmup_slow_tokens'])
|
| 991 |
+
if session is not None:
|
| 992 |
+
branch=session.fork();branch.advance(self.backend.encode('yes'))
|
| 993 |
+
if hasattr(branch,'log_probs'): branch.log_probs([self.backend.encode('yes')[0]])
|
| 994 |
+
else: _=branch.logits
|
| 995 |
+
del branch,session
|
| 996 |
+
elif callable(getattr(self.backend,'open_session',None)):
|
| 997 |
+
import copy
|
| 998 |
+
warm=copy.deepcopy(self.config)
|
| 999 |
+
warm['generation']['max_new_tokens']=self.config['server']['warmup_slow_tokens']
|
| 1000 |
+
warm['routing']['unparsed_policy']='force_answer'
|
| 1001 |
+
slow_attempt(self.backend,ids,{'yes':['yes',' yes']},warm,meter)
|
| 1002 |
+
|
| 1003 |
+
def _one(self, task):
|
| 1004 |
+
with self.counter.accounting():
|
| 1005 |
+
if self.is_dead():
|
| 1006 |
+
raise ServiceFailure('service failure')
|
| 1007 |
+
try:
|
| 1008 |
+
answer,detail=self._one_impl(task)
|
| 1009 |
+
if self.is_dead():
|
| 1010 |
+
raise ServiceFailure('service failure')
|
| 1011 |
+
detail['threshold_identity']=threshold_identity(self.config)
|
| 1012 |
+
detail['cost_fuse']=self.counter.record_cost(detail['usage_bases'][detail['accounting']],detail['path']=='slow')
|
| 1013 |
+
try:
|
| 1014 |
+
json.dumps([answer,detail],allow_nan=False)
|
| 1015 |
+
except ValueError as exc:
|
| 1016 |
+
raise ItemFailure('non-finite value in response') from exc
|
| 1017 |
+
except ItemFailure as exc:
|
| 1018 |
+
if self._item_failed() >= ITEM_FAILURE_LIMIT:
|
| 1019 |
+
raise ServiceFailure('consecutive item failures') from exc
|
| 1020 |
+
raise
|
| 1021 |
+
except ServiceFailure:
|
| 1022 |
+
self._mark_dead()
|
| 1023 |
+
raise
|
| 1024 |
+
self._item_succeeded()
|
| 1025 |
+
return answer,detail
|
| 1026 |
+
|
| 1027 |
+
def _one_impl(self, task):
|
| 1028 |
+
c, backend = self.config, self.backend
|
| 1029 |
+
uncounted_before=getattr(backend,'forward_counters',{}).get('uncounted_forwards',0)
|
| 1030 |
+
labels, _, groups = render(task)
|
| 1031 |
+
qtype = task['question']['type']
|
| 1032 |
+
detail = {'path':'fast', 'route_reason':'error', 'router_score':0.0,
|
| 1033 |
+
'prompt_tokens':0, 'completion_tokens':0, 'n_new_tokens':0,
|
| 1034 |
+
'prompt_tokens_details':{'cached_tokens':0,'engine_submitted_tokens':0},
|
| 1035 |
+
'fast_scores':None,'slow_scores':None,'budget_tier':'normal',
|
| 1036 |
+
'readout_output_tokens':0, 'forced_answer':False, 'draft_prediction':None}
|
| 1037 |
+
warning = None
|
| 1038 |
+
meters = []
|
| 1039 |
+
try:
|
| 1040 |
+
kept_task, (_, groups, fast_ids, slow_ids), length = prepare_task(task, backend, c)
|
| 1041 |
+
detail.update(length)
|
| 1042 |
+
long_item=length.get('long_input',False) or c['routing'].get('tier')=='long' or length.get('slow_prompt_exceeds_budget',False) or any(length[k] for k in ('truncated','character_precompressed','protected_content_exceeds_budget','physical_truncated'))
|
| 1043 |
+
hard=long_item if c['routing'].get('long_item_slow_policy','fast_only')=='fast_only' else (
|
| 1044 |
+
length['character_precompressed'] or (length['original_prompt_tokens'] or 0)>c['routing']['slow_max_prompt_tokens'] or length['protected_content_exceeds_budget'])
|
| 1045 |
+
hard=hard or c['routing'].get('tier')=='long' or length.get('slow_prompt_exceeds_budget',False)
|
| 1046 |
+
detail['long_item']=long_item
|
| 1047 |
+
r = c['routing']
|
| 1048 |
+
fast_scores = None
|
| 1049 |
+
fast_meter=new_meter()
|
| 1050 |
+
meters.append(fast_meter)
|
| 1051 |
+
try:
|
| 1052 |
+
with self.model_lock:
|
| 1053 |
+
fast_scores, out, retry_length = fast_attempt_with_retry(backend,task,c,fast_ids,labels,groups,fast_meter)
|
| 1054 |
+
except Exception as exc:
|
| 1055 |
+
self._raise_if_service_failure(exc)
|
| 1056 |
+
raise ItemFailure('item could not be processed') from exc
|
| 1057 |
+
if retry_length is not None:
|
| 1058 |
+
detail.update(retry_length)
|
| 1059 |
+
detail['fast_oom_retry']=True
|
| 1060 |
+
hard=True
|
| 1061 |
+
detail['prompt_tokens']+=fast_meter['prompt']
|
| 1062 |
+
detail['completion_tokens']+=fast_meter['output']
|
| 1063 |
+
detail['readout_output_tokens']+=fast_meter['probes']
|
| 1064 |
+
detail['prompt_tokens_details'].update(cached_tokens=fast_meter['cached'],engine_submitted_tokens=fast_meter['engine_submitted'])
|
| 1065 |
+
if fast_meter.get('engine_usage_unknown'): detail['usage_estimated']=True
|
| 1066 |
+
if fast_meter.get('cached_unknown'): detail['prompt_tokens_details']['cached_tokens_known']=False
|
| 1067 |
+
detail['fast_prompt_tokens']=fast_meter['prompt']
|
| 1068 |
+
detail['raw_label_scores']=detail['fast_scores']=fast_scores
|
| 1069 |
+
margin = label_margin(labels, fast_scores, r['margin_kind']) if fast_scores is not None else None
|
| 1070 |
+
detail.update(fast_margin=margin if margin is None or math.isfinite(margin) else None,
|
| 1071 |
+
fast_margin_infinite=margin == math.inf, margin_kind=r['margin_kind'])
|
| 1072 |
+
wants_slow = fast_scores is not None and not hard and (
|
| 1073 |
+
r['mode'] == 'uniform' or (r['mode'] == 'routed' and margin < r['margin_threshold']))
|
| 1074 |
+
wants_slow = wants_slow and (callable(getattr(backend,'slow_session',None)) or callable(getattr(backend,'open_session',None)))
|
| 1075 |
+
detail['budget_tier']='long' if hard else 'hard' if wants_slow else 'normal'
|
| 1076 |
+
detail['admission_quota'],detail['admission_quota_source']=self.counter.cost_fuse.quota(r['quota'])
|
| 1077 |
+
with self.counter.decision(wants_slow, quota=detail['admission_quota']) as decision:
|
| 1078 |
+
detail.update(path=decision.path, route_reason='oom_retry' if detail.get('fast_oom_retry') else 'length' if hard else decision.reason,
|
| 1079 |
+
quota_at_reservation=decision.snapshot)
|
| 1080 |
+
scores, block = fast_scores, 'fast'
|
| 1081 |
+
if decision.path == 'slow':
|
| 1082 |
+
meter = new_meter()
|
| 1083 |
+
meters.append(meter)
|
| 1084 |
+
try:
|
| 1085 |
+
scores, count, forced, whitespace, probes = bounded_slow_attempt(backend,slow_ids,groups,c,meter,self.model_lock,on_service_failure=self._mark_dead)
|
| 1086 |
+
detail['slow_raw_label_scores']=detail['slow_scores']=scores
|
| 1087 |
+
block = 'slow' if scores is not None else 'fallback'
|
| 1088 |
+
detail['route'] = ('slow_forced_answer' if forced else 'slow') if scores is not None else 'slow_unparsed_fast'
|
| 1089 |
+
if scores is None:
|
| 1090 |
+
scores = fast_scores
|
| 1091 |
+
detail.update(n_new_tokens=count, forced_answer=forced, whitespace_step=whitespace)
|
| 1092 |
+
except Exception as exc:
|
| 1093 |
+
self._raise_if_service_failure(exc)
|
| 1094 |
+
scores, block = fast_scores, 'fallback'
|
| 1095 |
+
warning = 'slow_failed: fast answer kept'
|
| 1096 |
+
detail.update(fallback='slow_to_fast', route='slow_timeout_fast' if isinstance(exc,TimeoutError) else 'slow_failed_fast')
|
| 1097 |
+
if isinstance(exc,TimeoutError):
|
| 1098 |
+
warning='slow_timeout: fast answer kept'
|
| 1099 |
+
detail['usage_estimated']=True
|
| 1100 |
+
# Detach response accounting from the draining worker.
|
| 1101 |
+
meter=dict(meter);meters[-1]=meter
|
| 1102 |
+
finally:
|
| 1103 |
+
if meter.get('engine_usage_unknown'): detail['usage_estimated']=True
|
| 1104 |
+
if meter.get('cached_unknown'): detail['prompt_tokens_details']['cached_tokens_known']=False
|
| 1105 |
+
detail['prompt_tokens'] += meter['prompt']
|
| 1106 |
+
detail['completion_tokens'] += meter['output']
|
| 1107 |
+
detail['n_new_tokens'] = meter['draft'] + meter['forced']
|
| 1108 |
+
detail['readout_output_tokens'] += meter['probes']
|
| 1109 |
+
detail['prompt_tokens_details']['cached_tokens'] += meter['cached']
|
| 1110 |
+
detail['prompt_tokens_details']['engine_submitted_tokens'] += meter['engine_submitted']
|
| 1111 |
+
detail['calibration_block']=block
|
| 1112 |
+
if scores is None:
|
| 1113 |
+
raise ItemFailure('no label scores')
|
| 1114 |
+
probs = probabilities(labels, scores, qtype, self.calibration.get(block, self.calibration['fast']))
|
| 1115 |
+
detail['quota_after_completion'] = decision.snapshot
|
| 1116 |
+
except (ItemFailure, ServiceFailure, QuotaStateError):
|
| 1117 |
+
raise
|
| 1118 |
+
except Exception as exc:
|
| 1119 |
+
# No substitute answer: an engine failure is service-level (HTTP 503); any other
|
| 1120 |
+
# failure fails this item (HTTP 422), which the harness scores as wrong.
|
| 1121 |
+
self._raise_if_service_failure(exc)
|
| 1122 |
+
raise ItemFailure('item could not be processed') from exc
|
| 1123 |
+
if (getattr(backend,'forward_counters',{}).get('uncounted_forwards',0)>uncounted_before
|
| 1124 |
+
or hasattr(backend,'forward_counters') and not getattr(backend,'stats_available',False)):
|
| 1125 |
+
detail['usage_estimated']=True
|
| 1126 |
+
warning='HF count unavailable: usage estimated'
|
| 1127 |
+
detail['backend_warnings']=[w for m in meters for w in m.get('warnings',[])]
|
| 1128 |
+
if any(m.get('cached_unknown') for m in meters):detail['usage_estimated']=True
|
| 1129 |
+
submitted=sum(m['engine_submitted'] for m in meters)
|
| 1130 |
+
cached=sum(m['cached'] for m in meters)
|
| 1131 |
+
generated=sum(m.get('engine_generated',m['output']) for m in meters)
|
| 1132 |
+
engine_input=sum(m.get('engine_input',m['engine_submitted']-m['cached']) for m in meters)
|
| 1133 |
+
if detail.get('usage_estimated'):
|
| 1134 |
+
engine_input=max(engine_input,submitted)+c['generation']['max_new_tokens']
|
| 1135 |
+
generated+=c['generation']['max_new_tokens']
|
| 1136 |
+
detail['input_tokens_details']=dict(cached_tokens=cached,engine_submitted_tokens=submitted,
|
| 1137 |
+
engine_calls=sum(m.get('engine_calls',0) for m in meters),engine_generated_tokens=generated,
|
| 1138 |
+
cached_tokens_known=not any(m.get('cached_unknown') for m in meters),
|
| 1139 |
+
generate_nonempty_calls=sum(m.get('generate_nonempty_calls',0) for m in meters),
|
| 1140 |
+
hf_prefill_tokens=sum(m.get('hf_prefill_tokens',0) for m in meters))
|
| 1141 |
+
detail['usage_bases']={
|
| 1142 |
+
'engine':dict(input_tokens=engine_input,output_tokens=generated),
|
| 1143 |
+
'each_once':dict(input_tokens=detail['prompt_tokens'],output_tokens=detail['completion_tokens']),
|
| 1144 |
+
'submitted':dict(input_tokens=submitted,output_tokens=detail['completion_tokens'])}
|
| 1145 |
+
if detail.get('usage_estimated'):
|
| 1146 |
+
for values in detail['usage_bases'].values():
|
| 1147 |
+
values['input_tokens']=max(values['input_tokens'],engine_input)
|
| 1148 |
+
values['output_tokens']=max(values['output_tokens'],generated)
|
| 1149 |
+
detail['accounting']=c.get('usage',{}).get('basis','engine')
|
| 1150 |
+
detail['kept_prompt_tokens_max'] = detail.get('kept_prompt_tokens', 0)
|
| 1151 |
+
detail['total_tokens'] = detail['prompt_tokens'] + detail['completion_tokens']
|
| 1152 |
+
detail['prediction'] = max(labels, key=probs.get)
|
| 1153 |
+
request_type = task['question']['request_type']
|
| 1154 |
+
if qtype == 'noul':
|
| 1155 |
+
style = c.get('interface', {}).get('boolean_keys', 'request')
|
| 1156 |
+
if style == 'true_false' or (style == 'request' and request_type == 'boolean'):
|
| 1157 |
+
probs = {'false':probs['no'], 'true':probs['yes']}
|
| 1158 |
+
answer = {'type':request_type, 'probabilities':probs}
|
| 1159 |
+
if qtype == 'noul': answer['noul'] = float(probs.get('yes', probs.get('true')))
|
| 1160 |
+
elif qtype == 'choice': answer['choice'] = detail['prediction']
|
| 1161 |
+
if warning:
|
| 1162 |
+
answer['warning'] = warning
|
| 1163 |
+
return answer, detail
|
| 1164 |
+
|
| 1165 |
+
def answer(self, payload):
|
| 1166 |
+
if self.is_dead():
|
| 1167 |
+
raise ServiceFailure('service failure')
|
| 1168 |
+
tasks = normalize_request(payload, self.config['server']['max_questions'])
|
| 1169 |
+
answers, details, errors, noul, choice = {}, {}, {}, {}, {}
|
| 1170 |
+
for name, task in tasks:
|
| 1171 |
+
answer, detail = self._one(task)
|
| 1172 |
+
answers[name], details[name] = answer, detail
|
| 1173 |
+
if 'warning' in answer:
|
| 1174 |
+
errors[name] = answer['warning']
|
| 1175 |
+
if task['question']['type'] == 'noul':
|
| 1176 |
+
noul[name] = answer['probabilities'].get('true', answer['probabilities'].get('yes'))
|
| 1177 |
+
else:
|
| 1178 |
+
choice[name] = detail['prediction']
|
| 1179 |
+
basis=self.config.get('usage',{}).get('basis','engine')
|
| 1180 |
+
alternatives={b:{k:sum(d['usage_bases'][b][k] for d in details.values())
|
| 1181 |
+
for k in ('input_tokens','output_tokens')} for b in ('engine','each_once','submitted')}
|
| 1182 |
+
p,o=alternatives[basis]['input_tokens'],alternatives[basis]['output_tokens']
|
| 1183 |
+
token_details={k:sum(d['input_tokens_details'][k] for d in details.values()) for k in
|
| 1184 |
+
('cached_tokens','engine_submitted_tokens','engine_calls','engine_generated_tokens','generate_nonempty_calls','hf_prefill_tokens')}
|
| 1185 |
+
token_details['cached_tokens_known']=all(d['input_tokens_details']['cached_tokens_known'] for d in details.values())
|
| 1186 |
+
result={'answers':answers,'model':self.config['model']['name'],
|
| 1187 |
+
'usage':dict(input_tokens=p,output_tokens=o,prompt_tokens=p,completion_tokens=o,total_tokens=p+o,
|
| 1188 |
+
cost_fuse=next(reversed(details.values()))['cost_fuse'],
|
| 1189 |
+
accounting=basis,usage_estimated=any(d.get('usage_estimated',False) for d in details.values()),
|
| 1190 |
+
alternatives=alternatives,questions=details,input_tokens_details=token_details,
|
| 1191 |
+
prompt_tokens_details=token_details,token_accounting=basis)}
|
| 1192 |
+
for key, values in (('noul',noul),('choice',choice)):
|
| 1193 |
+
if values:
|
| 1194 |
+
result[key] = next(iter(values.values())) if len(tasks) == 1 else values
|
| 1195 |
+
if errors:
|
| 1196 |
+
result['partial_errors'] = errors
|
| 1197 |
+
return result
|
service/fit_report.json
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_files": {
|
| 3 |
+
"adapter_config.json": {
|
| 4 |
+
"sha256": "140bb00c832d117f57d29650dabe50e992b2e5064e908aaa29230b3ce19ca2c4",
|
| 5 |
+
"size": 22953
|
| 6 |
+
},
|
| 7 |
+
"adapter_model.safetensors": {
|
| 8 |
+
"sha256": "2b9568998ffed5d133223ffdff03056be297285c643122db4d45365d0bd86970",
|
| 9 |
+
"size": 494595208
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"arm": "trained",
|
| 13 |
+
"complete": true,
|
| 14 |
+
"config_sha256": "5cebe511ef103d5104539d3ffa34435274c6a3bc08dbf40d5f80f9ec0ce3455a",
|
| 15 |
+
"draft": 256,
|
| 16 |
+
"elapsed_seconds": 0.0,
|
| 17 |
+
"engine_lora": {
|
| 18 |
+
"lora_dtype": "torch.bfloat16",
|
| 19 |
+
"max_lora_rank": 16,
|
| 20 |
+
"max_loras": 1
|
| 21 |
+
},
|
| 22 |
+
"fast_rows_source": {
|
| 23 |
+
"fit_report_sha256": "252cb2baf2ecb21a7892cb34b9334c1bad5f59fe7b924dd7f868addf3f27081e",
|
| 24 |
+
"protocol": "adapter_arm_31b_v1",
|
| 25 |
+
"records_sha256": "5e118133ce686b2d12b8d8a58b6e68560221c106eec54438dcfe1110e7ecfe7e",
|
| 26 |
+
"role": "q0 public231 fast-only collection of this arm",
|
| 27 |
+
"service_lock_sha256": "50a4c56c5c9fbfbde9c127ad0247736fa16355bc8deae90b5311f3ba19a03d44"
|
| 28 |
+
},
|
| 29 |
+
"fit_protocol": "31b-adapter-arm-v3-routed",
|
| 30 |
+
"input_budget": 6653,
|
| 31 |
+
"isolation_sha256": "802bfec07914c4c10dbdd35fdfdced0ef88945b06717252038da35b0b0d9bf99",
|
| 32 |
+
"model_sha256": "ddb0a8178194de6d3a3ed6e5a00ea01d6d89c143d43d169a68ad9561636cf4ff",
|
| 33 |
+
"n": 231,
|
| 34 |
+
"noul_constant": {
|
| 35 |
+
"applied_to": [
|
| 36 |
+
"fast",
|
| 37 |
+
"slow",
|
| 38 |
+
"fallback"
|
| 39 |
+
],
|
| 40 |
+
"fitted_on": "the 74 yes/no items of the 231 public items, in-sample fast fit",
|
| 41 |
+
"fitted_values": {
|
| 42 |
+
"fallback": 0.9459459459459459,
|
| 43 |
+
"fast": 0.9459459459459459,
|
| 44 |
+
"slow": 0.9459459459459459
|
| 45 |
+
},
|
| 46 |
+
"outputs": [
|
| 47 |
+
0.898,
|
| 48 |
+
0.102
|
| 49 |
+
],
|
| 50 |
+
"params_sha256": "b28ed73bb5a36e99a280a47a6b846e0c5b2de54f34a96efa4a10754b71ea7573",
|
| 51 |
+
"provenance": {
|
| 52 |
+
"development": {
|
| 53 |
+
"accuracy": 0.897815362931642,
|
| 54 |
+
"n": 1419,
|
| 55 |
+
"standard_error": 0.010166783957967924,
|
| 56 |
+
"standard_error_kind": "clustered by source group (663 groups)"
|
| 57 |
+
},
|
| 58 |
+
"result_sha256": "6cd7105b25050ece8ab7e99cd437db38368f6169bb452908785e413a3cdbe26a",
|
| 59 |
+
"rule": "Rule (v3, own development data only): the yes/no accuracy of our own held-out development data, rounded to 3 decimals. No leaderboard result of any kind was used for this constant.",
|
| 60 |
+
"rule_sha256": "be93d0ba00dd1a268f8722955111622ab71f0035ba66c60db86c4e88d35f0dbc"
|
| 61 |
+
},
|
| 62 |
+
"rule": "Rule (v3, own development data only): the yes/no accuracy of our own held-out development data, rounded to 3 decimals. No leaderboard result of any kind was used for this constant.",
|
| 63 |
+
"value": 0.898
|
| 64 |
+
},
|
| 65 |
+
"numeric_config": {
|
| 66 |
+
"NVIDIA_TF32_OVERRIDE": "0",
|
| 67 |
+
"VLLM_BATCH_INVARIANT": "1",
|
| 68 |
+
"cuda_matmul_allow_tf32": false,
|
| 69 |
+
"cudnn_allow_tf32": false,
|
| 70 |
+
"dtype": "bfloat16",
|
| 71 |
+
"engine_lora": {
|
| 72 |
+
"lora_dtype": "torch.bfloat16",
|
| 73 |
+
"max_lora_rank": 16,
|
| 74 |
+
"max_loras": 1
|
| 75 |
+
},
|
| 76 |
+
"postload_verified": {
|
| 77 |
+
"NVIDIA_TF32_OVERRIDE": "0",
|
| 78 |
+
"TORCH_ALLOW_TF32_CUBLAS_OVERRIDE": null,
|
| 79 |
+
"cuda_matmul_allow_tf32": false,
|
| 80 |
+
"cudnn_allow_tf32": false,
|
| 81 |
+
"float32_matmul_precision": "highest"
|
| 82 |
+
},
|
| 83 |
+
"process_environment": {
|
| 84 |
+
"DO_NOT_TRACK": "1",
|
| 85 |
+
"HF_DATASETS_OFFLINE": "1",
|
| 86 |
+
"HF_HUB_DISABLE_TELEMETRY": "1",
|
| 87 |
+
"HF_HUB_OFFLINE": "1",
|
| 88 |
+
"NVIDIA_TF32_OVERRIDE": "0",
|
| 89 |
+
"TRANSFORMERS_OFFLINE": "1",
|
| 90 |
+
"VLLM_BATCH_INVARIANT": "1",
|
| 91 |
+
"VLLM_NO_USAGE_STATS": "1",
|
| 92 |
+
"VLLM_USE_FLASHINFER_SAMPLER": "0",
|
| 93 |
+
"VLLM_WORKER_MULTIPROC_METHOD": "spawn"
|
| 94 |
+
},
|
| 95 |
+
"startup": {
|
| 96 |
+
"applied_environment": {
|
| 97 |
+
"DO_NOT_TRACK": "1",
|
| 98 |
+
"PYTHONUNBUFFERED": "1",
|
| 99 |
+
"VLLM_HOST_IP": "127.0.0.1",
|
| 100 |
+
"VLLM_NO_USAGE_STATS": "1",
|
| 101 |
+
"VLLM_USE_FLASHINFER_SAMPLER": "0"
|
| 102 |
+
},
|
| 103 |
+
"removed_attention_environment": {
|
| 104 |
+
"JEV_ATTENTION_BACKEND": null,
|
| 105 |
+
"VLLM_ATTENTION_BACKEND": null
|
| 106 |
+
},
|
| 107 |
+
"startup_environment_policy": {
|
| 108 |
+
"operator_cache_environment_preserved": true,
|
| 109 |
+
"removed_attention_variables": [
|
| 110 |
+
"VLLM_ATTENTION_BACKEND",
|
| 111 |
+
"JEV_ATTENTION_BACKEND"
|
| 112 |
+
],
|
| 113 |
+
"reused_function": "child_environment",
|
| 114 |
+
"source": "reference launch.py",
|
| 115 |
+
"source_sha256": "2136bcb3330a80ad3f7e73ee5c99633a41e26fa922f5c8aef6f7df696fb7ce11"
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
"torch_version": "2.13.0+cu130",
|
| 119 |
+
"versions": {
|
| 120 |
+
"compressed-tensors": "0.17.0",
|
| 121 |
+
"tokenizers": "0.23.2",
|
| 122 |
+
"torch": "2.13.0+cu130",
|
| 123 |
+
"transformers": "5.17.0",
|
| 124 |
+
"triton": "3.7.1",
|
| 125 |
+
"vllm": "0.31.0"
|
| 126 |
+
}
|
| 127 |
+
},
|
| 128 |
+
"phases": [
|
| 129 |
+
"q0 public231 fast-only rows of the arm (reused, hash-bound)",
|
| 130 |
+
"offline routed fit of the v3 slow setting",
|
| 131 |
+
"v3 yes/no constant in the fast, slow and fallback blocks"
|
| 132 |
+
],
|
| 133 |
+
"protocol": "adapter_arm_31b_v3",
|
| 134 |
+
"public_data_sha256": "d7ac378b817b9c32c2128a83bb8699f5770439503f7204c79572e2749319b5d4",
|
| 135 |
+
"public_fit_only": true,
|
| 136 |
+
"public_identity": {
|
| 137 |
+
"contains_public_benchmark": true,
|
| 138 |
+
"contains_sealed_benchmark": false,
|
| 139 |
+
"data_sha256": "8e7b2fec1275413649d2894670cbe3ea4a734c768a01ff833697123b2b44562c",
|
| 140 |
+
"n": 231,
|
| 141 |
+
"order_sha256": "3cac6d401ca18059628f0f48c8d7f974cbbea020f39f28109eaf84bd1a0f22e1",
|
| 142 |
+
"origin": "public231_crlf",
|
| 143 |
+
"public_sha256": {
|
| 144 |
+
"easy.jsonl": "314cd493a4c080fca5ad90e1c8a7c2b3f24137e66dc06f8fceaf51ea54f38467",
|
| 145 |
+
"hard.jsonl": "8c8f1efb5a04b7016f9c8cd8a70a485f58a70a918aac3e3c1107f41172ca91b6",
|
| 146 |
+
"original.jsonl": "b2abe9ac8cabd953a3bc452abd4b229da42ca6d0e647bf93f7638076f42db19f"
|
| 147 |
+
},
|
| 148 |
+
"public_source_verified": true,
|
| 149 |
+
"role": "service_fit_public",
|
| 150 |
+
"usage_scope": "service_fit_only"
|
| 151 |
+
},
|
| 152 |
+
"public_selection_score_exported": false,
|
| 153 |
+
"q": 0.056,
|
| 154 |
+
"records_sha256": "5e118133ce686b2d12b8d8a58b6e68560221c106eec54438dcfe1110e7ecfe7e",
|
| 155 |
+
"role": "service_fit_public",
|
| 156 |
+
"routing": {
|
| 157 |
+
"cost_fuse": {
|
| 158 |
+
"arm": 0.9,
|
| 159 |
+
"enabled": true,
|
| 160 |
+
"release": 0.85,
|
| 161 |
+
"warmup": 100
|
| 162 |
+
},
|
| 163 |
+
"excluded_types": [],
|
| 164 |
+
"mode": "routed",
|
| 165 |
+
"quota": 0.056,
|
| 166 |
+
"slow_draft": {
|
| 167 |
+
"max_new_tokens": 256,
|
| 168 |
+
"reasoning_words": 160
|
| 169 |
+
},
|
| 170 |
+
"slow_max_prompt_tokens": 840,
|
| 171 |
+
"slow_share_fuse": 0.2,
|
| 172 |
+
"target_slow_share": 0.056,
|
| 173 |
+
"threshold_rule": "bench.calibrate_margin(public231 rows that can draft, ceil(q x 231) of them)",
|
| 174 |
+
"threshold_rule_name": "eligible_items",
|
| 175 |
+
"unparsed_policy": "force_answer"
|
| 176 |
+
},
|
| 177 |
+
"selection_sha256": "621fde9159b24e773298511418d3373dcd2da45bed535327c5be01facf218994",
|
| 178 |
+
"serving_identity_sha256": "0513d121db967ec1a6aaccf627d3c42149d092dd027298da5b53684b030821ea",
|
| 179 |
+
"slow_calibration": {
|
| 180 |
+
"by_type": {
|
| 181 |
+
"choice": {
|
| 182 |
+
"fast_value": 1.2968395546510096,
|
| 183 |
+
"inherited": true,
|
| 184 |
+
"slow_samples": 0,
|
| 185 |
+
"value": 1.2968395546510096
|
| 186 |
+
},
|
| 187 |
+
"noul": {
|
| 188 |
+
"constant": "service parameter",
|
| 189 |
+
"fast_value": 0.898,
|
| 190 |
+
"fitted_fast_value": 0.9459459459459459,
|
| 191 |
+
"inherited": true,
|
| 192 |
+
"slow_samples": 0,
|
| 193 |
+
"value": 0.898
|
| 194 |
+
},
|
| 195 |
+
"score": {
|
| 196 |
+
"fast_value": 1.189207115002721,
|
| 197 |
+
"inherited": true,
|
| 198 |
+
"slow_samples": 0,
|
| 199 |
+
"value": 1.189207115002721
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
"routed_collection": {
|
| 203 |
+
"calibration_routed_quota": 0.1,
|
| 204 |
+
"collected": false,
|
| 205 |
+
"max_possible_slow_samples": 23,
|
| 206 |
+
"reason": "at the calibration-routed quota 0.10 the quota counter admits at most 23 of 231 public decisions to the slow path, fewer than 30 for any type, so every slow type inherits the fast parameter whatever such a collection shows; it is therefore not run"
|
| 207 |
+
},
|
| 208 |
+
"rule": "slow samples of a type < 30: the fast parameter of that type (temperature or yes/no confidence) is copied unchanged"
|
| 209 |
+
},
|
| 210 |
+
"source_code_sha256": "51269609aa077981a1a5e0bc5775a3d3aad5c1df04f6329b4c199fa2c5477d05",
|
| 211 |
+
"threshold": {
|
| 212 |
+
"achieved": 0.05627705627705628,
|
| 213 |
+
"eligible_below": 13,
|
| 214 |
+
"eligible_n": 181,
|
| 215 |
+
"n": 231,
|
| 216 |
+
"rule": "eligible_items",
|
| 217 |
+
"target": 0.056,
|
| 218 |
+
"threshold": 2.250016468674351
|
| 219 |
+
},
|
| 220 |
+
"v3_params": {
|
| 221 |
+
"cost_fuse_arm": 0.9,
|
| 222 |
+
"cost_fuse_release": 0.85,
|
| 223 |
+
"draft_max_new_tokens": 256,
|
| 224 |
+
"draft_reasoning_words": 160,
|
| 225 |
+
"exclude_noul": false,
|
| 226 |
+
"noul_p_cal": 0.898,
|
| 227 |
+
"noul_p_cal_provenance": {
|
| 228 |
+
"development": {
|
| 229 |
+
"accuracy": 0.897815362931642,
|
| 230 |
+
"n": 1419,
|
| 231 |
+
"standard_error": 0.010166783957967924,
|
| 232 |
+
"standard_error_kind": "clustered by source group (663 groups)"
|
| 233 |
+
},
|
| 234 |
+
"result_sha256": "6cd7105b25050ece8ab7e99cd437db38368f6169bb452908785e413a3cdbe26a",
|
| 235 |
+
"rule": "Rule (v3, own development data only): the yes/no accuracy of our own held-out development data, rounded to 3 decimals. No leaderboard result of any kind was used for this constant.",
|
| 236 |
+
"rule_sha256": "be93d0ba00dd1a268f8722955111622ab71f0035ba66c60db86c4e88d35f0dbc"
|
| 237 |
+
},
|
| 238 |
+
"schema": "adapter_arm_31b_v3_params_v1",
|
| 239 |
+
"slow_max_prompt_tokens": 840,
|
| 240 |
+
"slow_quota": 0.056,
|
| 241 |
+
"threshold_rule": "eligible_items",
|
| 242 |
+
"unparsed_policy": "force_answer"
|
| 243 |
+
},
|
| 244 |
+
"v3_params_sha256": "b28ed73bb5a36e99a280a47a6b846e0c5b2de54f34a96efa4a10754b71ea7573",
|
| 245 |
+
"variant": "key_value_aligned"
|
| 246 |
+
}
|
service/item_deadline.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-decision deadline for the drafting pass, plus two item rules (installed by server.py when
|
| 2 |
+
config.json has an `item_deadline` block, with the `item_rules` block; no package file is modified at
|
| 3 |
+
runtime).
|
| 4 |
+
|
| 5 |
+
One hard deadline per request (the harness sends one decision per request): the moment the service
|
| 6 |
+
starts answering the request + item_seconds. The draft is generated greedily in chunks of at most
|
| 7 |
+
`chunk_tokens`; each chunk is sized by a conservative speed model (a short synthetic warm-up at
|
| 8 |
+
startup, updated online) so that it ends before deadline - response reserve - readout reserve.
|
| 9 |
+
No engine call is abandoned in flight, so every call is metered exactly, and the model lock is
|
| 10 |
+
released when the decision returns.
|
| 11 |
+
- Draft budget used up without the answer marker: the marker is appended, exactly as when
|
| 12 |
+
max_new_tokens is reached.
|
| 13 |
+
- Readout that cannot finish before the deadline: the first-pass answer is returned (route
|
| 14 |
+
slow_timeout_fast) and the decision's usage is reconciled exactly from its meters.
|
| 15 |
+
- An answer marker that straddles a chunk boundary ends the draft at the token that completes it,
|
| 16 |
+
where one-shot generation would have stopped.
|
| 17 |
+
Decisions that do not take the drafting pass are unaffected by the deadline.
|
| 18 |
+
|
| 19 |
+
Item rules (wrap_service):
|
| 20 |
+
- slow_types: question types that may take the drafting pass (None = all). For any other type the
|
| 21 |
+
routing threshold is 0 for that decision: it never requests the drafting pass and the quota
|
| 22 |
+
counter never counts it.
|
| 23 |
+
- reread_tau: a choice decision answered in one pass although its length allowed the drafting pass
|
| 24 |
+
(budget tier not 'long', not a long item), whose first-pass margin is below tau, is read once
|
| 25 |
+
more, with the options in reverse order (each label keeps its criterion); the two answer
|
| 26 |
+
distributions are averaged by label. The second read is metered into the
|
| 27 |
+
decision's usage (so the cost fuse sees it) and is not a quota decision. If it fails (not an
|
| 28 |
+
engine failure) the first-pass answer is kept and `reread_failed` is recorded.
|
| 29 |
+
Controller.truncated_readout is a diagnostic used only by offline measurement; serving never calls it.
|
| 30 |
+
"""
|
| 31 |
+
import math
|
| 32 |
+
import re
|
| 33 |
+
import threading
|
| 34 |
+
import time
|
| 35 |
+
from contextlib import contextmanager
|
| 36 |
+
|
| 37 |
+
DEFAULTS = dict(item_seconds=9.0, response_reserve_s=0.35, readout_reserve_init_s=1.0,
|
| 38 |
+
readout_reserve_min_s=0.3, readout_safety=1.5, chunk_tokens=32, min_chunk_tokens=8,
|
| 39 |
+
token_safety=1.25, ewma_alpha=0.3, warmup_tokens=33, warmup_repeats=3)
|
| 40 |
+
|
| 41 |
+
_local = threading.local()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def need(ok, code):
|
| 45 |
+
if not ok:
|
| 46 |
+
raise RuntimeError(code)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def settings_from(values=None):
|
| 50 |
+
s = dict(DEFAULTS)
|
| 51 |
+
s.update(values or {})
|
| 52 |
+
need(set(s) == set(DEFAULTS), 'DEADLINE_SETTINGS_KEYS')
|
| 53 |
+
for key in ('item_seconds', 'response_reserve_s', 'readout_reserve_init_s', 'readout_reserve_min_s',
|
| 54 |
+
'readout_safety', 'token_safety', 'ewma_alpha'):
|
| 55 |
+
x = s[key]
|
| 56 |
+
need(isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x) and x > 0,
|
| 57 |
+
'DEADLINE_SETTING_' + key.upper())
|
| 58 |
+
for key in ('chunk_tokens', 'min_chunk_tokens', 'warmup_tokens', 'warmup_repeats'):
|
| 59 |
+
need(type(s[key]) is int and s[key] > 0, 'DEADLINE_SETTING_' + key.upper())
|
| 60 |
+
need(s['token_safety'] >= 1 and s['readout_safety'] >= 1 and s['ewma_alpha'] <= 1, 'DEADLINE_SAFETY')
|
| 61 |
+
need(s['min_chunk_tokens'] <= s['chunk_tokens'] and s['warmup_tokens'] >= 2 and s['warmup_repeats'] >= 2,
|
| 62 |
+
'DEADLINE_TOKEN_LIMITS')
|
| 63 |
+
need(s['item_seconds'] > s['response_reserve_s'] + s['readout_reserve_init_s'], 'DEADLINE_RESERVES')
|
| 64 |
+
return s
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class SpeedModel:
|
| 68 |
+
"""Conservative chunk planner: seconds(n) = call_s + n * token_s * token_safety."""
|
| 69 |
+
|
| 70 |
+
def __init__(self, token_s, call_s, settings):
|
| 71 |
+
need(all(isinstance(x, float) and math.isfinite(x) and x > 0 for x in (token_s, call_s)),
|
| 72 |
+
'DEADLINE_SPEED')
|
| 73 |
+
self.s = settings
|
| 74 |
+
self.lock = threading.Lock()
|
| 75 |
+
self.warm_token_s = token_s
|
| 76 |
+
self.token_s = token_s
|
| 77 |
+
self.call_s = call_s
|
| 78 |
+
self.ewma = None
|
| 79 |
+
self.readout = {}
|
| 80 |
+
self.chunks_observed = 0
|
| 81 |
+
|
| 82 |
+
def fit_tokens(self, seconds):
|
| 83 |
+
if not math.isfinite(seconds) or seconds <= self.call_s:
|
| 84 |
+
return 0
|
| 85 |
+
return int(math.floor((seconds - self.call_s) / (self.token_s * self.s['token_safety'])))
|
| 86 |
+
|
| 87 |
+
def observe_chunk(self, generated, seconds):
|
| 88 |
+
if generated < 4 or not math.isfinite(seconds) or seconds <= 0:
|
| 89 |
+
return
|
| 90 |
+
per = max(0., seconds - self.call_s) / generated
|
| 91 |
+
with self.lock:
|
| 92 |
+
a = self.s['ewma_alpha']
|
| 93 |
+
self.ewma = per if self.ewma is None else a * per + (1 - a) * self.ewma
|
| 94 |
+
self.token_s = max(self.warm_token_s, self.ewma)
|
| 95 |
+
self.chunks_observed += 1
|
| 96 |
+
|
| 97 |
+
def readout_reserve(self, kind):
|
| 98 |
+
v = self.readout.get(kind)
|
| 99 |
+
if v is None:
|
| 100 |
+
return self.s['readout_reserve_init_s']
|
| 101 |
+
return max(self.s['readout_reserve_min_s'], self.s['readout_safety'] * v)
|
| 102 |
+
|
| 103 |
+
def observe_readout(self, kind, seconds):
|
| 104 |
+
if not math.isfinite(seconds) or seconds < 0:
|
| 105 |
+
return
|
| 106 |
+
with self.lock:
|
| 107 |
+
a = self.s['ewma_alpha']
|
| 108 |
+
old = self.readout.get(kind)
|
| 109 |
+
self.readout[kind] = seconds if old is None else a * seconds + (1 - a) * old
|
| 110 |
+
|
| 111 |
+
def snapshot(self):
|
| 112 |
+
return dict(token_s=self.token_s, warm_token_s=self.warm_token_s, call_s=self.call_s,
|
| 113 |
+
chunks_observed=self.chunks_observed, readout={k: v for k, v in sorted(self.readout.items())})
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class ItemPlan:
|
| 117 |
+
def __init__(self, controller, start, max_new_tokens, kind):
|
| 118 |
+
s = controller.settings
|
| 119 |
+
self.c = controller
|
| 120 |
+
self.start = start
|
| 121 |
+
self.kind = kind
|
| 122 |
+
self.max_new_tokens = max_new_tokens
|
| 123 |
+
self.hard = start + s['item_seconds']
|
| 124 |
+
self.meter_deadline = self.hard - s['response_reserve_s']
|
| 125 |
+
self.meters = []
|
| 126 |
+
self.chunks = []
|
| 127 |
+
self.budget_stop = False
|
| 128 |
+
self.straddle = False
|
| 129 |
+
self.forced = False
|
| 130 |
+
self.draft_tokens = 0
|
| 131 |
+
self.slow_started = None
|
| 132 |
+
self.draft_end_at = None
|
| 133 |
+
self.readout_start = None
|
| 134 |
+
self.readout_s = None
|
| 135 |
+
self.timeout_phase = None
|
| 136 |
+
self.reconciled = False
|
| 137 |
+
# draft bookkeeping (memory only; never serialised)
|
| 138 |
+
self._prompt_ids = None
|
| 139 |
+
self._draft_ids = None
|
| 140 |
+
self._by_marker = False
|
| 141 |
+
self._groups = None
|
| 142 |
+
self._config = None
|
| 143 |
+
self._slow_meter = None
|
| 144 |
+
|
| 145 |
+
def draft_deadline(self):
|
| 146 |
+
return self.meter_deadline - self.c.speed.readout_reserve(self.kind)
|
| 147 |
+
|
| 148 |
+
def exact(self):
|
| 149 |
+
return all(not m.get('engine_usage_unknown') and not m.get('cached_unknown') for m in self.meters)
|
| 150 |
+
|
| 151 |
+
def audit(self, now):
|
| 152 |
+
def rel(t):
|
| 153 |
+
return None if t is None else round(t - self.start, 6)
|
| 154 |
+
speed = self.c.speed
|
| 155 |
+
return dict(schema='item-deadline-v1', item_seconds=self.c.settings['item_seconds'], elapsed_s=rel(now),
|
| 156 |
+
slow_started_s=rel(self.slow_started), draft_end_s=rel(self.draft_end_at),
|
| 157 |
+
readout_s=None if self.readout_s is None else round(self.readout_s, 6),
|
| 158 |
+
draft_tokens=self.draft_tokens, chunks=len(self.chunks),
|
| 159 |
+
chunk_tokens=[c['generated'] for c in self.chunks][:64],
|
| 160 |
+
chunk_seconds=[round(c['seconds'], 6) for c in self.chunks][:64],
|
| 161 |
+
chunk_end_s=[round(c['end'] - self.start, 6) for c in self.chunks][:64],
|
| 162 |
+
chunk_cum_tokens=[c['cum_tokens'] for c in self.chunks][:64],
|
| 163 |
+
chunk_cum_input=[c['cum_input'] for c in self.chunks][:64],
|
| 164 |
+
chunk_cum_generated=[c['cum_generated'] for c in self.chunks][:64],
|
| 165 |
+
draft_by_marker=bool(self._by_marker),
|
| 166 |
+
budget_stop=self.budget_stop, straddle=self.straddle, forced=self.forced,
|
| 167 |
+
timeout_phase=self.timeout_phase, reconciled=self.reconciled, exact=self.exact(),
|
| 168 |
+
max_new_tokens=self.max_new_tokens, token_s=round(speed.token_s, 6),
|
| 169 |
+
call_s=round(speed.call_s, 6), readout_reserve_s=round(speed.readout_reserve(self.kind), 6))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def reconcile(detail, meters):
|
| 173 |
+
"""Usage arithmetic of core.py _one_impl, without the timeout estimate."""
|
| 174 |
+
submitted = sum(m['engine_submitted'] for m in meters)
|
| 175 |
+
cached = sum(m['cached'] for m in meters)
|
| 176 |
+
generated = sum(m.get('engine_generated', m['output']) for m in meters)
|
| 177 |
+
engine_input = sum(m.get('engine_input', m['engine_submitted'] - m['cached']) for m in meters)
|
| 178 |
+
detail['usage_estimated'] = False
|
| 179 |
+
detail['input_tokens_details'].update(
|
| 180 |
+
cached_tokens=cached, engine_submitted_tokens=submitted, engine_generated_tokens=generated,
|
| 181 |
+
engine_calls=sum(m.get('engine_calls', 0) for m in meters), cached_tokens_known=True)
|
| 182 |
+
detail['usage_bases'] = {
|
| 183 |
+
'engine': dict(input_tokens=engine_input, output_tokens=generated),
|
| 184 |
+
'each_once': dict(input_tokens=detail['prompt_tokens'], output_tokens=detail['completion_tokens']),
|
| 185 |
+
'submitted': dict(input_tokens=submitted, output_tokens=detail['completion_tokens'])}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def marker_completion(decode, previous, new, marker):
|
| 189 |
+
"""Smallest j such that the marker is complete in decode(previous+new[:j]); None if absent."""
|
| 190 |
+
if marker not in decode(list(previous) + list(new)):
|
| 191 |
+
return None
|
| 192 |
+
for j in range(1, len(new) + 1):
|
| 193 |
+
if marker in decode(list(previous) + list(new[:j])):
|
| 194 |
+
return j
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@contextmanager
|
| 199 |
+
def request_clock(start=None):
|
| 200 |
+
previous = getattr(_local, 'request_start', None)
|
| 201 |
+
_local.request_start = time.monotonic() if start is None else start
|
| 202 |
+
try:
|
| 203 |
+
yield
|
| 204 |
+
finally:
|
| 205 |
+
_local.request_start = previous
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def active_plan():
|
| 209 |
+
return getattr(_local, 'plan', None)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _install_core(core):
|
| 213 |
+
if getattr(core, '_deadline_core', None):
|
| 214 |
+
return
|
| 215 |
+
original_bounded = core.bounded_slow_attempt
|
| 216 |
+
original_new_meter = core.new_meter
|
| 217 |
+
|
| 218 |
+
def bounded_slow_attempt(backend, ids, groups, config, meter, model_lock, *, on_service_failure=None):
|
| 219 |
+
plan = getattr(_local, 'plan', None)
|
| 220 |
+
if plan is None:
|
| 221 |
+
return original_bounded(backend, ids, groups, config, meter, model_lock,
|
| 222 |
+
on_service_failure=on_service_failure)
|
| 223 |
+
return plan.c.bounded(plan, backend, ids, groups, config, meter, model_lock, on_service_failure)
|
| 224 |
+
|
| 225 |
+
def new_meter():
|
| 226 |
+
m = original_new_meter()
|
| 227 |
+
plan = getattr(_local, 'plan', None)
|
| 228 |
+
if plan is not None:
|
| 229 |
+
plan.meters.append(m)
|
| 230 |
+
return m
|
| 231 |
+
|
| 232 |
+
core.bounded_slow_attempt = bounded_slow_attempt
|
| 233 |
+
core.new_meter = new_meter
|
| 234 |
+
core._deadline_core = dict(bounded_slow_attempt=original_bounded, new_meter=original_new_meter)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _install_backend(backend):
|
| 238 |
+
if getattr(backend, '_deadline_backend', None):
|
| 239 |
+
return
|
| 240 |
+
original = backend.slow_session
|
| 241 |
+
|
| 242 |
+
def slow_session(ids, unparsed_policy, meter, max_new_tokens=None):
|
| 243 |
+
plan = getattr(_local, 'plan', None)
|
| 244 |
+
if plan is None:
|
| 245 |
+
return original(ids, unparsed_policy, meter, max_new_tokens=max_new_tokens)
|
| 246 |
+
need(plan.c.backend is backend, 'DEADLINE_BACKEND_MISMATCH')
|
| 247 |
+
return plan.c.chunked_session(plan, ids, unparsed_policy, meter, max_new_tokens)
|
| 248 |
+
|
| 249 |
+
backend.slow_session = slow_session
|
| 250 |
+
backend._deadline_backend = dict(slow_session=original)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def measure_speed(backend, settings, system_prompt, clock=time.monotonic):
|
| 254 |
+
"""Synthetic prompt only. Paired 1/N exact-length greedy calls; conservative maxima."""
|
| 255 |
+
g = backend.generation
|
| 256 |
+
ids = backend.prompt_ids(system_prompt, 'Ready?', False)
|
| 257 |
+
n_tokens = settings['warmup_tokens']
|
| 258 |
+
pairs = []
|
| 259 |
+
|
| 260 |
+
def run(n):
|
| 261 |
+
params = backend.SamplingParams(max_tokens=n, min_tokens=n, temperature=0.0, top_p=1.0, top_k=-1,
|
| 262 |
+
repetition_penalty=g['repetition_penalty'], seed=g['seed'],
|
| 263 |
+
detokenize=False)
|
| 264 |
+
t0 = clock()
|
| 265 |
+
out = backend._completion(ids, params, None, 'draft')
|
| 266 |
+
dt = clock() - t0
|
| 267 |
+
need(len(out.token_ids) == n, 'DEADLINE_WARMUP_LENGTH')
|
| 268 |
+
return dt
|
| 269 |
+
|
| 270 |
+
for _ in range(settings['warmup_repeats']):
|
| 271 |
+
one = run(1)
|
| 272 |
+
many = run(n_tokens)
|
| 273 |
+
pairs.append((one, many))
|
| 274 |
+
call_s = float(max(x for x, _ in pairs))
|
| 275 |
+
token_s = float(max(max((y - x) / (n_tokens - 1), y / n_tokens) for x, y in pairs))
|
| 276 |
+
token_s = max(token_s, 1e-6)
|
| 277 |
+
call_s = max(call_s, 1e-6)
|
| 278 |
+
return SpeedModel(token_s, call_s, settings), dict(pairs_s=pairs, tokens=n_tokens)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
class Controller:
|
| 282 |
+
def __init__(self, core, backends, backend, settings, speed, clock=time.monotonic, warmup=None):
|
| 283 |
+
self.core, self.backends, self.backend = core, backends, backend
|
| 284 |
+
self.settings, self.speed, self.clock = settings, speed, clock
|
| 285 |
+
self.warmup = warmup
|
| 286 |
+
|
| 287 |
+
# ---- per item ---------------------------------------------------------------------------
|
| 288 |
+
def slow_attempt(self, backend, ids, groups, config, meter):
|
| 289 |
+
"""core.slow_attempt semantics (same exception mapping), except that the deadline
|
| 290 |
+
path skips release_failed_session's gc.collect(): on vLLM there is no allocator state to
|
| 291 |
+
clear, and a full collection would only delay the fast answer past the item cap."""
|
| 292 |
+
core = self.core
|
| 293 |
+
failed = None
|
| 294 |
+
try:
|
| 295 |
+
return core.slow_readout(backend, ids, groups, config, meter)
|
| 296 |
+
except TimeoutError:
|
| 297 |
+
failed = 'timeout'
|
| 298 |
+
except Exception as exc:
|
| 299 |
+
if core.unrecoverable_engine_error(exc):
|
| 300 |
+
raise core.ServiceFailure('unrecoverable engine failure') from exc
|
| 301 |
+
failed = 'error'
|
| 302 |
+
if failed == 'timeout':
|
| 303 |
+
raise TimeoutError('Slow deadline exceeded')
|
| 304 |
+
core.release_failed_session(backend)
|
| 305 |
+
raise RuntimeError('Slow session failed')
|
| 306 |
+
|
| 307 |
+
def bounded(self, plan, backend, ids, groups, config, meter, model_lock, on_service_failure):
|
| 308 |
+
core = self.core
|
| 309 |
+
plan._groups, plan._config, plan._slow_meter = groups, config, meter
|
| 310 |
+
meter['deadline'] = plan.meter_deadline
|
| 311 |
+
plan.slow_started = self.clock()
|
| 312 |
+
try:
|
| 313 |
+
with model_lock:
|
| 314 |
+
core.check_deadline(meter)
|
| 315 |
+
value = self.slow_attempt(backend, ids, groups, config, meter)
|
| 316 |
+
core.check_deadline(meter)
|
| 317 |
+
if plan.readout_start is not None:
|
| 318 |
+
plan.readout_s = self.clock() - plan.readout_start
|
| 319 |
+
self.speed.observe_readout(plan.kind, plan.readout_s)
|
| 320 |
+
return value
|
| 321 |
+
except Exception as exc:
|
| 322 |
+
if core.unrecoverable_engine_error(exc) and on_service_failure is not None:
|
| 323 |
+
on_service_failure()
|
| 324 |
+
if isinstance(exc, TimeoutError):
|
| 325 |
+
plan.timeout_phase = 'readout' if plan.readout_start is not None else 'draft'
|
| 326 |
+
raise
|
| 327 |
+
|
| 328 |
+
def chunked_session(self, plan, ids, unparsed_policy, meter, max_new_tokens=None):
|
| 329 |
+
backend, s, speed = self.backend, self.settings, self.speed
|
| 330 |
+
g = backend.generation
|
| 331 |
+
need(g['do_sample'] is False and g['repetition_penalty'] == 1.0, 'DEADLINE_CHUNKS_REQUIRE_GREEDY')
|
| 332 |
+
count = self.backends._max_new_tokens(max_new_tokens or plan.max_new_tokens)
|
| 333 |
+
marker = backend.marker
|
| 334 |
+
prompt = list(ids)
|
| 335 |
+
gen = []
|
| 336 |
+
by_marker = False
|
| 337 |
+
counted_prompt = False
|
| 338 |
+
while len(gen) < count:
|
| 339 |
+
now = self.clock()
|
| 340 |
+
if now >= plan.meter_deadline:
|
| 341 |
+
raise TimeoutError('item deadline before chunk')
|
| 342 |
+
left = count - len(gen)
|
| 343 |
+
n = min(s['chunk_tokens'], left, speed.fit_tokens(plan.draft_deadline() - now))
|
| 344 |
+
if n < min(s['min_chunk_tokens'], left):
|
| 345 |
+
if not gen:
|
| 346 |
+
raise TimeoutError('no draft budget')
|
| 347 |
+
plan.budget_stop = True
|
| 348 |
+
break
|
| 349 |
+
params = backend.SamplingParams(max_tokens=n, temperature=0.0, top_p=1.0, top_k=-1,
|
| 350 |
+
repetition_penalty=g['repetition_penalty'], seed=g['seed'],
|
| 351 |
+
stop=[marker], include_stop_str_in_output=True)
|
| 352 |
+
if not counted_prompt:
|
| 353 |
+
meter['prompt'] += len(prompt) # the backend's slow_session counts the prompt once
|
| 354 |
+
counted_prompt = True
|
| 355 |
+
t0 = self.clock()
|
| 356 |
+
out = backend._completion(prompt + gen, params, meter, 'draft')
|
| 357 |
+
t1 = self.clock()
|
| 358 |
+
dt = t1 - t0
|
| 359 |
+
new = list(out.token_ids)
|
| 360 |
+
speed.observe_chunk(len(new), dt)
|
| 361 |
+
plan.chunks.append(dict(n=n, generated=len(new), seconds=dt, end=t1, cum_tokens=len(gen) + len(new),
|
| 362 |
+
cum_input=int(meter.get('engine_input', 0)),
|
| 363 |
+
cum_generated=int(meter.get('engine_generated', 0))))
|
| 364 |
+
if getattr(out, 'stop_reason', None) == marker and getattr(out, 'finish_reason', None) == 'stop':
|
| 365 |
+
gen += new
|
| 366 |
+
by_marker = True
|
| 367 |
+
break
|
| 368 |
+
j = marker_completion(backend.decode, gen, new, marker)
|
| 369 |
+
if j is not None:
|
| 370 |
+
# One-shot generation would have stopped at the token completing the marker. The
|
| 371 |
+
# extra tokens of this chunk were generated and stay metered (exact cost).
|
| 372 |
+
gen += new[:j]
|
| 373 |
+
by_marker = True
|
| 374 |
+
plan.straddle = True
|
| 375 |
+
break
|
| 376 |
+
gen += new
|
| 377 |
+
if getattr(out, 'finish_reason', None) == 'stop' or len(new) < n:
|
| 378 |
+
break # EOS: the draft ended without the marker
|
| 379 |
+
plan.draft_tokens = len(gen)
|
| 380 |
+
plan.draft_end_at = self.clock()
|
| 381 |
+
plan._prompt_ids, plan._draft_ids, plan._by_marker = list(prompt), list(gen), by_marker
|
| 382 |
+
parsed = by_marker and bool(re.search(r'(?m)^[ \t]*' + re.escape(marker) + r'$', backend.decode(gen)))
|
| 383 |
+
if not parsed:
|
| 384 |
+
if unparsed_policy == 'fast':
|
| 385 |
+
return None, False
|
| 386 |
+
forced = backend.encode('\n' + marker)
|
| 387 |
+
gen += forced
|
| 388 |
+
meter['output'] += len(forced)
|
| 389 |
+
meter['forced'] += len(forced)
|
| 390 |
+
plan.forced = True
|
| 391 |
+
plan.readout_start = self.clock()
|
| 392 |
+
return self.backends.VLLMPrefixSession(backend, prompt + gen, meter), parsed
|
| 393 |
+
|
| 394 |
+
# ---- truncated-draft readout (diagnostic for offline measurement; not used when serving) ----
|
| 395 |
+
def truncated_readout(self, plan, n_tokens, labels, qtype, calibration_block):
|
| 396 |
+
"""Readout of this item's draft cut at n_tokens (no marker within them) + the forced answer
|
| 397 |
+
marker, on a fresh meter: what a one-shot cap of n_tokens would have read out (greedy drafts
|
| 398 |
+
are prefix-identical). Returns (probabilities dict, meter, seconds). Caller checks eligibility."""
|
| 399 |
+
need(plan._draft_ids is not None and len(plan._draft_ids) > n_tokens, 'DEADLINE_TRUNC_NOT_LONGER')
|
| 400 |
+
backend, core = self.backend, self.core
|
| 401 |
+
forced = backend.encode('\n' + backend.marker)
|
| 402 |
+
meter = self.backends.new_meter()
|
| 403 |
+
meter['output'] += len(forced)
|
| 404 |
+
meter['forced'] += len(forced)
|
| 405 |
+
t0 = self.clock()
|
| 406 |
+
session = self.backends.VLLMPrefixSession(backend, plan._prompt_ids + plan._draft_ids[:n_tokens] + forced, meter)
|
| 407 |
+
scores, _ = core.sequence_scores(session, backend, plan._groups, meter)
|
| 408 |
+
probs = core.probabilities(labels, scores, qtype, calibration_block)
|
| 409 |
+
return probs, meter, self.clock() - t0
|
| 410 |
+
|
| 411 |
+
# ---- low-margin choice re-read ------------------------------------------------------------
|
| 412 |
+
def reread_reading(self, service, task):
|
| 413 |
+
"""One fast read of a choice task with the option order reversed (label -> criterion pairs kept;
|
| 414 |
+
render re-letters the options), the service's 'fast' calibration. Returns (probabilities by label, meter,
|
| 415 |
+
seconds). The meter is created through core.new_meter, so inside an item it joins the item's meters."""
|
| 416 |
+
core = self.core
|
| 417 |
+
need(task['question']['type'] == 'choice' and task.get('labels'), 'DEADLINE_REREAD_TASK')
|
| 418 |
+
t2 = dict(task, labels=list(reversed(task['labels'])))
|
| 419 |
+
meter = core.new_meter()
|
| 420 |
+
t0 = self.clock()
|
| 421 |
+
_, (labels2, groups2, fast_ids2, _), _ = core.prepare_task(t2, self.backend, service.config)
|
| 422 |
+
with service.model_lock:
|
| 423 |
+
scores2, _, _ = core.fast_attempt_with_retry(self.backend, t2, service.config, fast_ids2, labels2, groups2, meter)
|
| 424 |
+
need(scores2 is not None, 'DEADLINE_REREAD_SCORES')
|
| 425 |
+
probs = core.probabilities(labels2, scores2, 'choice', service.calibration['fast'])
|
| 426 |
+
return probs, meter, self.clock() - t0
|
| 427 |
+
|
| 428 |
+
@staticmethod
|
| 429 |
+
def reread_due(task, detail, tau):
|
| 430 |
+
m = detail.get('fast_margin')
|
| 431 |
+
return (tau is not None and tau > 0 and task['question']['type'] == 'choice' and detail.get('path') == 'fast'
|
| 432 |
+
and detail.get('budget_tier') != 'long' and not detail.get('long_item')
|
| 433 |
+
and isinstance(m, (int, float)) and math.isfinite(m) and m < tau)
|
| 434 |
+
|
| 435 |
+
def apply_reread(self, service, task, answer_value, detail):
|
| 436 |
+
"""Online re-read: average the fast answer with the reversed-order read; add the read to the item's
|
| 437 |
+
usage (every basis) and token details; record it in detail['reread']."""
|
| 438 |
+
probs, meter, seconds = self.reread_reading(service, task)
|
| 439 |
+
first = answer_value['probabilities']
|
| 440 |
+
need(set(first) == set(probs), 'DEADLINE_REREAD_LABELS')
|
| 441 |
+
mixed = {k: (first[k] + probs[k]) / 2 for k in first}
|
| 442 |
+
winner = max(task['labels'], key=mixed.get)
|
| 443 |
+
engine_input = meter.get('engine_input', meter['engine_submitted'] - meter['cached'])
|
| 444 |
+
generated = meter.get('engine_generated', meter['output'])
|
| 445 |
+
bases = detail['usage_bases']
|
| 446 |
+
bases['engine']['input_tokens'] += engine_input
|
| 447 |
+
bases['engine']['output_tokens'] += generated
|
| 448 |
+
bases['each_once']['input_tokens'] += meter['prompt']
|
| 449 |
+
bases['each_once']['output_tokens'] += meter['output']
|
| 450 |
+
bases['submitted']['input_tokens'] += meter['engine_submitted']
|
| 451 |
+
bases['submitted']['output_tokens'] += meter['output']
|
| 452 |
+
d = detail['input_tokens_details']
|
| 453 |
+
d['cached_tokens'] += meter['cached']
|
| 454 |
+
d['engine_submitted_tokens'] += meter['engine_submitted']
|
| 455 |
+
d['engine_generated_tokens'] += generated
|
| 456 |
+
d['engine_calls'] += meter.get('engine_calls', 0)
|
| 457 |
+
if meter.get('cached_unknown'):
|
| 458 |
+
d['cached_tokens_known'] = False
|
| 459 |
+
if meter.get('engine_usage_unknown') or meter.get('cached_unknown'):
|
| 460 |
+
detail['usage_estimated'] = True
|
| 461 |
+
detail['prompt_tokens'] += meter['prompt']
|
| 462 |
+
detail['completion_tokens'] += meter['output']
|
| 463 |
+
detail['total_tokens'] = detail['prompt_tokens'] + detail['completion_tokens']
|
| 464 |
+
detail['readout_output_tokens'] += meter['probes']
|
| 465 |
+
detail['prediction'] = winner
|
| 466 |
+
detail['reread'] = dict(input_tokens=int(engine_input), output_tokens=int(generated), seconds=seconds,
|
| 467 |
+
order='reversed')
|
| 468 |
+
answer_value = dict(answer_value, probabilities=mixed)
|
| 469 |
+
answer_value['choice'] = winner
|
| 470 |
+
return answer_value, detail
|
| 471 |
+
|
| 472 |
+
# ---- per service ------------------------------------------------------------------------
|
| 473 |
+
def wrap_service(self, service, slow_types=None, reread_tau=None):
|
| 474 |
+
"""slow_types: question types allowed on the slow path (None = all); reread_tau: low-margin choice
|
| 475 |
+
re-read threshold (None or 0 = off)."""
|
| 476 |
+
need(service.backend is self.backend, 'DEADLINE_SERVICE_BACKEND')
|
| 477 |
+
need(not getattr(service, '_deadline_wrapped', False), 'DEADLINE_ALREADY_WRAPPED')
|
| 478 |
+
need(slow_types is None or (set(slow_types) <= {'choice', 'noul', 'score'} and len(slow_types) > 0), 'DEADLINE_SLOW_TYPES')
|
| 479 |
+
need(reread_tau is None or (isinstance(reread_tau, (int, float)) and math.isfinite(reread_tau) and reread_tau >= 0),
|
| 480 |
+
'DEADLINE_REREAD_TAU')
|
| 481 |
+
original_one, original_answer = service._one_impl, service.answer
|
| 482 |
+
clock = self.clock
|
| 483 |
+
allowed = None if slow_types is None else frozenset(slow_types)
|
| 484 |
+
|
| 485 |
+
def answer(payload):
|
| 486 |
+
owned = getattr(_local, 'request_start', None) is None
|
| 487 |
+
if owned:
|
| 488 |
+
_local.request_start = clock()
|
| 489 |
+
try:
|
| 490 |
+
return original_answer(payload)
|
| 491 |
+
finally:
|
| 492 |
+
if owned:
|
| 493 |
+
_local.request_start = None
|
| 494 |
+
|
| 495 |
+
def one(task):
|
| 496 |
+
need(getattr(_local, 'plan', None) is None, 'DEADLINE_NESTED_ITEM')
|
| 497 |
+
start = getattr(_local, 'request_start', None)
|
| 498 |
+
plan = ItemPlan(self, clock() if start is None else start,
|
| 499 |
+
service.config['generation']['max_new_tokens'], task['question']['type'])
|
| 500 |
+
service._deadline_last = plan
|
| 501 |
+
_local.plan = plan
|
| 502 |
+
routing = service.config['routing']
|
| 503 |
+
switched_off = allowed is not None and task['question']['type'] not in allowed
|
| 504 |
+
saved = routing['margin_threshold']
|
| 505 |
+
try:
|
| 506 |
+
if switched_off:
|
| 507 |
+
routing['margin_threshold'] = 0.0 # margins are >= 0: never requests slow, never counted
|
| 508 |
+
try:
|
| 509 |
+
answer_value, detail = original_one(task)
|
| 510 |
+
finally:
|
| 511 |
+
routing['margin_threshold'] = saved
|
| 512 |
+
if detail.get('route') == 'slow_timeout_fast' and plan.exact():
|
| 513 |
+
reconcile(detail, plan.meters)
|
| 514 |
+
plan.reconciled = True
|
| 515 |
+
if self.reread_due(task, detail, reread_tau):
|
| 516 |
+
try:
|
| 517 |
+
answer_value, detail = self.apply_reread(service, task, answer_value, detail)
|
| 518 |
+
except Exception as exc:
|
| 519 |
+
if self.core.unrecoverable_engine_error(exc):
|
| 520 |
+
raise self.core.ServiceFailure('unrecoverable engine failure') from exc
|
| 521 |
+
detail['reread_failed'] = type(exc).__name__ # fast answer kept, as without re-read
|
| 522 |
+
detail['item_deadline'] = plan.audit(clock())
|
| 523 |
+
detail['item_deadline']['slow_type_off'] = switched_off
|
| 524 |
+
return answer_value, detail
|
| 525 |
+
finally:
|
| 526 |
+
_local.plan = None
|
| 527 |
+
|
| 528 |
+
service._one_impl = one
|
| 529 |
+
service.answer = answer
|
| 530 |
+
service._deadline_wrapped = True
|
| 531 |
+
service._deadline_slow_types = None if allowed is None else sorted(allowed)
|
| 532 |
+
service._deadline_reread_tau = reread_tau
|
| 533 |
+
return service
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def install(core, backends, backend, settings=None, system_prompt=None, clock=time.monotonic, speed=None):
|
| 537 |
+
"""Process-wide, idempotent dispatchers + one controller for this backend."""
|
| 538 |
+
s = settings_from(settings)
|
| 539 |
+
existing = getattr(backend, '_deadline_controller', None)
|
| 540 |
+
if existing is not None:
|
| 541 |
+
need(existing.settings == s, 'DEADLINE_SETTINGS_CHANGED')
|
| 542 |
+
return existing
|
| 543 |
+
_install_core(core)
|
| 544 |
+
warm = None
|
| 545 |
+
if speed is None:
|
| 546 |
+
need(isinstance(system_prompt, str), 'DEADLINE_SYSTEM_PROMPT')
|
| 547 |
+
speed, warm = measure_speed(backend, s, system_prompt, clock)
|
| 548 |
+
_install_backend(backend)
|
| 549 |
+
controller = Controller(core, backends, backend, s, speed, clock, warm)
|
| 550 |
+
backend._deadline_controller = controller
|
| 551 |
+
return controller
|
service/launch.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Foreground supervisor: runtime and model checks, startup attempts, readiness, termination.
|
| 2 |
+
|
| 3 |
+
The service writes files only below --state-dir (plus shared memory in /dev/shm):
|
| 4 |
+
<state-dir>/<run-id>-startup/ attempts.jsonl, attempt-N.log, state-N/ (quota state), tmp/, home/
|
| 5 |
+
<state-dir>/cache/ compilation caches shared by runs that use the same state directory
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
from contextlib import contextmanager
|
| 9 |
+
import hashlib
|
| 10 |
+
import importlib.metadata
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import re
|
| 15 |
+
import signal
|
| 16 |
+
import socket
|
| 17 |
+
import subprocess
|
| 18 |
+
import sys
|
| 19 |
+
import tempfile
|
| 20 |
+
import threading
|
| 21 |
+
import time
|
| 22 |
+
from datetime import datetime, timezone
|
| 23 |
+
import urllib.request
|
| 24 |
+
|
| 25 |
+
ROOT=Path(__file__).resolve().parent
|
| 26 |
+
IMAGE='vllm/vllm-openai@sha256:a4a4c0437bf7240089da5f08aa370c4aee17ae5290f7a3b468825ee26c4c3a6b'
|
| 27 |
+
# Model identity: official W4A16 CT base files + our unmerged adapter (MODEL_MANIFEST.json).
|
| 28 |
+
_MANIFEST=json.loads((ROOT/'MODEL_MANIFEST.json').read_text(encoding='utf-8'))
|
| 29 |
+
MODEL_KEY='metask-gemma4-31b-lora-w4a16'
|
| 30 |
+
MODEL_REVISION=_MANIFEST['repo_id']+'@'+str(_MANIFEST.get('revision') or 'unpublished')
|
| 31 |
+
# q=0.056: routed; the quota counter below --state-dir caps the share of slow (drafting) decisions.
|
| 32 |
+
SERVED_MODE='routed'
|
| 33 |
+
VERSIONS={'vllm':'0.31.0','torch':'2.13.0','transformers':'5.17.0','tokenizers':'0.23.2',
|
| 34 |
+
'triton':'3.7.1','compressed-tensors':'0.17.0'}
|
| 35 |
+
MODEL_SHA256={n:v['sha256'] for n,v in _MANIFEST['files'].items() if not n.endswith('.safetensors')}
|
| 36 |
+
SHARD_BYTES={n:v['size'] for n,v in _MANIFEST['files'].items() if n.endswith('.safetensors')}
|
| 37 |
+
SHARD_SHA256={n:v['sha256'] for n,v in _MANIFEST['files'].items() if n.endswith('.safetensors')}
|
| 38 |
+
# Unmerged LoRA: <model>/adapter holds exactly these files; configs name it by ADAPTER_REF.
|
| 39 |
+
ADAPTER_REF='model-subdir:adapter'
|
| 40 |
+
_ADAPTER=_MANIFEST.get('adapter')
|
| 41 |
+
ADAPTER_SUBDIR=(_ADAPTER or {}).get('subdir') or 'adapter'
|
| 42 |
+
ADAPTER_FILES={n:dict(v) for n,v in ((_ADAPTER or {}).get('files') or {}).items()}
|
| 43 |
+
RUN_ID=re.compile(r'[A-Za-z0-9][A-Za-z0-9._-]{0,63}')
|
| 44 |
+
BINDS=('127.0.0.1','0.0.0.0')
|
| 45 |
+
STARTUP_TIMEOUT=1800
|
| 46 |
+
# vLLM places engine IPC sockets in TMPDIR; Unix socket paths are limited to 107 bytes.
|
| 47 |
+
MAX_TMP_PATH=70
|
| 48 |
+
# Startup attempts: vLLM's own choice (auto), then TRITON_ATTN. This checkpoint mixes attention
|
| 49 |
+
# head sizes 256 and 512; where FlashAttention 4 is unavailable (for example compute capability
|
| 50 |
+
# 8.x, the measured RTX 4090) vLLM 0.31 resolves auto to TRITON_ATTN, and FLASH_ATTN cannot start.
|
| 51 |
+
ATTEMPTS=(None,'TRITON_ATTN')
|
| 52 |
+
# Model folder entries a loader may read; among them only the pinned files are accepted.
|
| 53 |
+
LOADER_SUFFIXES=('.json','.safetensors','.bin','.pt','.pth','.gguf','.model','.tiktoken','.jinja','.py','.txt')
|
| 54 |
+
# Inherited path variables must stay inside --state-dir; unset ones default below state/cache or HOME.
|
| 55 |
+
PATH_VARIABLES=('XDG_CACHE_HOME','VLLM_CACHE_ROOT','TRITON_CACHE_DIR','TORCHINDUCTOR_CACHE_DIR','HF_HOME',
|
| 56 |
+
'CUDA_CACHE_PATH','NUMBA_CACHE_DIR','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_STATE_HOME',
|
| 57 |
+
'VLLM_CONFIG_ROOT','TRITON_HOME','FLASHINFER_WORKSPACE_BASE','TORCH_HOME','TORCH_EXTENSIONS_DIR',
|
| 58 |
+
'MPLCONFIGDIR','HF_HUB_CACHE','HUGGINGFACE_HUB_CACHE','TRANSFORMERS_CACHE','HF_ASSETS_CACHE')
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def check_preparation(config=None,allow_nonformal=False):
|
| 63 |
+
c=json.loads(Path(config or ROOT/'config.json').read_text(encoding='utf-8'))
|
| 64 |
+
if not c.get('package',{}).get('nonformal_fitted') or c['package'].get('sample_only'):
|
| 65 |
+
raise ValueError('Service fit pending; preparation placeholders cannot serve')
|
| 66 |
+
if not _MANIFEST.get('identity_sha256') or not _MANIFEST['files'] or not ADAPTER_FILES:
|
| 67 |
+
raise ValueError('Model identity pending')
|
| 68 |
+
if c.get('pipeline',{}).get('model_identity_sha256')!=_MANIFEST['identity_sha256']:
|
| 69 |
+
raise ValueError('Configuration and model manifest identities differ')
|
| 70 |
+
if c['model'].get('adapter')!=ADAPTER_REF:
|
| 71 |
+
raise ValueError('Configuration adapter reference differs from the model manifest')
|
| 72 |
+
if c.get('routing',{}).get('mode')!=SERVED_MODE:
|
| 73 |
+
raise ValueError('Configuration mode differs from the served fast-only mode')
|
| 74 |
+
if not allow_nonformal:
|
| 75 |
+
if not re.fullmatch('[0-9a-f]{40}',str(_MANIFEST.get('revision') or '')) or _MANIFEST['repo_id'].startswith('OWNER/'):
|
| 76 |
+
raise ValueError('The model manifest lacks the own Hugging Face repository and full revision')
|
| 77 |
+
if not c['package'].get('finalized') or not (ROOT/'FREEZE.json').is_file():
|
| 78 |
+
raise ValueError('Package has not been frozen (no FREEZE.json); use a frozen release package')
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def check_adapter(model):
|
| 82 |
+
"""Read-only check of <model>/adapter: exactly the pinned files, regular, pinned size and full SHA-256."""
|
| 83 |
+
if _ADAPTER is None:
|
| 84 |
+
return None
|
| 85 |
+
if not ADAPTER_FILES:
|
| 86 |
+
raise ValueError('Adapter identity pending: rebuild with the staged model folder')
|
| 87 |
+
folder=Path(model)/ADAPTER_SUBDIR
|
| 88 |
+
if folder.is_symlink() or not folder.is_dir():
|
| 89 |
+
raise ValueError(f'Missing adapter folder {ADAPTER_SUBDIR}/ in the model folder; it must hold '+', '.join(sorted(ADAPTER_FILES)))
|
| 90 |
+
names={p.name for p in folder.iterdir()}
|
| 91 |
+
if names!=set(ADAPTER_FILES):
|
| 92 |
+
raise ValueError(f'{ADAPTER_SUBDIR}/ must contain exactly: '+', '.join(sorted(ADAPTER_FILES)))
|
| 93 |
+
for name,entry in sorted(ADAPTER_FILES.items()):
|
| 94 |
+
path=_model_file(folder,name)
|
| 95 |
+
if path.stat().st_size!=entry['size'] or sha256(path)!=entry['sha256']:
|
| 96 |
+
raise ValueError(f'Adapter file differs from {MODEL_REVISION}: {name}')
|
| 97 |
+
return folder.resolve()
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def materialize_adapter(config,adapter):
|
| 101 |
+
"""Replace ADAPTER_REF by the verified adapter folder in model.adapter and the threshold identity."""
|
| 102 |
+
c=json.loads(json.dumps(config))
|
| 103 |
+
blocks=[c.get('routing',{})]+[p.get('routing',{}) for p in c.get('profiles',{}).values()]
|
| 104 |
+
if adapter is None:
|
| 105 |
+
if c['model'].get('adapter') is not None or any((p.get('model') or {}).get('adapter') is not None
|
| 106 |
+
for p in c.get('profiles',{}).values()):
|
| 107 |
+
raise ValueError('Configuration names an adapter, but this arm serves none')
|
| 108 |
+
return c
|
| 109 |
+
if c['model'].get('adapter')!=ADAPTER_REF:
|
| 110 |
+
raise ValueError('Adapter configuration must use the model-relative adapter reference')
|
| 111 |
+
value=str(adapter)
|
| 112 |
+
c['model']['adapter']=value
|
| 113 |
+
for profile in c.get('profiles',{}).values():
|
| 114 |
+
model=profile.get('model',{})
|
| 115 |
+
if 'adapter' in model:
|
| 116 |
+
if model['adapter']!=ADAPTER_REF:raise ValueError('Profile names a different adapter')
|
| 117 |
+
model['adapter']=value
|
| 118 |
+
for block in blocks:
|
| 119 |
+
fit=block.get('margin_threshold_fitted_for')
|
| 120 |
+
if isinstance(fit,dict) and 'adapter' in fit:
|
| 121 |
+
if fit['adapter']!=ADAPTER_REF:raise ValueError('Threshold was fitted for a different adapter reference')
|
| 122 |
+
fit['adapter']=value
|
| 123 |
+
return c
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def write_runtime_config(startup,config):
|
| 127 |
+
"""The materialized per-run configuration lives only below --state-dir."""
|
| 128 |
+
path=Path(startup)/'config.runtime.json'
|
| 129 |
+
with path.open('x',encoding='utf-8',newline='\n') as out:
|
| 130 |
+
json.dump(config,out,ensure_ascii=False,indent=2,allow_nan=False)
|
| 131 |
+
out.write('\n')
|
| 132 |
+
return path
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def check_python(version_info=None):
|
| 136 |
+
version=tuple((version_info or sys.version_info)[:2])
|
| 137 |
+
if version<(3,10):raise ValueError('Python >= 3.10 required')
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def check_versions():
|
| 141 |
+
found={}
|
| 142 |
+
for name,expected in VERSIONS.items():
|
| 143 |
+
try:actual=importlib.metadata.version(name)
|
| 144 |
+
except importlib.metadata.PackageNotFoundError:
|
| 145 |
+
raise ValueError(f'{name} is not installed; run in {IMAGE}') from None
|
| 146 |
+
if actual.split('+',1)[0]!=expected:
|
| 147 |
+
raise ValueError(f'{name} {actual} found, {expected} required; run in {IMAGE}')
|
| 148 |
+
found[name]=actual
|
| 149 |
+
return found
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _absolute(value,flag):
|
| 153 |
+
path=Path(value)
|
| 154 |
+
if not path.is_absolute():raise ValueError(f'{flag} must be an absolute path')
|
| 155 |
+
return path.resolve()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _overlap(a,b):
|
| 159 |
+
return a==b or a.is_relative_to(b) or b.is_relative_to(a)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def check_paths(model,state_dir):
|
| 163 |
+
model=_absolute(model,'--model');state=_absolute(state_dir,'--state-dir')
|
| 164 |
+
for path,flag in ((model,'--model'),(state,'--state-dir')):
|
| 165 |
+
if _overlap(path,ROOT):raise ValueError(f'{flag} must be outside the package and must not contain it')
|
| 166 |
+
if _overlap(model,state):raise ValueError('--model and --state-dir must not contain each other')
|
| 167 |
+
return model,state
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def sha256(path):
|
| 171 |
+
digest=hashlib.sha256()
|
| 172 |
+
with Path(path).open('rb') as source:
|
| 173 |
+
for chunk in iter(lambda:source.read(1<<20),b''):digest.update(chunk)
|
| 174 |
+
return digest.hexdigest()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
MODEL_FOLDER_RULE=('MODEL must contain regular files '
|
| 178 |
+
'(a Hugging Face cache snapshots/<sha> directory cannot be mounted alone)')
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _model_file(model,name):
|
| 182 |
+
path=model/name
|
| 183 |
+
if path.is_symlink():
|
| 184 |
+
try:path.resolve(strict=True)
|
| 185 |
+
except (OSError,RuntimeError): # RuntimeError: a link loop before Python 3.13
|
| 186 |
+
raise ValueError(f'Model file is a dangling or looping symbolic link: {name}; {MODEL_FOLDER_RULE}') from None
|
| 187 |
+
raise ValueError(f'Model file is a symbolic link: {name}; {MODEL_FOLDER_RULE}')
|
| 188 |
+
if not path.is_file():raise ValueError('Missing model file: '+name)
|
| 189 |
+
return path
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def check_model(model,full=False):
|
| 193 |
+
"""Read-only identity check of the model folder against MODEL_REVISION; nothing is written.
|
| 194 |
+
|
| 195 |
+
The pinned files must be regular files with the pinned SHA-256 (shards: size, or full hash).
|
| 196 |
+
Any other entry a loader may read (weights, tokenizer, processor or template files) is refused.
|
| 197 |
+
"""
|
| 198 |
+
model=Path(model)
|
| 199 |
+
if not MODEL_SHA256 or not SHARD_BYTES:raise ValueError('Model identity pending')
|
| 200 |
+
if not model.is_dir():raise ValueError('Model folder not found: '+str(model))
|
| 201 |
+
pinned={*MODEL_SHA256,*SHARD_BYTES,'PREPARATION.json'}
|
| 202 |
+
for entry in sorted(model.iterdir()):
|
| 203 |
+
name=entry.name
|
| 204 |
+
if name not in pinned and (name.lower().endswith(LOADER_SUFFIXES) or name.lower().startswith('tokenizer')):
|
| 205 |
+
raise ValueError(f'Unexpected file in model folder: {name}; use the files of {MODEL_REVISION} only')
|
| 206 |
+
for name,expected in MODEL_SHA256.items():
|
| 207 |
+
path=_model_file(model,name)
|
| 208 |
+
if sha256(path)!=expected:raise ValueError(f'Model file differs from {MODEL_REVISION}: {name}')
|
| 209 |
+
for name,size in SHARD_BYTES.items():
|
| 210 |
+
path=_model_file(model,name)
|
| 211 |
+
if path.stat().st_size!=size:raise ValueError(f'Model shard size differs from {MODEL_REVISION}: {name}')
|
| 212 |
+
if full and sha256(path)!=SHARD_SHA256[name]:raise ValueError(f'Model shard differs from {MODEL_REVISION}: {name}')
|
| 213 |
+
check_adapter(model)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
EXISTING_RUN='run_id already exists in --state-dir; use a new --state-dir (with the README commands: a new WORK)'
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def prepare_state(state,run_id=None):
|
| 220 |
+
"""Claim <state>/<run-id>-startup with exclusive creation; nothing is overwritten or removed."""
|
| 221 |
+
state=Path(state)
|
| 222 |
+
rid=run_id or datetime.now(timezone.utc).strftime('run-%Y%m%dT%H%M%S%fZ')
|
| 223 |
+
if not RUN_ID.fullmatch(rid):raise ValueError('run_id must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}')
|
| 224 |
+
startup=state/(rid+'-startup')
|
| 225 |
+
if len(os.fsencode(startup/'tmp'))>MAX_TMP_PATH:
|
| 226 |
+
raise ValueError(f'--state-dir path is too long for engine sockets; keep {startup/"tmp"} within {MAX_TMP_PATH} bytes')
|
| 227 |
+
try:state.mkdir(parents=True,exist_ok=True)
|
| 228 |
+
except OSError:raise ValueError('State directory is not writable: '+str(state)) from None
|
| 229 |
+
if (state/rid).exists() or startup.exists():raise ValueError(EXISTING_RUN)
|
| 230 |
+
cache=state/'cache'
|
| 231 |
+
if cache.is_symlink() or (cache.exists() and not cache.is_dir()):
|
| 232 |
+
raise ValueError(f'{cache} must be a directory, not a link or a file')
|
| 233 |
+
try:
|
| 234 |
+
startup.mkdir()
|
| 235 |
+
for name in ('tmp','home'):(startup/name).mkdir()
|
| 236 |
+
cache.mkdir(exist_ok=True)
|
| 237 |
+
with (startup/'attempts.jsonl').open('x',encoding='utf-8') as probe:
|
| 238 |
+
probe.flush();os.fsync(probe.fileno())
|
| 239 |
+
except FileExistsError:raise ValueError(EXISTING_RUN) from None
|
| 240 |
+
except OSError:raise ValueError('State directory is not writable: '+str(state)) from None
|
| 241 |
+
if cache.is_symlink() or not cache.resolve().is_relative_to(state.resolve()):
|
| 242 |
+
raise ValueError(f'{cache} must be a directory inside --state-dir, not a link')
|
| 243 |
+
return rid,startup
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def check_environment(base,state):
|
| 247 |
+
"""Refuse inherited cache or configuration paths outside --state-dir; nothing is written.
|
| 248 |
+
|
| 249 |
+
An inherited path variable is accepted only if it is absolute and inside --state-dir, so
|
| 250 |
+
writes cannot leave the state directory. start() runs this before claiming the run name.
|
| 251 |
+
"""
|
| 252 |
+
root=Path(state).resolve()
|
| 253 |
+
for key in PATH_VARIABLES:
|
| 254 |
+
if key not in base:continue
|
| 255 |
+
value=Path(base[key])
|
| 256 |
+
if not value.is_absolute() or not value.resolve().is_relative_to(root):
|
| 257 |
+
raise ValueError(f'{key}={base[key]} is outside --state-dir; do not set cache or configuration paths')
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def child_environment(base,state,startup):
|
| 261 |
+
"""Engine environment: offline, no telemetry, every write location below --state-dir."""
|
| 262 |
+
check_environment(base,state)
|
| 263 |
+
env=dict(base)
|
| 264 |
+
env['VLLM_BATCH_INVARIANT']='1'
|
| 265 |
+
env['VLLM_WORKER_MULTIPROC_METHOD']='spawn'
|
| 266 |
+
# vLLM 0.31 no longer reads the legacy attention variable; an inherited value would only log a warning.
|
| 267 |
+
env.pop('VLLM_ATTENTION_BACKEND',None);env.pop('JEV_ATTENTION_BACKEND',None)
|
| 268 |
+
env.update(VLLM_USE_FLASHINFER_SAMPLER='0',VLLM_NO_USAGE_STATS='1',DO_NOT_TRACK='1',
|
| 269 |
+
HF_HUB_OFFLINE='1',TRANSFORMERS_OFFLINE='1',PYTHONDONTWRITEBYTECODE='1',PYTHONUNBUFFERED='1',
|
| 270 |
+
VLLM_HOST_IP='127.0.0.1',HOME=str(startup/'home'),TMPDIR=str(startup/'tmp'))
|
| 271 |
+
cache=Path(state)/'cache'
|
| 272 |
+
for key,sub in (('XDG_CACHE_HOME',None),('VLLM_CACHE_ROOT','vllm'),('TRITON_CACHE_DIR','triton'),
|
| 273 |
+
('TORCHINDUCTOR_CACHE_DIR','inductor'),('HF_HOME','hf'),('CUDA_CACHE_PATH','nv'),
|
| 274 |
+
('NUMBA_CACHE_DIR','numba')):
|
| 275 |
+
env.setdefault(key,str(cache/sub if sub else cache))
|
| 276 |
+
return env
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def validate_health(health, no_cache=False):
|
| 280 |
+
bad=health.get('warnings',[])
|
| 281 |
+
if bad or health.get('status','ok')!='ok' or health.get('prefix_caching') is not (not no_cache) or health.get('supports_slow_session') is not True:
|
| 282 |
+
raise RuntimeError('Service readiness rejected: health warnings or cache/session capability; health='+json.dumps(health)[:1500])
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def start(model, port, mode='routed', config=None, no_cache=False, state_dir=None, run_id=None, bind='127.0.0.1', allow_nonformal=False):
|
| 286 |
+
check_preparation(config,allow_nonformal)
|
| 287 |
+
check_python()
|
| 288 |
+
check_versions()
|
| 289 |
+
if bind not in BINDS:raise ValueError('--bind must be one of '+', '.join(BINDS))
|
| 290 |
+
if state_dir is None:raise ValueError('--state-dir is required')
|
| 291 |
+
model,state=check_paths(model,state_dir)
|
| 292 |
+
check_environment(os.environ,state) # an input check: refused before anything is written
|
| 293 |
+
check_model(model)
|
| 294 |
+
from model_setup import report_processor_configs
|
| 295 |
+
report_processor_configs(model)
|
| 296 |
+
cfg=Path(config or ROOT/'config.json').resolve()
|
| 297 |
+
c=json.loads(cfg.read_text(encoding='utf-8'))
|
| 298 |
+
if mode=='routed' and not c.get('package',{}).get('finalized') and not allow_nonformal:
|
| 299 |
+
raise ValueError('Real fitted parameters must be finalized before routed serving')
|
| 300 |
+
try:
|
| 301 |
+
with socket.socket() as probe:
|
| 302 |
+
probe.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
|
| 303 |
+
probe.bind((bind,port))
|
| 304 |
+
except OSError:raise ValueError(f'Port {port} on {bind} is not available') from None
|
| 305 |
+
runtime=materialize_adapter(c,check_adapter(model))
|
| 306 |
+
rid,startup=prepare_state(state,run_id)
|
| 307 |
+
cfg=write_runtime_config(startup,runtime)
|
| 308 |
+
tempfile.tempdir=str(startup/'tmp')
|
| 309 |
+
env=child_environment(os.environ,state,startup)
|
| 310 |
+
args=[sys.executable,'-B',str(ROOT/'server.py'),'--config',str(cfg),'--model',str(model),
|
| 311 |
+
'--bind',bind,'--port',str(port),'--backend','vllm','--quant','awq',
|
| 312 |
+
'--mode',mode,'--model-key',MODEL_KEY,'--state-dir',str(state),'--run-id',rid]
|
| 313 |
+
if no_cache:args.append('--no-prefix-caching')
|
| 314 |
+
return start_attempts(args,env,startup,port,mode,no_cache)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def start_attempts(args,env,startup,port,mode='routed',no_cache=False,timeout=STARTUP_TIMEOUT):
|
| 318 |
+
"""auto first; TRITON_ATTN only if the engine fails to start. Each attempt has its own deadline."""
|
| 319 |
+
startup=Path(startup)
|
| 320 |
+
opener=urllib.request.build_opener(urllib.request.ProxyHandler({})) # readiness never uses a proxy
|
| 321 |
+
def event(attempt,backend,result,log=None):
|
| 322 |
+
# Best effort: a record that cannot be written or printed never blocks starting or stopping.
|
| 323 |
+
record=dict(attempt=attempt,requested_backend=backend or 'auto',result=result)
|
| 324 |
+
if log is not None:record['log']=str(log)
|
| 325 |
+
line=json.dumps(record)
|
| 326 |
+
try:
|
| 327 |
+
with (startup/'attempts.jsonl').open('a',encoding='utf-8',newline='\n') as out:
|
| 328 |
+
out.write(line+'\n')
|
| 329 |
+
except OSError:pass
|
| 330 |
+
try:print(line,flush=True)
|
| 331 |
+
except OSError:pass
|
| 332 |
+
for attempt,backend in enumerate(ATTEMPTS,1):
|
| 333 |
+
child_env=dict(env)
|
| 334 |
+
child_env.pop('VLLM_ATTENTION_BACKEND',None);child_env.pop('JEV_ATTENTION_BACKEND',None)
|
| 335 |
+
if backend:child_env['JEV_ATTENTION_BACKEND']=backend
|
| 336 |
+
log_path=startup/f'attempt-{attempt}.log'
|
| 337 |
+
child_env['JEV_ATTENTION_LOG']=str(log_path)
|
| 338 |
+
child_args=list(args)
|
| 339 |
+
# Separate quota state per attempt: a failed warmup never counts toward the served run.
|
| 340 |
+
child_args[child_args.index('--state-dir')+1]=str(startup/f'state-{attempt}')
|
| 341 |
+
event(attempt,backend,'starting',log_path)
|
| 342 |
+
proc=None
|
| 343 |
+
try:
|
| 344 |
+
with log_path.open('xb') as out:
|
| 345 |
+
proc=subprocess.Popen(child_args,cwd=ROOT,env=child_env,
|
| 346 |
+
stdout=out,stderr=subprocess.STDOUT,start_new_session=True)
|
| 347 |
+
deadline=time.monotonic()+timeout
|
| 348 |
+
while time.monotonic()<deadline:
|
| 349 |
+
logs=log_path.read_text(encoding='utf-8',errors='replace')
|
| 350 |
+
if 'EngineCore failed to start' in logs:
|
| 351 |
+
event(attempt,backend,'failed: EngineCore failed to start',log_path)
|
| 352 |
+
break
|
| 353 |
+
if proc.poll() is not None:
|
| 354 |
+
event(attempt,backend,'failed: exited before readiness',log_path)
|
| 355 |
+
break
|
| 356 |
+
try:
|
| 357 |
+
with opener.open(f'http://127.0.0.1:{port}/health',timeout=2) as response:
|
| 358 |
+
health=json.load(response)
|
| 359 |
+
ready=response.status==200 and health['mode']==mode and health['model_key']==MODEL_KEY
|
| 360 |
+
except (OSError,ValueError,KeyError):
|
| 361 |
+
ready=False
|
| 362 |
+
if ready:
|
| 363 |
+
# Recheck after the HTTP round trip; failure takes precedence.
|
| 364 |
+
if proc.poll() is not None or 'EngineCore failed to start' in log_path.read_text(encoding='utf-8',errors='replace'):
|
| 365 |
+
event(attempt,backend,'failed: engine stopped during readiness',log_path)
|
| 366 |
+
break
|
| 367 |
+
validate_health(health,no_cache)
|
| 368 |
+
event(attempt,backend,'ready',log_path)
|
| 369 |
+
print(json.dumps(health,ensure_ascii=False),flush=True)
|
| 370 |
+
return proc
|
| 371 |
+
time.sleep(1)
|
| 372 |
+
else:
|
| 373 |
+
raise TimeoutError(f'Health readiness timed out after {timeout} s; see {log_path}')
|
| 374 |
+
except BaseException as exc:
|
| 375 |
+
# Stop the server first; the record follows even if a stop request arrives meanwhile.
|
| 376 |
+
try:
|
| 377 |
+
if proc is not None:stop(proc)
|
| 378 |
+
finally:
|
| 379 |
+
event(attempt,backend,'aborted: '+type(exc).__name__,log_path)
|
| 380 |
+
raise
|
| 381 |
+
stop(proc)
|
| 382 |
+
raise RuntimeError(f'Both attention backend attempts (auto, TRITON_ATTN) failed before readiness; see {startup}')
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
KILL_WAIT=60
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@contextmanager
|
| 389 |
+
def cleanup_signals():
|
| 390 |
+
"""Hold SIGTERM, SIGINT and SIGHUP while child processes are being stopped.
|
| 391 |
+
|
| 392 |
+
A signal that arrives meanwhile is recorded, not acted on; once the previous handlers are
|
| 393 |
+
restored it is raised as KeyboardInterrupt, so a stop request is never lost.
|
| 394 |
+
"""
|
| 395 |
+
saved={};pending=[]
|
| 396 |
+
def hold(number,frame):pending.append(number)
|
| 397 |
+
try:
|
| 398 |
+
if threading.current_thread() is threading.main_thread():
|
| 399 |
+
for name in ('SIGTERM','SIGINT','SIGHUP'):
|
| 400 |
+
number=getattr(signal,name,None)
|
| 401 |
+
if number is not None:saved[number]=signal.signal(number,hold)
|
| 402 |
+
yield
|
| 403 |
+
finally:
|
| 404 |
+
for number,handler in saved.items():
|
| 405 |
+
if handler is not None:signal.signal(number,handler)
|
| 406 |
+
if pending:raise KeyboardInterrupt
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def stop(proc):
|
| 410 |
+
"""SIGTERM to the server's process group, up to 30 s, then SIGKILL to the group and a wait of
|
| 411 |
+
at most KILL_WAIT s; a signal during this sequence is raised afterwards (cleanup_signals)."""
|
| 412 |
+
with cleanup_signals():
|
| 413 |
+
try:os.killpg(proc.pid,signal.SIGTERM)
|
| 414 |
+
except ProcessLookupError:pass
|
| 415 |
+
try:
|
| 416 |
+
try:proc.wait(timeout=30)
|
| 417 |
+
except subprocess.TimeoutExpired:pass
|
| 418 |
+
finally:
|
| 419 |
+
# Reap descendants left in the process group before rebinding on retry.
|
| 420 |
+
try:os.killpg(proc.pid,signal.SIGKILL)
|
| 421 |
+
except ProcessLookupError:pass
|
| 422 |
+
try:proc.wait(timeout=KILL_WAIT)
|
| 423 |
+
except subprocess.TimeoutExpired:
|
| 424 |
+
try:print(f'launch: warning: process group {proc.pid} still running {KILL_WAIT} s after SIGKILL',
|
| 425 |
+
file=sys.stderr,flush=True)
|
| 426 |
+
except OSError:pass
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def main(argv=None):
|
| 430 |
+
ap=argparse.ArgumentParser(description=__doc__,formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 431 |
+
ap.add_argument('--model',required=True,help='absolute path of the model folder (read only)')
|
| 432 |
+
ap.add_argument('--state-dir',required=True,help='absolute path; the service writes only below it')
|
| 433 |
+
ap.add_argument('--run-id',help='new name for this run; default run-<UTC timestamp>')
|
| 434 |
+
ap.add_argument('--port',type=int,default=8000)
|
| 435 |
+
ap.add_argument('--bind',choices=BINDS,default='127.0.0.1')
|
| 436 |
+
ap.add_argument('--allow-nonformal',action='store_true',help='Local fitted engineering package only; never signs release')
|
| 437 |
+
a=ap.parse_args(argv)
|
| 438 |
+
def interrupted(*_):raise KeyboardInterrupt
|
| 439 |
+
for name in ('SIGTERM','SIGINT','SIGHUP'):
|
| 440 |
+
number=getattr(signal,name,None)
|
| 441 |
+
if number is not None:signal.signal(number,interrupted)
|
| 442 |
+
proc=None
|
| 443 |
+
try:
|
| 444 |
+
proc=start(a.model,a.port,mode=SERVED_MODE,state_dir=a.state_dir,run_id=a.run_id,bind=a.bind,allow_nonformal=a.allow_nonformal)
|
| 445 |
+
return proc.wait()
|
| 446 |
+
except KeyboardInterrupt:return 130
|
| 447 |
+
except (ValueError,RuntimeError,OSError) as exc:
|
| 448 |
+
print('launch: error: '+str(exc),file=sys.stderr,flush=True)
|
| 449 |
+
return 1
|
| 450 |
+
finally:
|
| 451 |
+
if proc:
|
| 452 |
+
# A stop request during this final stop is already being carried out.
|
| 453 |
+
try:stop(proc)
|
| 454 |
+
except KeyboardInterrupt:pass
|
| 455 |
+
|
| 456 |
+
if __name__=='__main__':raise SystemExit(main())
|
service/model_setup.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Report a missing processor config in the model folder; never downloads or copies."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# Gemma 4 keeps its processor settings in processor_config.json; the checkpoint has no
|
| 5 |
+
# preprocessor_config.json or video_preprocessor_config.json and needs neither.
|
| 6 |
+
NAMES=('processor_config.json',)
|
| 7 |
+
|
| 8 |
+
def report_processor_configs(model_dir):
|
| 9 |
+
"""Print nothing for a complete model folder; return the missing names."""
|
| 10 |
+
p=Path(model_dir)
|
| 11 |
+
missing=[n for n in NAMES if not (p/n).is_file()]
|
| 12 |
+
if missing:
|
| 13 |
+
print('[model_setup] processor config not in the model folder: '+', '.join(missing),flush=True)
|
| 14 |
+
return missing
|
service/pkg_check.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Offline package checks: integrity, model identity, configuration, adapter contract, serving
|
| 2 |
+
policy, calibration and disclosure items. No model is executed and nothing is downloaded.
|
| 3 |
+
|
| 4 |
+
A preparation package (config package.finalized false) and a frozen package
|
| 5 |
+
(finalized true plus FREEZE.json) are both checked. The freeze may change only
|
| 6 |
+
package.finalized in config.json and replace README.md, DISCLOSURE.md and
|
| 7 |
+
MODEL_CARD.md; every other file keeps the bytes of the prepared package. A Git
|
| 8 |
+
working copy of the package (a `.git` directory next to the files) is accepted.
|
| 9 |
+
|
| 10 |
+
SERVICE_POLICY (written by the builder from the selected serving parameters) states what this
|
| 11 |
+
package serves: the routing mode, threshold and quota, the drafting budget and its prompt limit, the
|
| 12 |
+
per-decision deadline and the item rules (types that may draft, low-margin choice re-read), the
|
| 13 |
+
post-calibration, the unparsed policy, the cost-fuse lines and prices and the yes/no confidence
|
| 14 |
+
constant of the base calibration. Every check below compares the configuration, calibration and fit
|
| 15 |
+
records with it, and the guards hold whatever it says.
|
| 16 |
+
"""
|
| 17 |
+
import argparse
|
| 18 |
+
import ast
|
| 19 |
+
import copy
|
| 20 |
+
import hashlib
|
| 21 |
+
import json
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
import re
|
| 24 |
+
|
| 25 |
+
ROOT=Path(__file__).resolve().parent
|
| 26 |
+
MODEL_KEY='metask-gemma4-31b-lora-w4a16'
|
| 27 |
+
MODEL_NAME='MetaDecision-31B-Rain'
|
| 28 |
+
ADAPTER_REF='model-subdir:adapter'
|
| 29 |
+
ADAPTER_SUBDIR='adapter'
|
| 30 |
+
ADAPTER_FILES=('adapter_config.json','adapter_model.safetensors')
|
| 31 |
+
TARGETS=('q_proj','k_proj','v_proj','o_proj','gate_proj','up_proj','down_proj')
|
| 32 |
+
LORA=dict(rank=16,alpha=32,target_modules=sorted(TARGETS))
|
| 33 |
+
PRICING_BASE='google/gemma-4-31B-it'
|
| 34 |
+
TRAINING_BASE='google/gemma-4-31B-it-qat-q4_0-unquantized'
|
| 35 |
+
TRAINING_REVISION='1e4d8beecacb8b7590c1d8bedd7335f687bf311f'
|
| 36 |
+
SERVED_BASE='google/gemma-4-31B-it-qat-w4a16-ct'
|
| 37 |
+
SERVED_REVISION='52f3f65bc7a02d555763bc923bd1d9094898219d'
|
| 38 |
+
SERVICE_POLICY={'protocol': 'adapter_arm_31b_v3', 'mode': 'routed', 'quota': 0.49, 'fuse': 0.49, 'margin_threshold': 6.625023671062799, 'fit_protocol': '31b-adapter-arm-v3-routed', 'fit_n': 231, 'base_fit': {'q': 0.056, 'draft': 256}, 'noul_p_cal': 0.898, 'draft': {'max_new_tokens': 192, 'reasoning_words': 50}, 'slow_max_prompt_tokens': 840, 'unparsed_policy': 'force_answer', 'slow_excluded_types': [], 'cost_fuse': {'arm': 0.9, 'enabled': True, 'release': 0.85, 'warmup': 100}, 'cost_fuse_prices': {'input_per_million': 0.059819178473256074, 'output_per_million': 0.059819178473256074}, 'item_deadline': {'item_seconds': 9.0, 'response_reserve_s': 0.35, 'readout_reserve_init_s': 1.0, 'readout_reserve_min_s': 0.3, 'readout_safety': 1.5, 'chunk_tokens': 32, 'min_chunk_tokens': 8, 'token_safety': 1.25, 'ewma_alpha': 0.3, 'warmup_tokens': 33, 'warmup_repeats': 3}, 'item_rules': {'slow_types': ['choice', 'noul', 'score'], 'reread_tau': 4.0}, 'post_calibration': {'choice': {'config': {'debias': False, 'temperature': False}, 'params': {}}, 'noul': {'config': {'temperature': 'type', 'floor': None}, 'params': {'T': 0.5}}, 'score': {'config': {'temperature': False, 'mode_weight': 1.0}, 'params': {}}}, 'post_calibration_active': True, 'core_patch': None, 'v3_params_sha256': 'b28ed73bb5a36e99a280a47a6b846e0c5b2de54f34a96efa4a10754b71ea7573', 'threshold_rule': 'eligible_items'}
|
| 39 |
+
INPUT_BUDGET=6653
|
| 40 |
+
MAX_MODEL_LEN=6656
|
| 41 |
+
GPU_MEMORY_UTILIZATION=0.94
|
| 42 |
+
MAX_LORA_RANK=16
|
| 43 |
+
PROTOCOL='adapter_arm_31b_v3'
|
| 44 |
+
FIT_PROTOCOLS={'fast_only':'31b-adapter-arm-v3-q0','routed':'31b-adapter-arm-v3-routed'}
|
| 45 |
+
# The reference cost fuse of the fast-only configuration; a routed package states its own lines in SERVICE_POLICY.
|
| 46 |
+
COST_FUSE={'enabled':True,'warmup':100,'arm':0.85,'release':0.8}
|
| 47 |
+
NOUL_P_CAL_RANGE=(0.81,0.95)
|
| 48 |
+
SLOW_READOUT_MARGIN=32
|
| 49 |
+
DRAFTING_WALL_TIMEOUT_SECONDS=60
|
| 50 |
+
SLOW_QUOTA_MAX=0.49
|
| 51 |
+
BASE_FUSE=0.2
|
| 52 |
+
ITEM_SECONDS_MAX=9.5
|
| 53 |
+
KINDS=('choice','noul','score')
|
| 54 |
+
SERVED_MODE=(SERVICE_POLICY or {}).get('mode')
|
| 55 |
+
REQUIRED=('core.py backends.py quota.py attention.py model_setup.py server.py bench.py item_deadline.py post_calibration.py '
|
| 56 |
+
'reconcile_usage.py launch.py serve.sh selftest.py selftest.sh pkg_check.py '
|
| 57 |
+
'prepare_model.sh config.json calibration.json MODEL_MANIFEST.json ADAPTER_MANIFEST.json '
|
| 58 |
+
'PIPELINE_PROVENANCE.json DISCLOSURE.md README.md MODEL_CARD.md '
|
| 59 |
+
'requirements.lock requirements.measured.lock LICENSE MANIFEST.sha256').split()
|
| 60 |
+
RUNTIME=('core.py','backends.py','quota.py','attention.py','model_setup.py','server.py','launch.py','item_deadline.py','post_calibration.py')
|
| 61 |
+
REFERENCE_CODE=('core.py','backends.py','quota.py','attention.py','model_setup.py','server.py','bench.py','reconcile_usage.py')
|
| 62 |
+
FREEZE_SCHEMA='metadecision_31b_freeze_v1'
|
| 63 |
+
FREEZE_DOCUMENTS=('README.md','DISCLOSURE.md','MODEL_CARD.md')
|
| 64 |
+
CONFIG_CHANGE='package.finalized: false -> true (single textual replacement)'
|
| 65 |
+
FROZEN_FLAG,PREPARED_FLAG=b'"finalized": true',b'"finalized": false'
|
| 66 |
+
TEXT_SUFFIXES=('.py','.md','.json','.sh','.lock','.txt')
|
| 67 |
+
def percent(value):
|
| 68 |
+
"""'6%' for 0.06, '2.5%' for 0.025, '6.25%' for 0.0625: the service's quota and fuse lines as written in the
|
| 69 |
+
documents, exact (never rounded)."""
|
| 70 |
+
from decimal import Decimal
|
| 71 |
+
text=format((Decimal(repr(float(value)))*100).normalize(),'f')
|
| 72 |
+
return (text.rstrip('0').rstrip('.') if '.' in text else text)+'%'
|
| 73 |
+
|
| 74 |
+
def policy_disclosure(policy):
|
| 75 |
+
"""Policy facts DISCLOSURE.md must state: the yes/no constant; routed: the drafting pass, its quota, budget,
|
| 76 |
+
prompt limit, 60 s limit and the cost-fuse lines."""
|
| 77 |
+
if not policy:return ()
|
| 78 |
+
items=('%.3f'%policy['noul_p_cal'],)
|
| 79 |
+
if policy['mode']!='routed':return items
|
| 80 |
+
pct=percent(policy['quota'])
|
| 81 |
+
items+=('drafting pass','slow-path quota '+pct,'at most '+pct+' of all decisions',
|
| 82 |
+
'at most %d tokens'%policy['draft']['max_new_tokens'],
|
| 83 |
+
'longer than {:,} tokens'.format(policy['slow_max_prompt_tokens']),
|
| 84 |
+
'deadline of %s s'%format(policy['item_deadline']['item_seconds'],'g'),'Cost fuse',
|
| 85 |
+
percent(policy['cost_fuse']['arm']),percent(policy['cost_fuse']['release']))
|
| 86 |
+
rules=policy.get('item_rules') or {}
|
| 87 |
+
if rules.get('reread_tau'):items+=('reverse order','below %s'%format(rules['reread_tau'],'g'))
|
| 88 |
+
if set(rules.get('slow_types') or KINDS)!=set(KINDS):items+=('only these question types',)
|
| 89 |
+
if policy.get('post_calibration_active'):items+=('Post-calibration',)
|
| 90 |
+
return items
|
| 91 |
+
|
| 92 |
+
# Necessary disclosure: base repo@revision, own weights, quantization, unmerged adapter,
|
| 93 |
+
# input budget and the cost/latency measurement conditions, plus the policy facts.
|
| 94 |
+
DISCLOSURE_REQUIRED=(PRICING_BASE,TRAINING_BASE,TRAINING_REVISION,SERVED_BASE,SERVED_REVISION,
|
| 95 |
+
'rank 16','alpha 32','W4A16','not merged','max_lora_rank=16','6,653',
|
| 96 |
+
'VLLM_BATCH_INVARIANT=1','engine token')+policy_disclosure(SERVICE_POLICY)
|
| 97 |
+
# README requirements: tested GPU and memory, the adapter route's vLLM options, the download tool, the mode.
|
| 98 |
+
README_REQUIRED=('NVIDIA A100 80GB','gpu_memory_utilization=0.94','max_model_len=6656','enable_lora',
|
| 99 |
+
'max_lora_rank=16','The model files take','`hf`','hf download')+(
|
| 100 |
+
('`"mode": "%s"`'%SERVED_MODE,) if SERVED_MODE else ())
|
| 101 |
+
|
| 102 |
+
def read(name):
|
| 103 |
+
return json.loads((ROOT/name).read_text(encoding='utf-8'))
|
| 104 |
+
|
| 105 |
+
def sha(path):
|
| 106 |
+
h=hashlib.sha256()
|
| 107 |
+
with path.open('rb') as f:
|
| 108 |
+
for block in iter(lambda:f.read(1<<20),b''):h.update(block)
|
| 109 |
+
return h.hexdigest()
|
| 110 |
+
|
| 111 |
+
def canonical(value):
|
| 112 |
+
return hashlib.sha256(json.dumps(value,ensure_ascii=False,sort_keys=True,separators=(',',':'),allow_nan=False).encode()).hexdigest()
|
| 113 |
+
|
| 114 |
+
def identity_sha256(m):
|
| 115 |
+
"""Identity of the model folder: official root files + the unmerged adapter/ files."""
|
| 116 |
+
identity={'format':m['format'],'files':m['files']}
|
| 117 |
+
if m.get('adapter') is not None:
|
| 118 |
+
a=m['adapter']
|
| 119 |
+
identity['adapter']={'format':a['format'],'subdir':a['subdir'],'files':a['files']}
|
| 120 |
+
return canonical(identity)
|
| 121 |
+
|
| 122 |
+
def check_manifest(root=ROOT):
|
| 123 |
+
expected={}
|
| 124 |
+
for line in (root/'MANIFEST.sha256').read_text(encoding='utf-8').splitlines():
|
| 125 |
+
digest,name=line.split(' ',1)
|
| 126 |
+
if not re.fullmatch('[0-9a-f]{64}',digest) or Path(name).name!=name or name in expected:
|
| 127 |
+
raise ValueError('Unsafe or duplicate manifest entry')
|
| 128 |
+
p=root/name
|
| 129 |
+
if p.is_symlink() or not p.is_file() or sha(p)!=digest:
|
| 130 |
+
raise ValueError('Missing, linked or changed package member: '+name)
|
| 131 |
+
expected[name]=digest
|
| 132 |
+
actual={p.name for p in root.iterdir() if p.is_file() and p.name!='MANIFEST.sha256'}
|
| 133 |
+
if actual!=set(expected):raise ValueError('Manifest membership differs')
|
| 134 |
+
for p in root.iterdir():
|
| 135 |
+
if p.is_symlink() or (p.is_dir() and p.name not in ('__pycache__','.git')):
|
| 136 |
+
raise ValueError('Unexpected package directory or symlink')
|
| 137 |
+
return expected
|
| 138 |
+
|
| 139 |
+
def front_matter(text):
|
| 140 |
+
"""Minimal YAML front-matter reader for top-level 'key: value' lines."""
|
| 141 |
+
lines=text.splitlines()
|
| 142 |
+
if not lines or lines[0].strip()!='---':raise ValueError('Model card lacks YAML front matter')
|
| 143 |
+
values={}
|
| 144 |
+
for line in lines[1:]:
|
| 145 |
+
if line.strip()=='---':return values
|
| 146 |
+
match=re.fullmatch(r'([A-Za-z_][A-Za-z0-9_]*):\s*(\S.*)?',line)
|
| 147 |
+
if match:values[match[1]]=(match[2] or '').strip()
|
| 148 |
+
raise ValueError('Unterminated model card front matter')
|
| 149 |
+
|
| 150 |
+
def check_adapter_contract(c,m):
|
| 151 |
+
adapter=m.get('adapter')
|
| 152 |
+
profiles=[(p.get('model') or {}) for p in c.get('profiles',{}).values()]
|
| 153 |
+
if (not isinstance(adapter,dict) or adapter.get('format')!='peft-lora' or adapter.get('subdir')!=ADAPTER_SUBDIR
|
| 154 |
+
or not isinstance(adapter.get('files'),dict)):
|
| 155 |
+
raise ValueError('The manifest must pin the unmerged adapter/ folder')
|
| 156 |
+
if adapter['files'] and (set(adapter['files'])!=set(ADAPTER_FILES) or adapter.get('merged') is not False
|
| 157 |
+
or adapter.get('rank')!=LORA['rank'] or adapter.get('alpha')!=LORA['alpha']
|
| 158 |
+
or adapter.get('target_modules')!=LORA['target_modules'] or adapter.get('variant') not in ('trained_original','key_value_aligned')
|
| 159 |
+
or adapter.get('training_base')!=TRAINING_BASE or adapter.get('training_revision')!=TRAINING_REVISION):
|
| 160 |
+
raise ValueError('Adapter manifest differs from the trained r16/alpha32 unmerged LoRA contract')
|
| 161 |
+
if c['model'].get('adapter')!=ADAPTER_REF or any('adapter' in p and p['adapter']!=ADAPTER_REF for p in profiles):
|
| 162 |
+
raise ValueError('Configuration must reference the model-relative adapter')
|
| 163 |
+
options=c['model'].get('vllm_options',{})
|
| 164 |
+
if options.get('max_lora_rank')!=MAX_LORA_RANK or 'enable_lora' in options:
|
| 165 |
+
raise ValueError('vLLM max_lora_rank=16 required; enable_lora is backend-owned')
|
| 166 |
+
record=read('ADAPTER_MANIFEST.json')
|
| 167 |
+
if (sha(ROOT/'ADAPTER_MANIFEST.json')!=adapter.get('adapter_manifest_sha256') or record.get('files')!=adapter['files']
|
| 168 |
+
or record.get('variant')!=adapter.get('variant') or (record.get('lora') or {}).get('merged') is not False
|
| 169 |
+
or (record.get('served_base') or {}).get('repo_id')!=SERVED_BASE
|
| 170 |
+
or (record.get('served_base') or {}).get('revision')!=SERVED_REVISION):
|
| 171 |
+
raise ValueError('ADAPTER_MANIFEST.json does not bind the pinned adapter files')
|
| 172 |
+
|
| 173 |
+
def policy_problems(policy):
|
| 174 |
+
"""Guards that hold whatever the builder wrote: mode, quota, yes/no constant, drafting budget and context."""
|
| 175 |
+
if not isinstance(policy,dict):return ['service policy pending']
|
| 176 |
+
problems=[]
|
| 177 |
+
p=policy.get('noul_p_cal')
|
| 178 |
+
if isinstance(p,bool) or not isinstance(p,float) or not NOUL_P_CAL_RANGE[0]<=p<=NOUL_P_CAL_RANGE[1]:
|
| 179 |
+
problems.append('yes/no constant outside [0.81, 0.95]')
|
| 180 |
+
if policy.get('protocol')!=PROTOCOL or policy.get('fit_protocol')!=FIT_PROTOCOLS.get(policy.get('mode')):
|
| 181 |
+
problems.append('protocol or fit protocol')
|
| 182 |
+
q=policy.get('quota')
|
| 183 |
+
if policy.get('mode')=='routed':
|
| 184 |
+
problems+=v4_policy_problems(policy)
|
| 185 |
+
if policy.get('mode')=='fast_only':
|
| 186 |
+
if q!=0 or policy.get('cost_fuse')!=COST_FUSE or policy.get('slow_excluded_types'):
|
| 187 |
+
problems.append('fast-only policy with a quota, other cost fuse or excluded types')
|
| 188 |
+
elif policy.get('mode')=='routed':
|
| 189 |
+
fuse,draft=policy.get('cost_fuse') or {},policy.get('draft') or {}
|
| 190 |
+
if isinstance(q,bool) or not isinstance(q,float) or not 0<q<=SLOW_QUOTA_MAX:problems.append('quota')
|
| 191 |
+
if (set(fuse)!={'enabled','warmup','arm','release'} or fuse['enabled'] is not True or fuse['warmup']!=100
|
| 192 |
+
or not 0.5<=fuse['release']<fuse['arm']<=0.9):problems.append('cost fuse lines')
|
| 193 |
+
length,prompt=draft.get('max_new_tokens'),policy.get('slow_max_prompt_tokens')
|
| 194 |
+
if (type(length) is not int or type(prompt) is not int or type(draft.get('reasoning_words')) is not int
|
| 195 |
+
or prompt+length+SLOW_READOUT_MARGIN>MAX_MODEL_LEN or prompt>INPUT_BUDGET):
|
| 196 |
+
problems.append('drafting budget outside the vLLM context')
|
| 197 |
+
if policy.get('unparsed_policy') not in ('fast','force_answer'):problems.append('unparsed policy')
|
| 198 |
+
if policy.get('slow_excluded_types') not in ([],['noul']):problems.append('excluded types')
|
| 199 |
+
if bool(policy.get('slow_excluded_types'))!=(policy.get('core_patch') is not None):
|
| 200 |
+
problems.append('excluded slow types need exactly the v3 core patch')
|
| 201 |
+
else:
|
| 202 |
+
problems.append('mode')
|
| 203 |
+
return problems
|
| 204 |
+
|
| 205 |
+
def v4_policy_problems(policy):
|
| 206 |
+
"""Fuse, cost-fuse prices, per-decision deadline, item rules and post-calibration of a routed package."""
|
| 207 |
+
problems=[]
|
| 208 |
+
q=policy.get('quota')
|
| 209 |
+
if not isinstance(q,float) or policy.get('fuse')!=max(BASE_FUSE,q):problems.append('fuse must be max(0.2, quota)')
|
| 210 |
+
t=policy.get('margin_threshold')
|
| 211 |
+
if isinstance(t,bool) or not isinstance(t,float) or not 0<=t<float('inf'):problems.append('margin threshold')
|
| 212 |
+
prices=policy.get('cost_fuse_prices') or {}
|
| 213 |
+
if (set(prices)!={'input_per_million','output_per_million'}
|
| 214 |
+
or any(isinstance(v,bool) or not isinstance(v,float) or not 0<v<float('inf') for v in prices.values())):
|
| 215 |
+
problems.append('cost-fuse prices')
|
| 216 |
+
try:
|
| 217 |
+
import item_deadline
|
| 218 |
+
s=item_deadline.settings_from(policy.get('item_deadline'))
|
| 219 |
+
if s!=policy.get('item_deadline') or not 0<s['item_seconds']<=ITEM_SECONDS_MAX:problems.append('item deadline')
|
| 220 |
+
except Exception:
|
| 221 |
+
problems.append('item deadline')
|
| 222 |
+
rules=policy.get('item_rules')
|
| 223 |
+
if (not isinstance(rules,dict) or set(rules)!={'slow_types','reread_tau'} or not isinstance(rules['slow_types'],list)
|
| 224 |
+
or not rules['slow_types'] or not set(rules['slow_types'])<=set(KINDS)
|
| 225 |
+
or len(set(rules['slow_types']))!=len(rules['slow_types'])
|
| 226 |
+
or isinstance(rules['reread_tau'],bool) or not isinstance(rules['reread_tau'],float)
|
| 227 |
+
or not 0<=rules['reread_tau']<float('inf')):
|
| 228 |
+
problems.append('item rules')
|
| 229 |
+
try:
|
| 230 |
+
import post_calibration
|
| 231 |
+
block=post_calibration.validate(policy.get('post_calibration'))
|
| 232 |
+
if bool(post_calibration.active(block))!=bool(policy.get('post_calibration_active')):problems.append('post-calibration flag')
|
| 233 |
+
except Exception:
|
| 234 |
+
problems.append('post-calibration')
|
| 235 |
+
return problems
|
| 236 |
+
|
| 237 |
+
def check_serving_policy(c):
|
| 238 |
+
"""SERVICE_POLICY's mode, quota, draft, prompt limit and cost fuse; 6653 input budget everywhere server.py can
|
| 239 |
+
apply it; 31B context/memory; the served configuration is server.py's fixed point."""
|
| 240 |
+
from core import apply_profile, apply_budget_tier
|
| 241 |
+
problems=policy_problems(SERVICE_POLICY)
|
| 242 |
+
if problems:raise ValueError('Service policy invalid: '+', '.join(problems))
|
| 243 |
+
policy=SERVICE_POLICY
|
| 244 |
+
r,limits,options,g=c['routing'],c['limits'],c['model'].get('vllm_options',{}),c['generation']
|
| 245 |
+
q,draft=policy['quota'],policy['draft']
|
| 246 |
+
if (r['mode']!=policy['mode'] or r['quota']!=q or r['target_slow_share']!=q or c['bench'].get('target_slow_share')!=q
|
| 247 |
+
or r['fuse']!=policy['fuse'] or c['usage']!={'basis':'engine'} or r.get('cost_fuse')!=policy['cost_fuse']):
|
| 248 |
+
raise ValueError('Changed serving mode, quota, usage or cost-fuse convention')
|
| 249 |
+
if (r['margin_threshold']!=policy['margin_threshold']
|
| 250 |
+
or c['pricing']['models'][c['pricing']['model_key']]!=policy['cost_fuse_prices']
|
| 251 |
+
or c.get('item_deadline')!=policy['item_deadline'] or c.get('item_rules')!=policy['item_rules']
|
| 252 |
+
or c.get('post_calibration')!=policy['post_calibration']):
|
| 253 |
+
raise ValueError('Changed threshold, cost-fuse prices, deadline, item rules or post-calibration')
|
| 254 |
+
if (r['margin_threshold']!=0 if policy['mode']=='fast_only' else not 0<r['margin_threshold']<float('inf')):
|
| 255 |
+
raise ValueError('Changed serving threshold')
|
| 256 |
+
if ((g['max_new_tokens'],g['reasoning_words'])!=(draft['max_new_tokens'],draft['reasoning_words'])
|
| 257 |
+
or (r['tiers']['hard'].get('max_new_tokens'),r['tiers']['hard'].get('reasoning_words'))!=(draft['max_new_tokens'],draft['reasoning_words'])
|
| 258 |
+
or r['slow_max_prompt_tokens']!=policy['slow_max_prompt_tokens'] or r['unparsed_policy']!=policy['unparsed_policy']
|
| 259 |
+
or r.get('slow_excluded_types',[])!=policy['slow_excluded_types']
|
| 260 |
+
or ('slow_excluded_types' in r and not r['slow_excluded_types'])):
|
| 261 |
+
raise ValueError('Changed drafting budget, prompt limit, unparsed policy or excluded types')
|
| 262 |
+
if policy['mode']=='routed' and (r.get('long_item_slow_policy')!='fast_only'
|
| 263 |
+
or g.get('wall_timeout_seconds')!=DRAFTING_WALL_TIMEOUT_SECONDS):
|
| 264 |
+
raise ValueError('Changed routed serving, drafting timeout or long-item policy')
|
| 265 |
+
if (limits['max_prompt_tokens'],limits['physical_max_prompt_tokens'])!=(INPUT_BUDGET,INPUT_BUDGET) \
|
| 266 |
+
or any(r['tiers'][t]['max_prompt_tokens']!=INPUT_BUDGET for t in ('normal','long')) \
|
| 267 |
+
or limits['oom_retry_prompt_tokens']>INPUT_BUDGET:
|
| 268 |
+
raise ValueError('Input budget must be 6653 for every prompt limit')
|
| 269 |
+
if options.get('max_model_len')!=MAX_MODEL_LEN or options.get('gpu_memory_utilization')!=GPU_MEMORY_UTILIZATION:
|
| 270 |
+
raise ValueError('vLLM context 6656 and GPU memory 0.94 required')
|
| 271 |
+
served=copy.deepcopy(c)
|
| 272 |
+
apply_profile(served)
|
| 273 |
+
apply_budget_tier(served,None)
|
| 274 |
+
served['routing']['mode']=SERVED_MODE
|
| 275 |
+
if served!=c:
|
| 276 |
+
raise ValueError('server.py would apply a different configuration than the measured one')
|
| 277 |
+
import core
|
| 278 |
+
if policy['slow_excluded_types'] and getattr(core,'SLOW_TYPE_EXCLUSION',None)!='routing.slow_excluded_types':
|
| 279 |
+
raise ValueError('Excluded slow types need the patched core.py')
|
| 280 |
+
|
| 281 |
+
def check_calibration_policy(cal):
|
| 282 |
+
"""The yes/no constant of SERVICE_POLICY in the fast, slow and fallback blocks; drafting-pass and fallback
|
| 283 |
+
answers use the first-pass temperatures (stated in DISCLOSURE.md)."""
|
| 284 |
+
blocks=cal['models'][MODEL_KEY]
|
| 285 |
+
p=SERVICE_POLICY['noul_p_cal']
|
| 286 |
+
for side in ('fast','slow','fallback'):
|
| 287 |
+
if blocks[side]['noul_calibration']['p_cal']!=p or blocks[side]['by_type']['noul'].get('p_cal')!=p:
|
| 288 |
+
raise ValueError('Calibration block does not carry the stated yes/no constant: '+side)
|
| 289 |
+
if blocks[side]['temperature_by_kind']!=blocks['fast']['temperature_by_kind']:
|
| 290 |
+
raise ValueError('DISCLOSURE.md states that drafting-pass answers use the first-pass calibration')
|
| 291 |
+
|
| 292 |
+
def prepared_config_bytes(data):
|
| 293 |
+
"""Undo the freeze's single textual change of config.json."""
|
| 294 |
+
if data.count(FROZEN_FLAG)!=1 or PREPARED_FLAG in data:
|
| 295 |
+
raise ValueError('Frozen config.json must differ from the prepared one only in package.finalized')
|
| 296 |
+
return data.replace(FROZEN_FLAG,PREPARED_FLAG)
|
| 297 |
+
|
| 298 |
+
def check_freeze(c,m,members):
|
| 299 |
+
"""None for a preparation package; the verified FREEZE.json record for a frozen one."""
|
| 300 |
+
path=ROOT/'FREEZE.json'
|
| 301 |
+
if not c['package'].get('finalized'):
|
| 302 |
+
if path.exists():raise ValueError('FREEZE.json in a package that is not finalized')
|
| 303 |
+
return None
|
| 304 |
+
if not path.is_file():raise ValueError('Finalized package without FREEZE.json')
|
| 305 |
+
f=read('FREEZE.json')
|
| 306 |
+
if f.get('schema')!=FREEZE_SCHEMA or f.get('status')!='FROZEN':
|
| 307 |
+
raise ValueError('Unknown freeze record')
|
| 308 |
+
if not c['package'].get('nonformal_fitted') or c['package'].get('sample_only'):
|
| 309 |
+
raise ValueError('Only the fitted package can be frozen')
|
| 310 |
+
if not re.fullmatch('[0-9a-f]{40}',str(m.get('revision') or '')) or m['repo_id'].startswith('OWNER/'):
|
| 311 |
+
raise ValueError('Frozen package lacks the own repository and full revision')
|
| 312 |
+
if (f.get('repo_id'),f.get('revision'),f.get('model_identity_sha256'))!=(m['repo_id'],m['revision'],m['identity_sha256']):
|
| 313 |
+
raise ValueError('Freeze record names another repository, revision or model')
|
| 314 |
+
prepared=f.get('prepared_files')
|
| 315 |
+
replaced=f.get('replaced_documents') or {}
|
| 316 |
+
if not isinstance(prepared,dict) or set(members)-{'FREEZE.json'}!=set(prepared) or set(replaced)-set(FREEZE_DOCUMENTS):
|
| 317 |
+
raise ValueError('Frozen package membership differs from the prepared package')
|
| 318 |
+
for name,digest in members.items():
|
| 319 |
+
if name=='FREEZE.json':continue
|
| 320 |
+
if name=='config.json':
|
| 321 |
+
before=hashlib.sha256(prepared_config_bytes((ROOT/name).read_bytes())).hexdigest()
|
| 322 |
+
if before!=prepared[name] or f.get('config')!={'prepared_sha256':before,'frozen_sha256':digest,'change':CONFIG_CHANGE}:
|
| 323 |
+
raise ValueError('Frozen config.json is not the recorded freeze of the prepared config')
|
| 324 |
+
elif name in replaced:
|
| 325 |
+
if replaced[name]!={'prepared_sha256':prepared[name],'frozen_sha256':digest}:
|
| 326 |
+
raise ValueError('Replaced document differs from the freeze record: '+name)
|
| 327 |
+
elif digest!=prepared[name]:
|
| 328 |
+
raise ValueError('Freeze changed a file other than config.json and the documents: '+name)
|
| 329 |
+
return f
|
| 330 |
+
|
| 331 |
+
def disclosure_problems(root=ROOT):
|
| 332 |
+
"""Necessary disclosure items that DISCLOSURE.md (and the model card base_model) must state."""
|
| 333 |
+
text=(root/'DISCLOSURE.md').read_text(encoding='utf-8',errors='replace')
|
| 334 |
+
problems=['DISCLOSURE.md lacks: '+item for item in DISCLOSURE_REQUIRED if item not in text]
|
| 335 |
+
readme=(root/'README.md').read_text(encoding='utf-8',errors='replace')
|
| 336 |
+
problems+=['README.md lacks: '+item for item in README_REQUIRED if item not in readme]
|
| 337 |
+
card=(root/'MODEL_CARD.md').read_text(encoding='utf-8',errors='replace')
|
| 338 |
+
try:
|
| 339 |
+
values=front_matter(card)
|
| 340 |
+
except ValueError as exc:
|
| 341 |
+
return problems+[str(exc)]
|
| 342 |
+
if values.get('base_model')!=PRICING_BASE:
|
| 343 |
+
problems.append('MODEL_CARD.md front matter base_model must be '+PRICING_BASE)
|
| 344 |
+
for item in (TRAINING_BASE,TRAINING_REVISION,SERVED_BASE,SERVED_REVISION):
|
| 345 |
+
if item not in card:problems.append('MODEL_CARD.md lacks: '+item)
|
| 346 |
+
return problems
|
| 347 |
+
|
| 348 |
+
def checks():
|
| 349 |
+
for name in REQUIRED:
|
| 350 |
+
if not (ROOT/name).is_file():raise ValueError('Missing '+name)
|
| 351 |
+
members=check_manifest()
|
| 352 |
+
for p in ROOT.glob('*.py'):ast.parse(p.read_text(encoding='utf-8'))
|
| 353 |
+
c,m,prov=read('config.json'),read('MODEL_MANIFEST.json'),read('PIPELINE_PROVENANCE.json')
|
| 354 |
+
from core import validate_config, validate_threshold, validate_calibration
|
| 355 |
+
import launch
|
| 356 |
+
validate_config(c)
|
| 357 |
+
if c['model']['model_key']!=MODEL_KEY or c['model']['name']!=MODEL_NAME or set(c['profiles'])!={MODEL_KEY}:
|
| 358 |
+
raise ValueError('Wrong trained model key')
|
| 359 |
+
if c['pipeline']['arm']!='trained' or prov.get('arm')!='trained' or m.get('arm')!='trained':
|
| 360 |
+
raise ValueError('Arm differs between config, manifest and provenance')
|
| 361 |
+
if c['pipeline']['model_identity_sha256']!=m['identity_sha256']:
|
| 362 |
+
raise ValueError('Model identity mismatch')
|
| 363 |
+
if m.get('revision') is not None and not re.fullmatch('[0-9a-f]{40}',m['revision']):
|
| 364 |
+
raise ValueError('Abbreviated/mutable revision rejected')
|
| 365 |
+
if m['repo_id'].startswith('google/'):
|
| 366 |
+
raise ValueError('Own HF repository required')
|
| 367 |
+
if (m.get('served_base')or{}).get('repo_id')!=SERVED_BASE or (m.get('served_base')or{}).get('revision')!=SERVED_REVISION:
|
| 368 |
+
raise ValueError('Served base must be the official W4A16 CT revision')
|
| 369 |
+
check_adapter_contract(c,m)
|
| 370 |
+
check_serving_policy(c)
|
| 371 |
+
frozen=check_freeze(c,m,members)
|
| 372 |
+
complete=bool(m['files']) and bool(m['adapter']['files'])
|
| 373 |
+
if complete and identity_sha256(m)!=m['identity_sha256']:raise ValueError('Manifest identity hash differs')
|
| 374 |
+
if not complete and m['identity_sha256'] is not None:raise ValueError('Incomplete model has an identity')
|
| 375 |
+
cal=read('calibration.json')
|
| 376 |
+
if set(cal['models'])!={MODEL_KEY}:raise ValueError('Foreign calibration model')
|
| 377 |
+
for side in ('fast','slow','fallback'):validate_calibration(cal,MODEL_KEY,side)
|
| 378 |
+
check_calibration_policy(cal)
|
| 379 |
+
fitted=c['package'].get('nonformal_fitted',False)
|
| 380 |
+
if fitted:
|
| 381 |
+
if validate_threshold(c):raise ValueError('Threshold identity differs from the configuration')
|
| 382 |
+
# The served configuration is the materialized one; its threshold identity must still hold.
|
| 383 |
+
if validate_threshold(launch.materialize_adapter(c,Path('/materialized/adapter'))):
|
| 384 |
+
raise ValueError('Materialized threshold identity differs')
|
| 385 |
+
lock=read('SERVICE_LOCK.json')
|
| 386 |
+
report=read('fit_report.json')
|
| 387 |
+
if (lock['protocol']!=prov.get('protocol') or lock['protocol']!=PROTOCOL
|
| 388 |
+
or lock['serving_identity_sha256']!=m['identity_sha256'] or lock['arm']!='trained'
|
| 389 |
+
or lock.get('adapter_files')!=m['adapter']['files'] or lock.get('q')!=SERVICE_POLICY['quota']):
|
| 390 |
+
raise ValueError('Fit lock identity mismatch')
|
| 391 |
+
actual={'config_file':frozen['config']['prepared_sha256'] if frozen else sha(ROOT/'config.json'),
|
| 392 |
+
'calibration':sha(ROOT/'calibration.json'),'fit_report':sha(ROOT/'fit_report.json')}
|
| 393 |
+
for stem,digest in actual.items():
|
| 394 |
+
if lock[stem+'_sha256']!=digest:raise ValueError('Fit hash mismatch')
|
| 395 |
+
base=SERVICE_POLICY['base_fit']
|
| 396 |
+
if (report.get('fit_protocol')!=SERVICE_POLICY['fit_protocol'] or report.get('n')!=SERVICE_POLICY['fit_n']
|
| 397 |
+
or report.get('q')!=base['q']
|
| 398 |
+
or (report.get('noul_constant') or {}).get('value')!=SERVICE_POLICY['noul_p_cal']
|
| 399 |
+
or report.get('draft')!=base['draft']):
|
| 400 |
+
raise ValueError('Wrong fitting method')
|
| 401 |
+
else:
|
| 402 |
+
if not c['package']['sample_only'] or not cal['models'][MODEL_KEY].get('placeholder'):
|
| 403 |
+
raise ValueError('Unfitted package must clearly carry placeholders')
|
| 404 |
+
if (c['package']['finalized'] and frozen is None) or prov.get('release_ready') or prov.get('official_submission_authorized'):
|
| 405 |
+
raise ValueError('Only the recorded freeze may finalize; the builder never authorizes a release')
|
| 406 |
+
for name in REFERENCE_CODE+('item_deadline.py','post_calibration.py'):
|
| 407 |
+
if sha(ROOT/name)!=prov['source_files'][name]:raise ValueError('Request code changed: '+name)
|
| 408 |
+
patch_record=prov.get('core_patch')
|
| 409 |
+
if patch_record!=SERVICE_POLICY.get('core_patch'):raise ValueError('Core patch record differs from the policy')
|
| 410 |
+
if patch_record is None:
|
| 411 |
+
if prov['source_files']['core.py']!=prov['reference_source_files']['core.py']:raise ValueError('Reference core.py changed')
|
| 412 |
+
else:
|
| 413 |
+
raise ValueError('This package serves the unchanged core.py')
|
| 414 |
+
server_patch=prov.get('server_patch') or {}
|
| 415 |
+
if (sha(ROOT/'server.py')!=server_patch.get('patched_server_sha256')
|
| 416 |
+
or prov['reference_source_files']['server.py']!=server_patch.get('reference_server_sha256')):
|
| 417 |
+
raise ValueError('server.py is not the declared extension patch of the reference server.py')
|
| 418 |
+
if any(prov['source_files'][n]!=prov['reference_source_files'][n] for n in REFERENCE_CODE if n not in ('core.py','server.py')):
|
| 419 |
+
raise ValueError('Reference request code changed')
|
| 420 |
+
if 'install_extensions(service, c)' not in (ROOT/'server.py').read_text(encoding='utf-8'):
|
| 421 |
+
raise ValueError('server.py does not install the serving extensions')
|
| 422 |
+
for name in RUNTIME:
|
| 423 |
+
text=(ROOT/name).read_text(encoding='utf-8')
|
| 424 |
+
if re.search(r'\brmtree\s*\(|\b(?:snapshot_download|hf_hub_download)\s*\(',text):
|
| 425 |
+
raise ValueError('Runtime downloads or recursive deletion found')
|
| 426 |
+
launch_text=(ROOT/'launch.py').read_text(encoding='utf-8')
|
| 427 |
+
for token in ("HF_HUB_OFFLINE='1'","TRANSFORMERS_OFFLINE='1'","env['VLLM_BATCH_INVARIANT']='1'","env['VLLM_WORKER_MULTIPROC_METHOD']='spawn'",
|
| 428 |
+
"VLLM_USE_FLASHINFER_SAMPLER='0'","def check_adapter(","def materialize_adapter(",
|
| 429 |
+
" check_adapter(model)\n","cfg=write_runtime_config(startup,runtime)","config.runtime.json",
|
| 430 |
+
"SERVED_MODE='%s'"%SERVED_MODE,"mode=SERVED_MODE","(ROOT/'FREEZE.json').is_file()"):
|
| 431 |
+
if token not in launch_text:raise ValueError('Offline/invariant/adapter/mode/freeze startup setting missing: '+token.strip())
|
| 432 |
+
if launch.ADAPTER_REF!=ADAPTER_REF or launch.MODEL_KEY!=MODEL_KEY or launch.SERVED_MODE!=c['routing']['mode']:
|
| 433 |
+
raise ValueError('Launcher adapter reference, model key or mode differs')
|
| 434 |
+
for p in ROOT.glob('*.sh'):
|
| 435 |
+
if re.search(r'\brm\s+-[^\n]*r|\brmtree\b',p.read_text(encoding='utf-8')):
|
| 436 |
+
raise ValueError('Recursive deletion in shell')
|
| 437 |
+
serve=(ROOT/'serve.sh').read_text(encoding='utf-8')
|
| 438 |
+
if 'VLLM_USE_FLASHINFER_SAMPLER=0' not in serve or 'VLLM_BATCH_INVARIANT=1' not in serve or 'VLLM_WORKER_MULTIPROC_METHOD=spawn' not in serve:
|
| 439 |
+
raise ValueError('serve.sh must set VLLM_USE_FLASHINFER_SAMPLER=0, VLLM_BATCH_INVARIANT=1 and VLLM_WORKER_MULTIPROC_METHOD=spawn')
|
| 440 |
+
problems=disclosure_problems()
|
| 441 |
+
if problems:raise ValueError('Necessary disclosure incomplete: '+'; '.join(problems))
|
| 442 |
+
pins=dict(line.split('==',1) for line in (ROOT/'requirements.lock').read_text().splitlines() if '==' in line and not line.startswith('#'))
|
| 443 |
+
for name,version in launch.VERSIONS.items():
|
| 444 |
+
if pins.get(name,'').split('+')[0]!=version:raise ValueError('Runtime version pin mismatch')
|
| 445 |
+
return {'status':'FROZEN_PACKAGE_CHECK_PASS' if frozen else 'PREPARATION_CHECK_PASS','model_execution':False,'network':False,
|
| 446 |
+
'adapter':'unmerged adapter/ ('+m['adapter'].get('variant','pending')+')','served_mode':SERVED_MODE,
|
| 447 |
+
'input_budget':INPUT_BUDGET,'service_fit':'frozen' if frozen else 'fitted_not_frozen' if fitted else 'pending',
|
| 448 |
+
'serving_extensions':['item_deadline','item_rules','post_calibration']}
|
| 449 |
+
|
| 450 |
+
def main():
|
| 451 |
+
argparse.ArgumentParser(description=__doc__).parse_args()
|
| 452 |
+
print(json.dumps(checks(),ensure_ascii=False,indent=2))
|
| 453 |
+
return 0
|
| 454 |
+
|
| 455 |
+
if __name__=='__main__':raise SystemExit(main())
|
service/post_calibration.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-type post-calibration of the reported probabilities (no model call, no tokens).
|
| 2 |
+
|
| 3 |
+
server.py installs it when config.json has a `post_calibration` block. For each question type the
|
| 4 |
+
block holds the options chosen on the service's own fitting data and their fitted parameters:
|
| 5 |
+
choice: debias (divide out the mean probability of each option position, per option count, then
|
| 6 |
+
renormalise), then a temperature;
|
| 7 |
+
noul: a temperature, then a decision-side floor (P(yes) is moved to at least `floor` on the
|
| 8 |
+
side it already favours, or to at most 1 - floor);
|
| 9 |
+
score: a temperature, then a reading weight w (probability mass w moved onto the most probable
|
| 10 |
+
level; w = 1 puts all mass on it).
|
| 11 |
+
The temperature option is off, one value per type ("type"), or one value per type and route cell
|
| 12 |
+
("route": fast, slow, slow_timeout_fast, fast_reread; a cell without its own value uses the type
|
| 13 |
+
value). A temperature t replaces the probabilities p by p^(1/t), renormalised. A type without an
|
| 14 |
+
entry, or with every option off, and a question with a single option or level, are returned exactly
|
| 15 |
+
as the service computed them. The answer label (`choice`, `noul`, the prediction) is recomputed from
|
| 16 |
+
the calibrated probabilities.
|
| 17 |
+
"""
|
| 18 |
+
import math
|
| 19 |
+
|
| 20 |
+
KINDS = ('choice', 'noul', 'score')
|
| 21 |
+
OPTIONS = {'choice': ('debias', 'temperature'), 'noul': ('temperature', 'floor'),
|
| 22 |
+
'score': ('temperature', 'mode_weight')}
|
| 23 |
+
ROUTE_BUCKETS = ('fast', 'slow', 'slow_timeout_fast', 'fast_reread')
|
| 24 |
+
TEMPERATURE_MODES = (False, True, 'type', 'route')
|
| 25 |
+
T_MIN, T_MAX = 0.5, 8.0
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def need(ok, code):
|
| 29 |
+
if not ok:
|
| 30 |
+
raise RuntimeError(code)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def finite(x):
|
| 34 |
+
return isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ---------------------------------------------------------------- transforms
|
| 38 |
+
def distribution(p):
|
| 39 |
+
need(len(p) >= 2 and all(type(v) in (float, int) and math.isfinite(v) and 0 <= v <= 1 for v in p), 'BAD_PROBS')
|
| 40 |
+
need(abs(sum(p)-1) < 1e-6, 'BAD_PROB_SUM')
|
| 41 |
+
return list(p)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def softmax(scores):
|
| 45 |
+
need(scores and all(math.isfinite(x) for x in scores), 'BAD_LOGITS')
|
| 46 |
+
z = max(scores)
|
| 47 |
+
w = [math.exp(x-z) for x in scores]
|
| 48 |
+
return [x/sum(w) for x in w]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def temperature(p, t):
|
| 52 |
+
distribution(p)
|
| 53 |
+
need(math.isfinite(t) and t >= .5, 'BAD_TEMPERATURE')
|
| 54 |
+
return list(p) if t == 1 else softmax([math.log(max(x, 1e-300))/t for x in p])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _argmax(p):
|
| 58 |
+
return max(range(len(p)), key=p.__getitem__)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def debias(p, prior):
|
| 62 |
+
"""Option-position prior divided out, renormalised."""
|
| 63 |
+
need(len(prior) == len(p) and all(w > 0 for w in prior), 'CAL_PRIOR')
|
| 64 |
+
q = [x / w for x, w in zip(p, prior)]
|
| 65 |
+
s = sum(q)
|
| 66 |
+
return [x / s for x in q]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def noul_floor(p, floor):
|
| 70 |
+
"""Labels no/yes: P(yes) is committed to >= floor on the favoured side (<= 1 - floor on the other)."""
|
| 71 |
+
need(len(p) == 2 and .8 < floor < 1, 'CAL_NOUL_FLOOR')
|
| 72 |
+
if p[1] >= floor or p[1] <= 1 - floor:
|
| 73 |
+
return list(p)
|
| 74 |
+
y = floor if p[1] >= .5 else 1 - floor
|
| 75 |
+
return [1 - y, y]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def score_mode(p, weight):
|
| 79 |
+
"""Weight 0 keeps the distribution; weight 1 puts all mass on the most probable level."""
|
| 80 |
+
need(0 <= weight <= 1, 'CAL_SCORE_WEIGHT')
|
| 81 |
+
if weight == 0:
|
| 82 |
+
return list(p)
|
| 83 |
+
m = _argmax(p)
|
| 84 |
+
return [(1 - weight) * x + (weight if i == m else 0.) for i, x in enumerate(p)]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def n_active(cfg):
|
| 88 |
+
return sum(bool(v) for v in cfg.values())
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def apply_one(kind, cfg, params, p, bucket='fast'):
|
| 92 |
+
q = list(p)
|
| 93 |
+
if cfg.get('debias') and str(len(q)) in params.get('prior', {}):
|
| 94 |
+
q = debias(q, params['prior'][str(len(q))])
|
| 95 |
+
mode = cfg.get('temperature')
|
| 96 |
+
if mode:
|
| 97 |
+
need(mode in (True, 'type', 'route'), 'CAL_TEMPERATURE_MODE')
|
| 98 |
+
t = params['route_T'].get(bucket, params['T']) if mode == 'route' else params['T']
|
| 99 |
+
q = temperature(q, t)
|
| 100 |
+
if kind == 'noul' and cfg.get('floor'):
|
| 101 |
+
q = noul_floor(q, cfg['floor'])
|
| 102 |
+
if kind == 'score' and cfg.get('mode_weight'):
|
| 103 |
+
q = score_mode(q, cfg['mode_weight'])
|
| 104 |
+
return distribution(q)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def route_bucket(detail):
|
| 108 |
+
"""Route cell of one answered decision, from the service's own per-decision record:
|
| 109 |
+
slow_timeout_fast = first-pass answer served after an admitted drafting pass (deadline, failure or
|
| 110 |
+
unparsed draft; the 'fallback' calibration block); slow = answer read after the drafting pass
|
| 111 |
+
(appended answer line included); fast_reread = one-pass answer with the reversed-order re-read
|
| 112 |
+
averaged in; fast = everything else."""
|
| 113 |
+
if detail.get('route') in ('slow_timeout_fast', 'slow_failed_fast', 'slow_unparsed_fast') \
|
| 114 |
+
or detail.get('calibration_block') == 'fallback':
|
| 115 |
+
return 'slow_timeout_fast'
|
| 116 |
+
if detail.get('path') == 'slow' or detail.get('calibration_block') == 'slow' \
|
| 117 |
+
or detail.get('route') in ('slow', 'slow_forced_answer'):
|
| 118 |
+
return 'slow'
|
| 119 |
+
if isinstance(detail.get('reread'), dict):
|
| 120 |
+
return 'fast_reread'
|
| 121 |
+
return 'fast'
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ---------------------------------------------------------------- configuration
|
| 125 |
+
def _temperature_value(t):
|
| 126 |
+
return finite(t) and T_MIN <= t <= T_MAX
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def validate(block):
|
| 130 |
+
"""The config.json block: {type: {"config": {option: value},
|
| 131 |
+
"params": {"T": float, "route_T": {cell: float}, "route_counts": {cell: int}, "prior": {...}}}}."""
|
| 132 |
+
need(isinstance(block, dict) and set(block) <= set(KINDS), 'POSTCAL_TYPES')
|
| 133 |
+
for kind, entry in block.items():
|
| 134 |
+
need(isinstance(entry, dict) and set(entry) == {'config', 'params'}, 'POSTCAL_ENTRY')
|
| 135 |
+
cfg, params = entry['config'], entry['params']
|
| 136 |
+
need(isinstance(cfg, dict) and set(cfg) <= set(OPTIONS[kind]), 'POSTCAL_OPTIONS')
|
| 137 |
+
need(isinstance(params, dict) and set(params) <= {'T', 'prior', 'route_T', 'route_counts'}, 'POSTCAL_PARAMS')
|
| 138 |
+
need('debias' not in cfg or type(cfg['debias']) is bool, 'POSTCAL_SWITCH')
|
| 139 |
+
mode = cfg.get('temperature', False)
|
| 140 |
+
need(any(mode is m or (isinstance(m, str) and mode == m) for m in TEMPERATURE_MODES), 'POSTCAL_TEMPERATURE_MODE')
|
| 141 |
+
if mode:
|
| 142 |
+
need(_temperature_value(params.get('T')), 'POSTCAL_TEMPERATURE')
|
| 143 |
+
if mode == 'route':
|
| 144 |
+
cells = params.get('route_T')
|
| 145 |
+
need(isinstance(cells, dict) and set(cells) <= set(ROUTE_BUCKETS)
|
| 146 |
+
and all(_temperature_value(v) for v in cells.values()), 'POSTCAL_ROUTE_TEMPERATURE')
|
| 147 |
+
else:
|
| 148 |
+
need('route_T' not in params, 'POSTCAL_ROUTE_TEMPERATURE_UNUSED')
|
| 149 |
+
counts = params.get('route_counts')
|
| 150 |
+
need(counts is None or (isinstance(counts, dict) and set(counts) <= set(ROUTE_BUCKETS)
|
| 151 |
+
and all(type(v) is int and v >= 0 for v in counts.values())), 'POSTCAL_ROUTE_COUNTS')
|
| 152 |
+
if cfg.get('debias'):
|
| 153 |
+
prior = params.get('prior')
|
| 154 |
+
need(isinstance(prior, dict) and all(
|
| 155 |
+
k.isdigit() and int(k) >= 2 and isinstance(v, list) and len(v) == int(k)
|
| 156 |
+
and all(finite(w) and w > 0 for w in v) for k, v in prior.items()), 'POSTCAL_PRIOR')
|
| 157 |
+
if cfg.get('floor') is not None:
|
| 158 |
+
need(finite(cfg['floor']) and .8 < cfg['floor'] < 1, 'POSTCAL_FLOOR')
|
| 159 |
+
if 'mode_weight' in cfg:
|
| 160 |
+
need(finite(cfg['mode_weight']) and 0 <= cfg['mode_weight'] <= 1, 'POSTCAL_MODE_WEIGHT')
|
| 161 |
+
return block
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def active(block):
|
| 165 |
+
"""{type: [active option names]} for the types this block changes."""
|
| 166 |
+
return {kind: sorted(k for k, v in entry['config'].items() if v)
|
| 167 |
+
for kind, entry in sorted(block.items()) if n_active(entry['config'])}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ---------------------------------------------------------------- service hook
|
| 171 |
+
def wrap_service(service, block, core):
|
| 172 |
+
"""Calibrate each answer after the service computed it (same order as the measured setup:
|
| 173 |
+
after the deadline wrapper and its item rules, before the answer is serialised)."""
|
| 174 |
+
policy = validate(block)
|
| 175 |
+
need(not getattr(service, '_post_calibration_wrapped', False), 'POSTCAL_ALREADY_WRAPPED')
|
| 176 |
+
original = service._one_impl
|
| 177 |
+
|
| 178 |
+
def one(task):
|
| 179 |
+
answer, detail = original(task)
|
| 180 |
+
kind = task['question']['type']
|
| 181 |
+
entry = policy.get(kind)
|
| 182 |
+
if entry is None or not n_active(entry['config']):
|
| 183 |
+
return answer, detail
|
| 184 |
+
labels = core.render(task)[0]
|
| 185 |
+
if len(labels) < 2:
|
| 186 |
+
return answer, detail # one option or level: nothing to calibrate
|
| 187 |
+
probs = answer['probabilities']
|
| 188 |
+
if set(probs) == set(labels):
|
| 189 |
+
keys = list(labels)
|
| 190 |
+
elif kind == 'noul' and set(probs) == {'false', 'true'}:
|
| 191 |
+
keys = ['false', 'true'] # the service's no/yes order
|
| 192 |
+
else:
|
| 193 |
+
raise core.ItemFailure('post-calibration label mismatch')
|
| 194 |
+
bucket = route_bucket(detail)
|
| 195 |
+
try:
|
| 196 |
+
q = apply_one(kind, entry['config'], entry['params'], [probs[k] for k in keys], bucket)
|
| 197 |
+
except (RuntimeError, ValueError, ZeroDivisionError, OverflowError) as exc:
|
| 198 |
+
raise core.ItemFailure('post-calibration failed') from exc
|
| 199 |
+
calibrated = dict(zip(keys, q))
|
| 200 |
+
answer['probabilities'] = {k: calibrated[k] for k in probs}
|
| 201 |
+
by_label = dict(zip(labels, q))
|
| 202 |
+
detail['prediction'] = max(labels, key=by_label.get)
|
| 203 |
+
if kind == 'noul':
|
| 204 |
+
answer['noul'] = float(calibrated.get('yes', calibrated.get('true')))
|
| 205 |
+
elif kind == 'choice':
|
| 206 |
+
answer['choice'] = detail['prediction']
|
| 207 |
+
detail['post_calibration'] = dict(options=sorted(k for k, v in entry['config'].items() if v), route=bucket)
|
| 208 |
+
return answer, detail
|
| 209 |
+
|
| 210 |
+
service._one_impl = one
|
| 211 |
+
service._post_calibration_wrapped = True
|
| 212 |
+
return service
|
service/prepare_model.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# NETWORKED maintainer preparation, never called by serve.sh.
|
| 3 |
+
# Downloads the pinned revision: the official base files at the folder root and the unmerged LoRA in adapter/.
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
PKG=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
| 6 |
+
exec "${PYTHON:-python3}" -B - "$PKG" "${1:?new absolute external model directory}" <<'PY'
|
| 7 |
+
import json,re,sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
pkg=Path(sys.argv[1]).resolve(); dest=Path(sys.argv[2])
|
| 10 |
+
m=json.loads((pkg/'MODEL_MANIFEST.json').read_text())
|
| 11 |
+
if not dest.is_absolute():raise SystemExit('Use an absolute destination')
|
| 12 |
+
dest=dest.resolve()
|
| 13 |
+
if dest.exists() or dest.is_relative_to(pkg) or pkg.is_relative_to(dest):raise SystemExit('Use a new separate model directory')
|
| 14 |
+
if m['repo_id'].startswith('OWNER/') or not re.fullmatch('[0-9a-f]{40}',str(m.get('revision') or '')):
|
| 15 |
+
raise SystemExit('Set the own Hugging Face repository and a complete 40-character revision in MODEL_MANIFEST.json')
|
| 16 |
+
if not m['files'] or not m['adapter']['files']:raise SystemExit('Model hash manifest is pending')
|
| 17 |
+
patterns=[*m['files'],*(m['adapter']['subdir']+'/'+name for name in m['adapter']['files'])]
|
| 18 |
+
from huggingface_hub import snapshot_download
|
| 19 |
+
snapshot_download(repo_id=m['repo_id'],revision=m['revision'],local_dir=dest,allow_patterns=patterns)
|
| 20 |
+
sys.path.insert(0,str(pkg))
|
| 21 |
+
from launch import check_model
|
| 22 |
+
check_model(dest,full=True)
|
| 23 |
+
print('Downloaded base and adapter files match the pinned manifest; no inference or submission performed.')
|
| 24 |
+
PY
|
service/quota.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Durable, conservative per-decision slow-path quota.
|
| 2 |
+
|
| 3 |
+
The lock spans inference: a slow attempt is charged before work starts, while
|
| 4 |
+
the denominator grows only after successful work. A crash therefore cannot
|
| 5 |
+
create quota credit. One counter file represents one global service history.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from contextlib import contextmanager
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from decimal import Decimal
|
| 12 |
+
import errno
|
| 13 |
+
import json
|
| 14 |
+
import math
|
| 15 |
+
import os
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
import tempfile
|
| 18 |
+
import threading
|
| 19 |
+
import time
|
| 20 |
+
from typing import Iterator
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class QuotaStateError(RuntimeError):
|
| 24 |
+
"""Counter storage is unreadable or invalid; inference must fail closed."""
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class QuotaDecision:
|
| 29 |
+
path: str
|
| 30 |
+
reason: str
|
| 31 |
+
snapshot: dict
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class _PathLock:
|
| 35 |
+
def __init__(self) -> None:
|
| 36 |
+
self.mutex = threading.RLock()
|
| 37 |
+
self.local = threading.local()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
_REGISTRY_MUTEX = threading.Lock()
|
| 41 |
+
_PATH_LOCKS: dict[str, _PathLock] = {}
|
| 42 |
+
_ROOT = Path(__file__).resolve().parent
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@contextmanager
|
| 46 |
+
def _os_lock(path: Path) -> Iterator[None]:
|
| 47 |
+
# Keep the lock file permanently: unlinking it would let another process
|
| 48 |
+
# lock a different inode while an existing process still holds this lock.
|
| 49 |
+
with path.open("a+b") as handle:
|
| 50 |
+
if os.name == "nt":
|
| 51 |
+
import msvcrt
|
| 52 |
+
|
| 53 |
+
handle.seek(0, os.SEEK_END)
|
| 54 |
+
if handle.tell() == 0:
|
| 55 |
+
handle.write(b"\0")
|
| 56 |
+
handle.flush()
|
| 57 |
+
while True:
|
| 58 |
+
handle.seek(0)
|
| 59 |
+
try:
|
| 60 |
+
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
| 61 |
+
break
|
| 62 |
+
except OSError as exc:
|
| 63 |
+
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
|
| 64 |
+
raise
|
| 65 |
+
time.sleep(0.05)
|
| 66 |
+
try:
|
| 67 |
+
yield
|
| 68 |
+
finally:
|
| 69 |
+
handle.seek(0)
|
| 70 |
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
| 71 |
+
else:
|
| 72 |
+
import fcntl
|
| 73 |
+
|
| 74 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
| 75 |
+
try:
|
| 76 |
+
yield
|
| 77 |
+
finally:
|
| 78 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _fsync_directory(path: Path) -> None:
|
| 82 |
+
if os.name == "nt":
|
| 83 |
+
return # Windows does not expose a directory fsync via os.open.
|
| 84 |
+
descriptor = os.open(str(path), os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
| 85 |
+
try:
|
| 86 |
+
try:
|
| 87 |
+
os.fsync(descriptor)
|
| 88 |
+
except OSError as exc:
|
| 89 |
+
if exc.errno not in (errno.EINVAL, errno.ENOTSUP):
|
| 90 |
+
raise
|
| 91 |
+
finally:
|
| 92 |
+
os.close(descriptor)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def cost_settings(config):
|
| 96 |
+
settings = config['routing'].get('cost_fuse', {'enabled': False})
|
| 97 |
+
prices = config['pricing']['models'][config['pricing']['model_key']]
|
| 98 |
+
known = all(isinstance(prices.get(k), (int, float)) and not isinstance(prices[k], bool)
|
| 99 |
+
and math.isfinite(prices[k]) and prices[k] >= 0
|
| 100 |
+
for k in ('input_per_million', 'output_per_million'))
|
| 101 |
+
if settings.get('enabled', True) and not known and config['routing']['mode'] == 'routed':
|
| 102 |
+
raise ValueError('Routed cost fuse requires known profile prices; explicitly use --cost-fuse off')
|
| 103 |
+
return settings, prices, known
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class CostFuse:
|
| 107 |
+
def __init__(self, limit_per_1000, *, warmup=100, arm=.85, release=.80, state=None, enabled=True):
|
| 108 |
+
if not math.isfinite(limit_per_1000) or limit_per_1000 <= 0 or not 0 < release < arm <= 1:
|
| 109 |
+
raise ValueError('Invalid cost fuse limits')
|
| 110 |
+
if type(warmup) is not int or warmup < 100: raise ValueError('Cost warmup must be at least 100')
|
| 111 |
+
self.limit = limit_per_1000 / 1000
|
| 112 |
+
self.warmup, self.arm, self.release, self.enabled = warmup, arm, release, enabled
|
| 113 |
+
self.s = dict(n=0, cost=0., n_slow=0, cost_slow=0., engaged=False, transitions=0) if state is None else dict(state)
|
| 114 |
+
v=self.s
|
| 115 |
+
if (any(type(v.get(k)) is not int or v[k]<0 for k in ('n','n_slow','transitions'))
|
| 116 |
+
or v['n_slow']>v['n'] or type(v.get('engaged')) is not bool
|
| 117 |
+
or any(not isinstance(v.get(k),(int,float)) or not math.isfinite(v[k]) or v[k]<0 for k in ('cost','cost_slow'))
|
| 118 |
+
or v['cost_slow']>v['cost']+1e-12): raise QuotaStateError('Invalid cost fuse state')
|
| 119 |
+
|
| 120 |
+
def state(self):
|
| 121 |
+
return 'off' if not self.enabled else 'observe' if self.s['n'] < self.warmup else 'engaged' if self.s['engaged'] else 'idle'
|
| 122 |
+
|
| 123 |
+
def quota(self, tier_quota):
|
| 124 |
+
s=self.s
|
| 125 |
+
if self.state()!='engaged': return tier_quota,'tier'
|
| 126 |
+
n_fast=s['n']-s['n_slow']
|
| 127 |
+
fast=(s['cost']-s['cost_slow'])/n_fast if n_fast else 0.
|
| 128 |
+
slow=s['cost_slow']/s['n_slow'] if s['n_slow'] else None
|
| 129 |
+
# If even fast exceeds target, remove slow admissions; never alter fast.
|
| 130 |
+
q=0. if fast>=self.release*self.limit else (
|
| 131 |
+
max(0.,(self.release*self.limit-fast)/(slow-fast)) if slow is not None and slow>fast else tier_quota)
|
| 132 |
+
return (q,'cost') if q<tier_quota else (tier_quota,'tier')
|
| 133 |
+
|
| 134 |
+
def record(self, cost_usd, slow):
|
| 135 |
+
if not math.isfinite(cost_usd) or cost_usd<0: raise ValueError('Invalid observed cost')
|
| 136 |
+
s=self.s;s['n']+=1;s['cost']+=cost_usd
|
| 137 |
+
if slow:s['n_slow']+=1;s['cost_slow']+=cost_usd
|
| 138 |
+
if not self.enabled or s['n']<self.warmup:return
|
| 139 |
+
running=s['cost']/s['n'];old=s['engaged']
|
| 140 |
+
if not old and running>=self.arm*self.limit:s['engaged']=True
|
| 141 |
+
elif old and running<=self.release*self.limit:s['engaged']=False
|
| 142 |
+
if old!=s['engaged']:s['transitions']+=1
|
| 143 |
+
|
| 144 |
+
def snapshot(self, quota):
|
| 145 |
+
q,source=self.quota(quota)
|
| 146 |
+
return dict(state=self.state(),n=self.s['n'],running_cost_per_1000=self.s['cost']/self.s['n']*1000 if self.s['n'] else 0.,
|
| 147 |
+
effective_quota=q,quota_source=source,transitions=self.s['transitions'],cap_per_1000=self.limit*1000)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def configure_cost(counter, config):
|
| 151 |
+
settings,prices,known=cost_settings(config)
|
| 152 |
+
counter.cost_prices=prices if known else None
|
| 153 |
+
counter.cost_options=dict(limit_per_1000=config['limits']['cost_per_1000_decisions_usd']*(1-config['pricing']['margin']),
|
| 154 |
+
warmup=settings.get('warmup',100),arm=settings.get('arm',.85),release=settings.get('release',.80),
|
| 155 |
+
enabled=settings.get('enabled',True) and known and config['routing']['mode']=='routed')
|
| 156 |
+
counter.cost_fuse=CostFuse(**counter.cost_options)
|
| 157 |
+
counter.tier_quota=config['routing']['quota']
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class QuotaCounter:
|
| 161 |
+
"""Serialize decisions across threads/processes sharing ``state_path``.
|
| 162 |
+
|
| 163 |
+
``quota`` and ``fuse`` must come from the caller's configuration. Uniform
|
| 164 |
+
experiments can explicitly use quota=fuse=1; an ordinary routed service
|
| 165 |
+
should use its configured limits. Persisted slow attempts include failures.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def __init__(self, state_path: str | Path, *, quota: float, fuse: float):
|
| 169 |
+
if (
|
| 170 |
+
isinstance(quota, bool)
|
| 171 |
+
or isinstance(fuse, bool)
|
| 172 |
+
or not math.isfinite(quota)
|
| 173 |
+
or not math.isfinite(fuse)
|
| 174 |
+
or not 0 <= quota <= fuse <= 1
|
| 175 |
+
):
|
| 176 |
+
raise ValueError("Require finite 0 <= quota <= fuse <= 1")
|
| 177 |
+
self.quota = float(quota)
|
| 178 |
+
self.fuse = float(fuse)
|
| 179 |
+
self._quota = Decimal(str(quota))
|
| 180 |
+
self._fuse = Decimal(str(fuse))
|
| 181 |
+
target = Path(state_path)
|
| 182 |
+
if not target.is_absolute():
|
| 183 |
+
raise QuotaStateError("Quota state path must be absolute")
|
| 184 |
+
# Symbolic links are resolved first; state and lock must stay beside each other, outside the package.
|
| 185 |
+
self.state_path = target.resolve()
|
| 186 |
+
if self.state_path.is_relative_to(_ROOT) or _ROOT.is_relative_to(self.state_path):
|
| 187 |
+
raise QuotaStateError("Quota state must be separate from the package")
|
| 188 |
+
self.lock_path = self.state_path.with_name(self.state_path.name + ".lock")
|
| 189 |
+
if self.lock_path.resolve().parent != self.state_path.parent:
|
| 190 |
+
raise QuotaStateError("Quota lock must stay inside its state directory")
|
| 191 |
+
key = os.path.normcase(str(self.state_path))
|
| 192 |
+
with _REGISTRY_MUTEX:
|
| 193 |
+
self._lock = _PATH_LOCKS.setdefault(key, _PathLock())
|
| 194 |
+
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
| 195 |
+
with self._exclusive():
|
| 196 |
+
if self.state_path.exists():
|
| 197 |
+
self._read()
|
| 198 |
+
else:
|
| 199 |
+
self._write({"version": 2, "total": 0, "slow": 0})
|
| 200 |
+
|
| 201 |
+
@contextmanager
|
| 202 |
+
def _exclusive(self) -> Iterator[None]:
|
| 203 |
+
with self._lock.mutex:
|
| 204 |
+
depth = getattr(self._lock.local, "depth", 0)
|
| 205 |
+
if depth:
|
| 206 |
+
self._lock.local.depth = depth + 1
|
| 207 |
+
try:
|
| 208 |
+
yield
|
| 209 |
+
finally:
|
| 210 |
+
self._lock.local.depth = depth
|
| 211 |
+
else:
|
| 212 |
+
with _os_lock(self.lock_path):
|
| 213 |
+
self._lock.local.depth = 1
|
| 214 |
+
try:
|
| 215 |
+
yield
|
| 216 |
+
finally:
|
| 217 |
+
self._lock.local.depth = 0
|
| 218 |
+
|
| 219 |
+
def _read(self) -> dict:
|
| 220 |
+
try:
|
| 221 |
+
with self.state_path.open("r", encoding="utf-8") as handle:
|
| 222 |
+
state = json.load(handle)
|
| 223 |
+
except (OSError, ValueError) as exc:
|
| 224 |
+
raise QuotaStateError("Cannot read quota state; refusing to reset it") from exc
|
| 225 |
+
if (
|
| 226 |
+
not isinstance(state, dict)
|
| 227 |
+
or type(state.get("version")) is not int
|
| 228 |
+
or state["version"] not in (1, 2)
|
| 229 |
+
or any(type(state.get(key)) is not int or state[key] < 0 for key in ("total", "slow"))
|
| 230 |
+
):
|
| 231 |
+
raise QuotaStateError("Invalid quota state; refusing to reset it")
|
| 232 |
+
# slow may exceed total after a failed or interrupted slow attempt.
|
| 233 |
+
return state
|
| 234 |
+
|
| 235 |
+
def _write(self, state: dict) -> None:
|
| 236 |
+
temporary: str | None = None
|
| 237 |
+
try:
|
| 238 |
+
with tempfile.NamedTemporaryFile(
|
| 239 |
+
mode="w", encoding="utf-8", dir=self.state_path.parent,
|
| 240 |
+
prefix=self.state_path.name + ".", suffix=".tmp", delete=False,
|
| 241 |
+
) as handle:
|
| 242 |
+
temporary = handle.name
|
| 243 |
+
json.dump(state, handle, sort_keys=True, allow_nan=False)
|
| 244 |
+
handle.write("\n")
|
| 245 |
+
handle.flush()
|
| 246 |
+
os.fsync(handle.fileno())
|
| 247 |
+
os.replace(temporary, self.state_path)
|
| 248 |
+
temporary = None
|
| 249 |
+
_fsync_directory(self.state_path.parent)
|
| 250 |
+
except OSError as exc:
|
| 251 |
+
raise QuotaStateError("Cannot persist quota state; refusing inference") from exc
|
| 252 |
+
finally:
|
| 253 |
+
if temporary is not None:
|
| 254 |
+
try:
|
| 255 |
+
Path(temporary).unlink(missing_ok=True)
|
| 256 |
+
except OSError:
|
| 257 |
+
pass
|
| 258 |
+
|
| 259 |
+
@staticmethod
|
| 260 |
+
def _snapshot(state: dict) -> dict:
|
| 261 |
+
total, slow = state["total"], state["slow"]
|
| 262 |
+
return {
|
| 263 |
+
"total": total,
|
| 264 |
+
"slow": slow,
|
| 265 |
+
"slow_fraction": slow / total if total else float(slow > 0),
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
def snapshot(self) -> dict:
|
| 269 |
+
with self._exclusive():
|
| 270 |
+
return self._snapshot(self._read())
|
| 271 |
+
|
| 272 |
+
def configure_cost(self, config):
|
| 273 |
+
configure_cost(self, config)
|
| 274 |
+
with self._exclusive():self.cost_fuse=CostFuse(**self.cost_options,state=self._read().get('cost_fuse'))
|
| 275 |
+
|
| 276 |
+
@contextmanager
|
| 277 |
+
def accounting(self):
|
| 278 |
+
with self._exclusive():
|
| 279 |
+
state=self._read()
|
| 280 |
+
self.cost_fuse=CostFuse(**self.cost_options,state=state.get('cost_fuse'))
|
| 281 |
+
yield
|
| 282 |
+
|
| 283 |
+
def record_cost(self, usage, slow):
|
| 284 |
+
with self._exclusive():
|
| 285 |
+
before=self.cost_fuse.snapshot(self.tier_quota)
|
| 286 |
+
if self.cost_prices is not None:
|
| 287 |
+
p=self.cost_prices
|
| 288 |
+
self.cost_fuse.record((usage['input_tokens']*p['input_per_million']+usage['output_tokens']*p['output_per_million'])/1e6,slow)
|
| 289 |
+
after=self.cost_fuse.snapshot(self.tier_quota)
|
| 290 |
+
state=self._read();state.update(version=2,cost_fuse=self.cost_fuse.s);self._write(state)
|
| 291 |
+
if any(before[k]!=after[k] for k in ('state','effective_quota')):
|
| 292 |
+
print('cost_fuse '+json.dumps(after,sort_keys=True),flush=True)
|
| 293 |
+
return after
|
| 294 |
+
|
| 295 |
+
@contextmanager
|
| 296 |
+
def decision(self, wants_slow: bool, quota=None) -> Iterator[QuotaDecision]:
|
| 297 |
+
"""Reserve, run caller work, then commit only on normal context exit.
|
| 298 |
+
|
| 299 |
+
Use ``with counter.decision(wants_slow) as decision`` and run inference
|
| 300 |
+
inside that context. Propagate failures out of the block: swallowing an
|
| 301 |
+
inference failure would incorrectly record a successful decision.
|
| 302 |
+
"""
|
| 303 |
+
with self._exclusive():
|
| 304 |
+
if getattr(self._lock.local, "active_decision", False):
|
| 305 |
+
raise RuntimeError("Nested quota decisions are not supported")
|
| 306 |
+
self._lock.local.active_decision = True
|
| 307 |
+
try:
|
| 308 |
+
state = self._read()
|
| 309 |
+
total, slow = state["total"], state["slow"]
|
| 310 |
+
if Decimal(slow) > self._fuse * total:
|
| 311 |
+
path, reason = "fast", "fuse"
|
| 312 |
+
elif not wants_slow:
|
| 313 |
+
path, reason = "fast", "router"
|
| 314 |
+
elif Decimal(slow + 1) > self._fuse * (total + 1):
|
| 315 |
+
path, reason = "fast", "fuse"
|
| 316 |
+
elif Decimal(slow + 1) > min(self._quota, Decimal(str(quota)) if quota is not None else self._quota) * (total + 1):
|
| 317 |
+
path, reason = "fast", "quota"
|
| 318 |
+
else:
|
| 319 |
+
path, reason = "slow", "eligible"
|
| 320 |
+
state["slow"] += 1
|
| 321 |
+
self._write(state)
|
| 322 |
+
decision = QuotaDecision(path, reason, self._snapshot(state))
|
| 323 |
+
yield decision
|
| 324 |
+
state["total"] += 1
|
| 325 |
+
self._write(state)
|
| 326 |
+
decision.snapshot = self._snapshot(state)
|
| 327 |
+
finally:
|
| 328 |
+
self._lock.local.active_decision = False
|
| 329 |
+
|
| 330 |
+
class RuntimeCounter:
|
| 331 |
+
"""Run-scoped durable counter: startup IO failures abort; later IO failures use fast only."""
|
| 332 |
+
def __init__(self, config):
|
| 333 |
+
import logging
|
| 334 |
+
import uuid
|
| 335 |
+
routing = config['routing']
|
| 336 |
+
self.run_id = routing.get('run_id') or uuid.uuid4().hex
|
| 337 |
+
if Path(self.run_id).name != self.run_id or self.run_id in ('.', '..'):
|
| 338 |
+
raise ValueError('run_id must be a single directory name')
|
| 339 |
+
self.memory_only = False
|
| 340 |
+
self._mutex = threading.RLock()
|
| 341 |
+
self._state = {'total':0, 'slow':0}
|
| 342 |
+
self.durable = None
|
| 343 |
+
if not routing.get('state_dir') or not Path(routing['state_dir']).is_absolute():
|
| 344 |
+
raise QuotaStateError('An absolute quota state directory is required; refusing startup')
|
| 345 |
+
state = Path(routing['state_dir']).resolve()
|
| 346 |
+
if state.is_relative_to(_ROOT) or _ROOT.is_relative_to(state):
|
| 347 |
+
raise QuotaStateError('Quota state directory must be separate from the package')
|
| 348 |
+
directory = (state / self.run_id).resolve()
|
| 349 |
+
if directory.parent != state:
|
| 350 |
+
raise QuotaStateError('Run directory must stay inside the state directory')
|
| 351 |
+
for name in ('run.json', 'quota.json', 'quota.json.lock'):
|
| 352 |
+
if (directory / name).resolve().parent != directory:
|
| 353 |
+
raise QuotaStateError('Run files must stay inside the run directory')
|
| 354 |
+
fingerprint = {k:routing[k] for k in ('mode','quota','fuse')}
|
| 355 |
+
fingerprint.update(model_key=config['model']['model_key'], run_id=self.run_id,
|
| 356 |
+
tier=routing.get('tier'), tiers=routing.get('tiers'), cost_fuse=routing.get('cost_fuse'),
|
| 357 |
+
pricing=config.get('pricing'), cost_cap=config['limits']['cost_per_1000_decisions_usd'],
|
| 358 |
+
basis=config.get('usage',{}).get('basis','engine'))
|
| 359 |
+
try:
|
| 360 |
+
if routing.get('resume'):
|
| 361 |
+
if not routing.get('run_id'):
|
| 362 |
+
raise ValueError('resume requires an explicit run_id')
|
| 363 |
+
if not (directory/'quota.json').is_file() or not (directory/'run.json').is_file():
|
| 364 |
+
raise ValueError('Resume state is missing')
|
| 365 |
+
if json.loads((directory/'run.json').read_text(encoding='utf-8')) != fingerprint:
|
| 366 |
+
raise ValueError('Run fingerprint mismatch')
|
| 367 |
+
else:
|
| 368 |
+
if directory.exists():
|
| 369 |
+
raise ValueError('run_id already exists; select a new run_id or explicit resume')
|
| 370 |
+
directory.mkdir(parents=True, exist_ok=False)
|
| 371 |
+
with (directory/'run.json').open('x', encoding='utf-8') as handle:
|
| 372 |
+
handle.write(json.dumps(fingerprint))
|
| 373 |
+
self.durable = QuotaCounter(directory/'quota.json', quota=routing['quota'], fuse=routing['fuse'])
|
| 374 |
+
self._state = self.durable.snapshot()
|
| 375 |
+
except (OSError, QuotaStateError):
|
| 376 |
+
if routing.get('resume'):
|
| 377 |
+
raise ValueError('Resume state cannot be read or validated') from None
|
| 378 |
+
raise QuotaStateError('Quota persistence unavailable; refusing startup') from None
|
| 379 |
+
|
| 380 |
+
def configure_cost(self, config):
|
| 381 |
+
configure_cost(self, config)
|
| 382 |
+
if self.durable is not None:
|
| 383 |
+
self.durable.configure_cost(config)
|
| 384 |
+
self.cost_fuse=self.durable.cost_fuse
|
| 385 |
+
|
| 386 |
+
@contextmanager
|
| 387 |
+
def accounting(self):
|
| 388 |
+
import logging
|
| 389 |
+
with self._mutex:
|
| 390 |
+
context=None
|
| 391 |
+
if not self.memory_only:
|
| 392 |
+
try:
|
| 393 |
+
context=self.durable.accounting();context.__enter__()
|
| 394 |
+
self.cost_fuse=self.durable.cost_fuse
|
| 395 |
+
except (OSError,QuotaStateError):
|
| 396 |
+
context=None;self.memory_only=True
|
| 397 |
+
logging.warning('Cost state unavailable; memory counts and fast only')
|
| 398 |
+
try:yield
|
| 399 |
+
except BaseException as exc:
|
| 400 |
+
if context is not None:context.__exit__(type(exc),exc,exc.__traceback__)
|
| 401 |
+
raise
|
| 402 |
+
else:
|
| 403 |
+
if context is not None:context.__exit__(None,None,None)
|
| 404 |
+
|
| 405 |
+
def record_cost(self, usage, slow):
|
| 406 |
+
import logging
|
| 407 |
+
before=self.cost_fuse.s['n']
|
| 408 |
+
if not self.memory_only:
|
| 409 |
+
try:return self.durable.record_cost(usage,slow)
|
| 410 |
+
except (OSError,QuotaStateError):
|
| 411 |
+
self.memory_only=True
|
| 412 |
+
logging.warning('Cost commit unavailable; memory counts and fast only')
|
| 413 |
+
# A failed disk commit may already have updated this shared fuse object.
|
| 414 |
+
if self.cost_prices is not None and self.cost_fuse.s['n']==before:
|
| 415 |
+
p=self.cost_prices
|
| 416 |
+
self.cost_fuse.record((usage['input_tokens']*p['input_per_million']+usage['output_tokens']*p['output_per_million'])/1e6,slow)
|
| 417 |
+
return self.cost_fuse.snapshot(self.tier_quota)
|
| 418 |
+
|
| 419 |
+
def snapshot(self):
|
| 420 |
+
with self._mutex:
|
| 421 |
+
return dict(self._state)
|
| 422 |
+
|
| 423 |
+
@contextmanager
|
| 424 |
+
def decision(self, wants_slow, quota=None):
|
| 425 |
+
import logging
|
| 426 |
+
with self._mutex:
|
| 427 |
+
context = None
|
| 428 |
+
if not self.memory_only:
|
| 429 |
+
try:
|
| 430 |
+
context = self.durable.decision(wants_slow, quota=quota)
|
| 431 |
+
decision = context.__enter__()
|
| 432 |
+
self._state = dict(decision.snapshot)
|
| 433 |
+
except (OSError, QuotaStateError):
|
| 434 |
+
context = None
|
| 435 |
+
self.memory_only = True
|
| 436 |
+
logging.warning('Quota persistence failed; using memory counts and fast only')
|
| 437 |
+
if context is None:
|
| 438 |
+
decision = QuotaDecision('fast', 'memory_only', dict(self._state))
|
| 439 |
+
try:
|
| 440 |
+
yield decision
|
| 441 |
+
except BaseException as exc:
|
| 442 |
+
if context is not None:
|
| 443 |
+
context.__exit__(type(exc), exc, exc.__traceback__)
|
| 444 |
+
raise
|
| 445 |
+
else:
|
| 446 |
+
self._state['total'] += 1
|
| 447 |
+
if context is not None:
|
| 448 |
+
try:
|
| 449 |
+
context.__exit__(None, None, None)
|
| 450 |
+
self._state = dict(decision.snapshot)
|
| 451 |
+
except (OSError, QuotaStateError):
|
| 452 |
+
self.memory_only = True
|
| 453 |
+
logging.warning('Quota commit failed; using memory counts and fast only')
|
| 454 |
+
decision.snapshot = dict(self._state)
|
service/reconcile_usage.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Usage reconciliation before release (maintainer tool): reads bench outputs and engine counter
|
| 2 |
+
snapshots only; sends no inference requests and reads no request content.
|
| 3 |
+
|
| 4 |
+
python3 -B reconcile_usage.py --run /abs/out/fast [--run-off /abs/out/routed_off] [--tol 0.001]
|
| 5 |
+
Exit code: 0 passed; 1 an identity does not hold; 2 a precondition is not met (missing data or counters).
|
| 6 |
+
"""
|
| 7 |
+
import argparse, json, sys, math
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from core import external_path
|
| 11 |
+
|
| 12 |
+
COUNTERS = ('prompt_tokens', 'generation_tokens', 'prefix_cache_queries', 'prefix_cache_hits',
|
| 13 |
+
'num_preemptions', 'request_success')
|
| 14 |
+
FIELDS = ('input', 'output', 'total', 'cached', 'submitted', 'calls', 'generated')
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def resolve(prefix):
|
| 18 |
+
"""Run prefixes are absolute paths outside the package (see bench.py --output-prefix)."""
|
| 19 |
+
return external_path(prefix)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load(prefix):
|
| 23 |
+
p = resolve(prefix)
|
| 24 |
+
rows = [json.loads(x) for x in p.with_name(p.name + '.jsonl').read_text(encoding='utf-8').splitlines() if x.strip()]
|
| 25 |
+
stats = json.loads(p.with_name(p.name + '.engine_stats.json').read_text(encoding='utf-8'))
|
| 26 |
+
rows = [r for r in rows if r.get('concurrency', 1) == 1] # reconcile the serial level only
|
| 27 |
+
if not rows:
|
| 28 |
+
sys.exit(2)
|
| 29 |
+
return p, rows, stats
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def delta(stats):
|
| 33 |
+
level = stats['by_concurrency']['1']
|
| 34 |
+
before, after = level['before']['counters'], level['after']['counters']
|
| 35 |
+
missing = [k for k in COUNTERS if k not in before or k not in after]
|
| 36 |
+
if missing:
|
| 37 |
+
raise ValueError('engine counters missing: '+','.join(missing))
|
| 38 |
+
if any(isinstance(v,bool) or not isinstance(v,(int,float)) or not math.isfinite(v) for d in (before,after) for v in (d[k] for k in COUNTERS)):raise ValueError('Invalid counter')
|
| 39 |
+
return {k: after[k] - before[k] for k in COUNTERS}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def row_usage(row):
|
| 43 |
+
u = row.get('usage') or {}
|
| 44 |
+
if not isinstance(u,dict):return False,{}
|
| 45 |
+
d = u.get('input_tokens_details') or {}
|
| 46 |
+
if not isinstance(d,dict):return False,{}
|
| 47 |
+
v = dict(input=u.get('input_tokens'), output=u.get('output_tokens'), total=u.get('total_tokens'),
|
| 48 |
+
cached=d.get('cached_tokens'), submitted=d.get('engine_submitted_tokens'),
|
| 49 |
+
calls=d.get('engine_calls'), generated=d.get('engine_generated_tokens'))
|
| 50 |
+
ok = (row.get('ok') and u.get('accounting') == 'engine' and not u.get('usage_estimated')
|
| 51 |
+
and d.get('cached_tokens_known', True) and all(type(v[k]) is int and v[k] >= 0 for k in FIELDS))
|
| 52 |
+
return bool(ok), v
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def close(x, y, tol):
|
| 56 |
+
return abs(x - y) <= tol * max(1, abs(y))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def reconcile_one(rows, stats, tol):
|
| 60 |
+
fails, parsed = [], [row_usage(r) for r in rows]
|
| 61 |
+
if not rows or len({r.get('id') for r in rows})!=len(rows) or any(r.get('concurrency',1)!=1 for r in rows) or not all(ok for ok, _ in parsed):
|
| 62 |
+
return {'precondition': 'all rows ok, engine basis, exact usage'}, ['precondition']
|
| 63 |
+
U = [v for _, v in parsed]
|
| 64 |
+
S = {k: sum(v[k] for v in U) for k in FIELDS}
|
| 65 |
+
if S['generated']!=S['output']:fails.append('generated/output identity broken')
|
| 66 |
+
level=stats.get('by_concurrency',{}).get('1',{})
|
| 67 |
+
source=level.get('after',{}).get('source')
|
| 68 |
+
if source=='hf_forward_hook':
|
| 69 |
+
try:
|
| 70 |
+
before,after=level['before']['counters'],level['after']['counters']
|
| 71 |
+
if after.get('uncounted_forwards',0)>before.get('uncounted_forwards',0):
|
| 72 |
+
return {'precondition':'HF uncounted forwards'},['precondition']
|
| 73 |
+
D={k:after[k]-before[k] for k in ('prefill_tokens','forward_tokens')}
|
| 74 |
+
ds=[r['usage']['input_tokens_details'] for r in rows]
|
| 75 |
+
prefill=sum(d['hf_prefill_tokens'] for d in ds)
|
| 76 |
+
calls=sum(d['generate_nonempty_calls'] for d in ds)
|
| 77 |
+
except (KeyError,TypeError): return {'precondition':'HF counters missing'},['precondition']
|
| 78 |
+
if S['cached'] or any(v['input']!=v['submitted'] or v['total']!=v['input']+v['output'] for v in U): fails.append('HF row identity broken')
|
| 79 |
+
if D['prefill_tokens']!=prefill: fails.append('HF initial prefill mismatch')
|
| 80 |
+
if D['forward_tokens']!=S['input']+S['output']-calls: fails.append('HF forward identity broken')
|
| 81 |
+
return dict(sums=S,engine_delta=D,counter_mode='hf_forward'),fails
|
| 82 |
+
try: D = delta(stats)
|
| 83 |
+
except (KeyError,ValueError,TypeError): return {'precondition':'engine counters missing'},['precondition']
|
| 84 |
+
if any(v<0 for v in D.values()): return {'precondition':'counter reset'},['precondition']
|
| 85 |
+
if D['num_preemptions']:
|
| 86 |
+
return {'precondition':'preemption during run'},['precondition']
|
| 87 |
+
if D['request_success'] != S['calls']:
|
| 88 |
+
return {'precondition':'stray, warmup or missed engine calls'},['precondition']
|
| 89 |
+
bad = [r['id'] for r, v in zip(rows, U) if v['input'] != v['submitted'] - v['cached'] or v['total'] != v['input'] + v['output']]
|
| 90 |
+
if bad:
|
| 91 |
+
fails.append('row identity broken: %s' % bad[:5])
|
| 92 |
+
if close(S['submitted'], D['prompt_tokens'], tol):
|
| 93 |
+
mode, prefill = 'full_prompt', D['prompt_tokens'] - D['prefix_cache_hits']
|
| 94 |
+
elif close(S['submitted'] - S['cached'], D['prompt_tokens'], tol):
|
| 95 |
+
mode, prefill = 'computed_only', D['prompt_tokens']
|
| 96 |
+
else:
|
| 97 |
+
mode, prefill = 'unknown', None
|
| 98 |
+
fails.append('sum(engine_submitted) matches neither engine prompt-counter definition')
|
| 99 |
+
if not close(S['cached'], D['prefix_cache_hits'], tol):
|
| 100 |
+
fails.append('sum(cached_tokens) != delta prefix_cache_hits')
|
| 101 |
+
if prefill is not None and not close(S['input'], prefill, tol): # identity E1
|
| 102 |
+
fails.append('E1: sum(usage.input_tokens) != engine prefill')
|
| 103 |
+
if not close(S['output'], D['generation_tokens'], tol):
|
| 104 |
+
fails.append('sum(usage.output_tokens) != delta generation_tokens')
|
| 105 |
+
return dict(sums=S, engine_delta=D, counter_mode=mode, engine_prefill=prefill), fails
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def reconcile_pair(on_rows, off_rows, off_stats, tol):
|
| 109 |
+
fails = []
|
| 110 |
+
if off_stats.get('health', {}).get('prefix_caching') is not False:
|
| 111 |
+
fails.append('off run did not disable prefix caching')
|
| 112 |
+
if not all(row_usage(r)[0] for r in on_rows+off_rows): return {},['precondition']
|
| 113 |
+
if len({r['id'] for r in on_rows})!=len(on_rows) or len({r['id'] for r in off_rows})!=len(off_rows): return {},['precondition']
|
| 114 |
+
on = {r['id']: row_usage(r)[1] for r in on_rows}
|
| 115 |
+
off = {r['id']: row_usage(r)[1] for r in off_rows}
|
| 116 |
+
if any(v['cached'] for v in off.values()):
|
| 117 |
+
fails.append('off run reports cached tokens')
|
| 118 |
+
matched = [i for i in on if i in off and on[i]['submitted'] == off[i]['submitted'] and on[i]['output'] == off[i]['output']]
|
| 119 |
+
coverage = len(matched) / max(1, len(on),len(off))
|
| 120 |
+
if coverage < 0.99:
|
| 121 |
+
fails.append('decode drift: only %.1f%% rows identical between runs' % (100 * coverage))
|
| 122 |
+
diff = sum(off[i]['input'] - on[i]['input'] for i in matched)
|
| 123 |
+
cached = sum(on[i]['cached'] for i in matched)
|
| 124 |
+
if not close(diff, cached, tol): # identity E2
|
| 125 |
+
fails.append('E2: sum(input_off) - sum(input_on) != sum(cached_on)')
|
| 126 |
+
return dict(matched=len(matched), coverage=coverage, input_diff=diff, cached_on=cached), fails
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def main(argv=None):
|
| 130 |
+
ap = argparse.ArgumentParser(description=__doc__)
|
| 131 |
+
ap.add_argument('--run', required=True)
|
| 132 |
+
ap.add_argument('--run-off')
|
| 133 |
+
ap.add_argument('--tol', type=float, default=0.001)
|
| 134 |
+
a = ap.parse_args(argv)
|
| 135 |
+
if not 0<=a.tol<=0.001: ap.error('tol must be within [0, 0.001]')
|
| 136 |
+
try: path, rows, stats = load(a.run)
|
| 137 |
+
except (OSError,ValueError,KeyError) as exc:
|
| 138 |
+
print('precondition: '+str(exc), file=sys.stderr); return 2
|
| 139 |
+
report, fails = reconcile_one(rows, stats, a.tol)
|
| 140 |
+
if a.run_off:
|
| 141 |
+
try: _, off_rows, off_stats = load(a.run_off)
|
| 142 |
+
except (OSError,ValueError,KeyError) as exc:
|
| 143 |
+
print('precondition: '+str(exc), file=sys.stderr); return 2
|
| 144 |
+
off_report, off_fails = reconcile_one(off_rows, off_stats, a.tol)
|
| 145 |
+
pair, pair_fails = reconcile_pair(rows, off_rows, off_stats, a.tol)
|
| 146 |
+
report.update(off=off_report, pair=pair)
|
| 147 |
+
fails += ['off: ' + f for f in off_fails] + pair_fails
|
| 148 |
+
report.update(tolerance=a.tol, passed=not fails, submission_ready=bool(a.run_off) and not fails, failures=fails)
|
| 149 |
+
with path.with_name(path.name + '.reconcile.json').open('x', encoding='utf-8') as out: # exclusive create, never overwrite
|
| 150 |
+
json.dump(report, out, ensure_ascii=False, indent=2, allow_nan=False)
|
| 151 |
+
print(json.dumps({'passed': not fails, 'failures': fails}, ensure_ascii=False))
|
| 152 |
+
return 0 if not fails else (2 if any('precondition' in f for f in fails) else 1)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == '__main__':
|
| 156 |
+
raise SystemExit(main())
|
service/requirements.lock
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Measured version lock; versions only, no artifact hashes.
|
| 2 |
+
# All measured entries retained; torch CUDA build made explicit.
|
| 3 |
+
--extra-index-url https://download.pytorch.org/whl/cu130
|
| 4 |
+
agent-detector==2.0.0
|
| 5 |
+
aiohappyeyeballs==2.7.1
|
| 6 |
+
aiohttp==3.14.3
|
| 7 |
+
aiosignal==1.4.0
|
| 8 |
+
annotated-doc==0.0.5
|
| 9 |
+
annotated-types==0.8.0
|
| 10 |
+
anthropic==1.11.0
|
| 11 |
+
anyio==4.15.1
|
| 12 |
+
apache-tvm-ffi==0.1.11
|
| 13 |
+
astor==0.8.1
|
| 14 |
+
async-timeout==5.0.1
|
| 15 |
+
attrs==26.1.0
|
| 16 |
+
backports.strenum==1.3.1
|
| 17 |
+
blake3==1.0.10
|
| 18 |
+
cachetools==7.2.0
|
| 19 |
+
cbor2==6.1.5
|
| 20 |
+
certifi==2026.7.22
|
| 21 |
+
cffi==2.1.1
|
| 22 |
+
charset-normalizer==3.5.2
|
| 23 |
+
click==8.5.0
|
| 24 |
+
cloudpickle==3.1.2
|
| 25 |
+
compressed-tensors==0.17.0
|
| 26 |
+
cryptography==50.0.2
|
| 27 |
+
cuda-bindings==13.4.3
|
| 28 |
+
cuda-core==1.2.1
|
| 29 |
+
cuda-pathfinder==1.8.3
|
| 30 |
+
cuda-python==13.4.1
|
| 31 |
+
cuda-tile==1.6.0
|
| 32 |
+
cuda-toolkit==13.0.3.0
|
| 33 |
+
depyf==0.20.0
|
| 34 |
+
detect-installer==0.2.1
|
| 35 |
+
dill==0.4.1
|
| 36 |
+
dnspython==2.8.0
|
| 37 |
+
docstring_parser==0.18.0
|
| 38 |
+
einops==0.8.2
|
| 39 |
+
email-validator==2.3.0
|
| 40 |
+
exceptiongroup==1.3.1
|
| 41 |
+
fastapi==0.136.3
|
| 42 |
+
fastapi-cli==0.0.32
|
| 43 |
+
fastapi-cloud-cli==0.26.0
|
| 44 |
+
fastar==0.12.0
|
| 45 |
+
fastsafetensors==0.4.0
|
| 46 |
+
filelock==4.0.12
|
| 47 |
+
flashinfer-python==0.7.0.post1
|
| 48 |
+
frozenlist==1.8.0
|
| 49 |
+
fsspec==2026.9.0
|
| 50 |
+
googleapis-common-protos==1.75.5
|
| 51 |
+
grpcio==1.84.0
|
| 52 |
+
h11==0.16.0
|
| 53 |
+
hf-xet==1.6.0
|
| 54 |
+
httpcore==1.0.9
|
| 55 |
+
httpcore2==2.13.1
|
| 56 |
+
httptools==0.8.0
|
| 57 |
+
httpx==0.28.1
|
| 58 |
+
httpx2==2.13.1
|
| 59 |
+
huggingface_hub==1.33.0
|
| 60 |
+
humming-kernels==0.1.16
|
| 61 |
+
idna==3.20
|
| 62 |
+
ijson==3.5.1
|
| 63 |
+
instanttensor==0.2.0
|
| 64 |
+
interegular==0.3.3
|
| 65 |
+
Jinja2==3.1.6
|
| 66 |
+
jiter==0.17.0
|
| 67 |
+
jmespath==1.1.0
|
| 68 |
+
jsonschema==4.26.0
|
| 69 |
+
jsonschema-specifications==2025.9.1
|
| 70 |
+
lark==1.2.2
|
| 71 |
+
llguidance==1.7.6
|
| 72 |
+
llvmlite==0.47.0
|
| 73 |
+
lm-format-enforcer==0.11.3
|
| 74 |
+
loguru==0.7.3
|
| 75 |
+
markdown-it-py==4.2.0
|
| 76 |
+
MarkupSafe==3.0.4
|
| 77 |
+
mcp==2.3.0
|
| 78 |
+
mcp-types==2.3.0
|
| 79 |
+
mdurl==0.1.2
|
| 80 |
+
mistral_common==1.12.0
|
| 81 |
+
ml_dtypes==0.6.0
|
| 82 |
+
model-hosting-container-standards==0.1.16
|
| 83 |
+
mpmath==1.3.0
|
| 84 |
+
msgspec==0.22.0
|
| 85 |
+
multidict==6.9.1
|
| 86 |
+
nccl-extensions==0.1.0
|
| 87 |
+
nccl4py==0.6.0
|
| 88 |
+
networkx==3.4.2
|
| 89 |
+
ninja==1.13.2
|
| 90 |
+
numba==0.65.0
|
| 91 |
+
numpy==2.2.6
|
| 92 |
+
nvidia-cublas==13.1.1.3
|
| 93 |
+
nvidia-cuda-cccl==13.3.4.3.1
|
| 94 |
+
nvidia-cuda-crt==13.4.92
|
| 95 |
+
nvidia-cuda-cupti==13.0.85
|
| 96 |
+
nvidia-cuda-nvcc==13.4.92
|
| 97 |
+
nvidia-cuda-nvdisasm==13.4.92
|
| 98 |
+
nvidia-cuda-nvrtc==13.0.88
|
| 99 |
+
nvidia-cuda-runtime==13.0.96
|
| 100 |
+
nvidia-cudnn-cu13==9.20.0.48
|
| 101 |
+
nvidia-cudnn-frontend==1.30.0
|
| 102 |
+
nvidia-cufft==12.0.0.61
|
| 103 |
+
nvidia-cufile==1.15.1.6
|
| 104 |
+
nvidia-curand==10.4.0.35
|
| 105 |
+
nvidia-cusolver==12.0.4.66
|
| 106 |
+
nvidia-cusparse==12.6.3.3
|
| 107 |
+
nvidia-cusparselt-cu13==0.8.1
|
| 108 |
+
nvidia-cutlass-dsl==4.7.1
|
| 109 |
+
nvidia-cutlass-dsl-libs-base==4.7.1
|
| 110 |
+
nvidia-cutlass-dsl-libs-core==4.7.1
|
| 111 |
+
nvidia-cutlass-dsl-libs-cu12==4.7.1
|
| 112 |
+
nvidia-cutlass-dsl-libs-cu13==4.7.1
|
| 113 |
+
nvidia-ml-py==13.615.71
|
| 114 |
+
nvidia-nccl-cu13==2.29.7
|
| 115 |
+
nvidia-nvjitlink==13.4.92
|
| 116 |
+
nvidia-nvshmem-cu13==3.4.5
|
| 117 |
+
nvidia-nvtx==13.0.85
|
| 118 |
+
nvidia-nvvm==13.4.92
|
| 119 |
+
nvtx==0.2.15
|
| 120 |
+
openai==3.24.0
|
| 121 |
+
opencv-python-headless==5.0.0.93
|
| 122 |
+
opentelemetry-api==1.45.0
|
| 123 |
+
opentelemetry-exporter-http-transport==0.66b0
|
| 124 |
+
opentelemetry-exporter-otlp==1.45.0
|
| 125 |
+
opentelemetry-exporter-otlp-common==0.66b0
|
| 126 |
+
opentelemetry-exporter-otlp-proto-common==1.45.0
|
| 127 |
+
opentelemetry-exporter-otlp-proto-grpc==1.45.0
|
| 128 |
+
opentelemetry-exporter-otlp-proto-http==1.45.0
|
| 129 |
+
opentelemetry-proto==1.45.0
|
| 130 |
+
opentelemetry-sdk==1.45.0
|
| 131 |
+
opentelemetry-semantic-conventions==0.66b0
|
| 132 |
+
opentelemetry-semantic-conventions-ai==0.5.1
|
| 133 |
+
oss-harmony==0.0.11
|
| 134 |
+
outlines_core==0.2.14
|
| 135 |
+
packaging==26.3
|
| 136 |
+
partial-json-parser==0.2.1.1.post7
|
| 137 |
+
pillow==12.3.0
|
| 138 |
+
prometheus-fastapi-instrumentator==8.1.0
|
| 139 |
+
prometheus_client==0.26.0
|
| 140 |
+
propcache==0.5.4
|
| 141 |
+
protobuf==7.36.2
|
| 142 |
+
psutil==7.2.2
|
| 143 |
+
py-cpuinfo==9.0.0
|
| 144 |
+
pybase64==1.5.1
|
| 145 |
+
pycountry==26.2.16
|
| 146 |
+
pycparser==3.0
|
| 147 |
+
pydantic==2.13.5
|
| 148 |
+
pydantic-extra-types==2.11.1
|
| 149 |
+
pydantic-settings==2.15.0
|
| 150 |
+
pydantic_core==2.46.5
|
| 151 |
+
Pygments==2.21.0
|
| 152 |
+
PyJWT==2.15.1
|
| 153 |
+
pynvvideocodec==2.0.4
|
| 154 |
+
python-dotenv==1.2.4
|
| 155 |
+
python-json-logger==4.2.0
|
| 156 |
+
python-multipart==0.0.32
|
| 157 |
+
PyYAML==6.0.3
|
| 158 |
+
pyzmq==27.2.0
|
| 159 |
+
quack-kernels==0.6.5
|
| 160 |
+
referencing==0.37.0
|
| 161 |
+
regex==2026.9.29
|
| 162 |
+
requests==2.34.2
|
| 163 |
+
rich==15.0.0
|
| 164 |
+
rich-toolkit==0.20.5
|
| 165 |
+
rignore==0.8.1
|
| 166 |
+
rpds-py==0.30.0
|
| 167 |
+
safetensors==0.8.0
|
| 168 |
+
sentencepiece==0.2.2
|
| 169 |
+
sentry-sdk==2.71.0
|
| 170 |
+
setproctitle==1.3.8
|
| 171 |
+
shellingham==1.5.4
|
| 172 |
+
sniffio==1.3.1
|
| 173 |
+
sse-starlette==3.5.0
|
| 174 |
+
starlette==1.7.0
|
| 175 |
+
supervisor==4.3.0
|
| 176 |
+
sympy==1.14.0
|
| 177 |
+
tabulate==0.10.0
|
| 178 |
+
tiktoken==0.14.0
|
| 179 |
+
tilelang==0.1.12
|
| 180 |
+
tokenizers==0.23.2
|
| 181 |
+
tokenspeed-mla==0.1.8
|
| 182 |
+
tokenspeed-triton==3.8.10.post20260920
|
| 183 |
+
tomli==2.4.1
|
| 184 |
+
torch==2.13.0+cu130
|
| 185 |
+
torch_c_dlpack_ext==0.1.5
|
| 186 |
+
torchaudio==2.11.0+cu130
|
| 187 |
+
torchcodec==0.17.0+cu130
|
| 188 |
+
torchvision==0.28.0+cu130
|
| 189 |
+
tqdm==4.70.1
|
| 190 |
+
transformers==5.17.0
|
| 191 |
+
triton==3.7.1
|
| 192 |
+
truststore==0.10.4
|
| 193 |
+
typer==0.27.2
|
| 194 |
+
typing-inspection==0.4.4
|
| 195 |
+
typing_extensions==4.16.0
|
| 196 |
+
urllib3==2.8.0
|
| 197 |
+
uvicorn==0.54.0
|
| 198 |
+
uvloop==0.23.0
|
| 199 |
+
vllm==0.31.0
|
| 200 |
+
watchfiles==1.3.0
|
| 201 |
+
websockets==16.1.1
|
| 202 |
+
xgrammar==0.2.7
|
| 203 |
+
yarl==1.25.1
|
| 204 |
+
z3-solver==4.15.4.0
|
service/requirements.measured.lock
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
agent-detector==2.0.0
|
| 2 |
+
aiohappyeyeballs==2.7.1
|
| 3 |
+
aiohttp==3.14.3
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
annotated-doc==0.0.5
|
| 6 |
+
annotated-types==0.8.0
|
| 7 |
+
anthropic==1.11.0
|
| 8 |
+
anyio==4.15.1
|
| 9 |
+
apache-tvm-ffi==0.1.11
|
| 10 |
+
astor==0.8.1
|
| 11 |
+
async-timeout==5.0.1
|
| 12 |
+
attrs==26.1.0
|
| 13 |
+
backports.strenum==1.3.1
|
| 14 |
+
blake3==1.0.10
|
| 15 |
+
cachetools==7.2.0
|
| 16 |
+
cbor2==6.1.5
|
| 17 |
+
certifi==2026.7.22
|
| 18 |
+
cffi==2.1.1
|
| 19 |
+
charset-normalizer==3.5.2
|
| 20 |
+
click==8.5.0
|
| 21 |
+
cloudpickle==3.1.2
|
| 22 |
+
compressed-tensors==0.17.0
|
| 23 |
+
cryptography==50.0.2
|
| 24 |
+
cuda-bindings==13.4.3
|
| 25 |
+
cuda-core==1.2.1
|
| 26 |
+
cuda-pathfinder==1.8.3
|
| 27 |
+
cuda-python==13.4.1
|
| 28 |
+
cuda-tile==1.6.0
|
| 29 |
+
cuda-toolkit==13.0.3.0
|
| 30 |
+
depyf==0.20.0
|
| 31 |
+
detect-installer==0.2.1
|
| 32 |
+
dill==0.4.1
|
| 33 |
+
dnspython==2.8.0
|
| 34 |
+
docstring_parser==0.18.0
|
| 35 |
+
einops==0.8.2
|
| 36 |
+
email-validator==2.3.0
|
| 37 |
+
exceptiongroup==1.3.1
|
| 38 |
+
fastapi==0.136.3
|
| 39 |
+
fastapi-cli==0.0.32
|
| 40 |
+
fastapi-cloud-cli==0.26.0
|
| 41 |
+
fastar==0.12.0
|
| 42 |
+
fastsafetensors==0.4.0
|
| 43 |
+
filelock==4.0.12
|
| 44 |
+
flashinfer-python==0.7.0.post1
|
| 45 |
+
frozenlist==1.8.0
|
| 46 |
+
fsspec==2026.9.0
|
| 47 |
+
googleapis-common-protos==1.75.5
|
| 48 |
+
grpcio==1.84.0
|
| 49 |
+
h11==0.16.0
|
| 50 |
+
hf-xet==1.6.0
|
| 51 |
+
httpcore==1.0.9
|
| 52 |
+
httpcore2==2.13.1
|
| 53 |
+
httptools==0.8.0
|
| 54 |
+
httpx==0.28.1
|
| 55 |
+
httpx2==2.13.1
|
| 56 |
+
huggingface_hub==1.33.0
|
| 57 |
+
humming-kernels==0.1.16
|
| 58 |
+
idna==3.20
|
| 59 |
+
ijson==3.5.1
|
| 60 |
+
instanttensor==0.2.0
|
| 61 |
+
interegular==0.3.3
|
| 62 |
+
Jinja2==3.1.6
|
| 63 |
+
jiter==0.17.0
|
| 64 |
+
jmespath==1.1.0
|
| 65 |
+
jsonschema==4.26.0
|
| 66 |
+
jsonschema-specifications==2025.9.1
|
| 67 |
+
lark==1.2.2
|
| 68 |
+
llguidance==1.7.6
|
| 69 |
+
llvmlite==0.47.0
|
| 70 |
+
lm-format-enforcer==0.11.3
|
| 71 |
+
loguru==0.7.3
|
| 72 |
+
markdown-it-py==4.2.0
|
| 73 |
+
MarkupSafe==3.0.4
|
| 74 |
+
mcp==2.3.0
|
| 75 |
+
mcp-types==2.3.0
|
| 76 |
+
mdurl==0.1.2
|
| 77 |
+
mistral_common==1.12.0
|
| 78 |
+
ml_dtypes==0.6.0
|
| 79 |
+
model-hosting-container-standards==0.1.16
|
| 80 |
+
mpmath==1.3.0
|
| 81 |
+
msgspec==0.22.0
|
| 82 |
+
multidict==6.9.1
|
| 83 |
+
nccl-extensions==0.1.0
|
| 84 |
+
nccl4py==0.6.0
|
| 85 |
+
networkx==3.4.2
|
| 86 |
+
ninja==1.13.2
|
| 87 |
+
numba==0.65.0
|
| 88 |
+
numpy==2.2.6
|
| 89 |
+
nvidia-cublas==13.1.1.3
|
| 90 |
+
nvidia-cuda-cccl==13.3.4.3.1
|
| 91 |
+
nvidia-cuda-crt==13.4.92
|
| 92 |
+
nvidia-cuda-cupti==13.0.85
|
| 93 |
+
nvidia-cuda-nvcc==13.4.92
|
| 94 |
+
nvidia-cuda-nvdisasm==13.4.92
|
| 95 |
+
nvidia-cuda-nvrtc==13.0.88
|
| 96 |
+
nvidia-cuda-runtime==13.0.96
|
| 97 |
+
nvidia-cudnn-cu13==9.20.0.48
|
| 98 |
+
nvidia-cudnn-frontend==1.30.0
|
| 99 |
+
nvidia-cufft==12.0.0.61
|
| 100 |
+
nvidia-cufile==1.15.1.6
|
| 101 |
+
nvidia-curand==10.4.0.35
|
| 102 |
+
nvidia-cusolver==12.0.4.66
|
| 103 |
+
nvidia-cusparse==12.6.3.3
|
| 104 |
+
nvidia-cusparselt-cu13==0.8.1
|
| 105 |
+
nvidia-cutlass-dsl==4.7.1
|
| 106 |
+
nvidia-cutlass-dsl-libs-base==4.7.1
|
| 107 |
+
nvidia-cutlass-dsl-libs-core==4.7.1
|
| 108 |
+
nvidia-cutlass-dsl-libs-cu12==4.7.1
|
| 109 |
+
nvidia-cutlass-dsl-libs-cu13==4.7.1
|
| 110 |
+
nvidia-ml-py==13.615.71
|
| 111 |
+
nvidia-nccl-cu13==2.29.7
|
| 112 |
+
nvidia-nvjitlink==13.4.92
|
| 113 |
+
nvidia-nvshmem-cu13==3.4.5
|
| 114 |
+
nvidia-nvtx==13.0.85
|
| 115 |
+
nvidia-nvvm==13.4.92
|
| 116 |
+
nvtx==0.2.15
|
| 117 |
+
openai==3.24.0
|
| 118 |
+
opencv-python-headless==5.0.0.93
|
| 119 |
+
opentelemetry-api==1.45.0
|
| 120 |
+
opentelemetry-exporter-http-transport==0.66b0
|
| 121 |
+
opentelemetry-exporter-otlp==1.45.0
|
| 122 |
+
opentelemetry-exporter-otlp-common==0.66b0
|
| 123 |
+
opentelemetry-exporter-otlp-proto-common==1.45.0
|
| 124 |
+
opentelemetry-exporter-otlp-proto-grpc==1.45.0
|
| 125 |
+
opentelemetry-exporter-otlp-proto-http==1.45.0
|
| 126 |
+
opentelemetry-proto==1.45.0
|
| 127 |
+
opentelemetry-sdk==1.45.0
|
| 128 |
+
opentelemetry-semantic-conventions==0.66b0
|
| 129 |
+
opentelemetry-semantic-conventions-ai==0.5.1
|
| 130 |
+
oss-harmony==0.0.11
|
| 131 |
+
outlines_core==0.2.14
|
| 132 |
+
packaging==26.3
|
| 133 |
+
partial-json-parser==0.2.1.1.post7
|
| 134 |
+
pillow==12.3.0
|
| 135 |
+
prometheus-fastapi-instrumentator==8.1.0
|
| 136 |
+
prometheus_client==0.26.0
|
| 137 |
+
propcache==0.5.4
|
| 138 |
+
protobuf==7.36.2
|
| 139 |
+
psutil==7.2.2
|
| 140 |
+
py-cpuinfo==9.0.0
|
| 141 |
+
pybase64==1.5.1
|
| 142 |
+
pycountry==26.2.16
|
| 143 |
+
pycparser==3.0
|
| 144 |
+
pydantic==2.13.5
|
| 145 |
+
pydantic-extra-types==2.11.1
|
| 146 |
+
pydantic-settings==2.15.0
|
| 147 |
+
pydantic_core==2.46.5
|
| 148 |
+
Pygments==2.21.0
|
| 149 |
+
PyJWT==2.15.1
|
| 150 |
+
pynvvideocodec==2.0.4
|
| 151 |
+
python-dotenv==1.2.4
|
| 152 |
+
python-json-logger==4.2.0
|
| 153 |
+
python-multipart==0.0.32
|
| 154 |
+
PyYAML==6.0.3
|
| 155 |
+
pyzmq==27.2.0
|
| 156 |
+
quack-kernels==0.6.5
|
| 157 |
+
referencing==0.37.0
|
| 158 |
+
regex==2026.9.29
|
| 159 |
+
requests==2.34.2
|
| 160 |
+
rich==15.0.0
|
| 161 |
+
rich-toolkit==0.20.5
|
| 162 |
+
rignore==0.8.1
|
| 163 |
+
rpds-py==0.30.0
|
| 164 |
+
safetensors==0.8.0
|
| 165 |
+
sentencepiece==0.2.2
|
| 166 |
+
sentry-sdk==2.71.0
|
| 167 |
+
setproctitle==1.3.8
|
| 168 |
+
shellingham==1.5.4
|
| 169 |
+
sniffio==1.3.1
|
| 170 |
+
sse-starlette==3.5.0
|
| 171 |
+
starlette==1.7.0
|
| 172 |
+
supervisor==4.3.0
|
| 173 |
+
sympy==1.14.0
|
| 174 |
+
tabulate==0.10.0
|
| 175 |
+
tiktoken==0.14.0
|
| 176 |
+
tilelang==0.1.12
|
| 177 |
+
tokenizers==0.23.2
|
| 178 |
+
tokenspeed-mla==0.1.8
|
| 179 |
+
tokenspeed-triton==3.8.10.post20260920
|
| 180 |
+
tomli==2.4.1
|
| 181 |
+
torch==2.13.0+cu130
|
| 182 |
+
torch_c_dlpack_ext==0.1.5
|
| 183 |
+
torchaudio==2.11.0+cu130
|
| 184 |
+
torchcodec==0.17.0+cu130
|
| 185 |
+
torchvision==0.28.0+cu130
|
| 186 |
+
tqdm==4.70.1
|
| 187 |
+
transformers==5.17.0
|
| 188 |
+
triton==3.7.1
|
| 189 |
+
truststore==0.10.4
|
| 190 |
+
typer==0.27.2
|
| 191 |
+
typing-inspection==0.4.4
|
| 192 |
+
typing_extensions==4.16.0
|
| 193 |
+
urllib3==2.8.0
|
| 194 |
+
uvicorn==0.54.0
|
| 195 |
+
uvloop==0.23.0
|
| 196 |
+
vllm==0.31.0
|
| 197 |
+
watchfiles==1.3.0
|
| 198 |
+
websockets==16.1.1
|
| 199 |
+
xgrammar==0.2.7
|
| 200 |
+
yarl==1.25.1
|
| 201 |
+
z3-solver==4.15.4.0
|
service/selftest.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic offline selftest. Never imports a model runtime or opens a socket."""
|
| 2 |
+
import argparse
|
| 3 |
+
import copy
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import re
|
| 8 |
+
import socket
|
| 9 |
+
import tempfile
|
| 10 |
+
import unittest
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from unittest.mock import patch
|
| 13 |
+
import backends
|
| 14 |
+
import core
|
| 15 |
+
import launch
|
| 16 |
+
import pkg_check
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class WordTokenizer:
|
| 20 |
+
"""Reversible synthetic tokenizer: one token per whitespace-prefixed word."""
|
| 21 |
+
def __init__(self):
|
| 22 |
+
self.ids, self.pieces = {}, []
|
| 23 |
+
|
| 24 |
+
def _id(self, piece):
|
| 25 |
+
if piece not in self.ids:
|
| 26 |
+
self.ids[piece] = len(self.pieces)
|
| 27 |
+
self.pieces.append(piece)
|
| 28 |
+
return self.ids[piece]
|
| 29 |
+
|
| 30 |
+
def encode(self, text, add_special_tokens=False):
|
| 31 |
+
return [self._id(p) for p in re.findall(r'\s*\S+|\s+', text)]
|
| 32 |
+
|
| 33 |
+
def decode(self, ids, skip_special_tokens=True):
|
| 34 |
+
return ''.join(self.pieces[i] for i in ids)
|
| 35 |
+
|
| 36 |
+
def apply_chat_template(self, messages, tokenize=False, add_generation_prompt=True, **_):
|
| 37 |
+
return '<turn>user ' + messages[0]['content'] + ' <turn>model'
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def synthetic_task(words):
|
| 41 |
+
"""Invented rules + narrative of about `words` tokens; a yes/no question about the rules."""
|
| 42 |
+
rules = 'Rules:\nR1: A crate is late if it reaches dock B after day 30.\nR2: Late crates are refunded.\n\n'
|
| 43 |
+
sentences, day = [], 0
|
| 44 |
+
while sum(len(s.split()) for s in sentences) < words:
|
| 45 |
+
day += 1
|
| 46 |
+
sentences.append('Day %d: the courier moved crate C%d from dock A to dock B without delay.' % (day, day))
|
| 47 |
+
state = rules + 'Events:\n' + ' '.join(sentences)
|
| 48 |
+
payload = {'state': state, 'questions': {'q': {'type': 'noul', 'instructions': 'Is crate C7 refunded?'}}}
|
| 49 |
+
return core.normalize_request(payload, 1)[0][1]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class SelfTest(unittest.TestCase):
|
| 53 |
+
def test_package_integrity(self):
|
| 54 |
+
self.assertIn(pkg_check.checks()['status'], ('PREPARATION_CHECK_PASS', 'FROZEN_PACKAGE_CHECK_PASS'))
|
| 55 |
+
|
| 56 |
+
def test_pending_cannot_serve(self):
|
| 57 |
+
config = pkg_check.read('config.json')
|
| 58 |
+
if not config['package'].get('nonformal_fitted'):
|
| 59 |
+
with self.assertRaisesRegex(ValueError, 'fit pending'):
|
| 60 |
+
launch.check_preparation(allow_nonformal=True)
|
| 61 |
+
if config['package'].get('finalized'):
|
| 62 |
+
launch.check_preparation() # a frozen package starts formally, without any extra switch
|
| 63 |
+
else:
|
| 64 |
+
with self.assertRaises(ValueError):
|
| 65 |
+
launch.check_preparation()
|
| 66 |
+
|
| 67 |
+
def test_offline_environment(self):
|
| 68 |
+
state = Path(Path.cwd().anchor) / 'jev-31b-offline-state'
|
| 69 |
+
env = launch.child_environment({}, state, state / 'startup')
|
| 70 |
+
for key in ('HF_HUB_OFFLINE', 'TRANSFORMERS_OFFLINE', 'VLLM_BATCH_INVARIANT'):
|
| 71 |
+
self.assertEqual(env[key], '1')
|
| 72 |
+
self.assertEqual(env['VLLM_USE_FLASHINFER_SAMPLER'], '0')
|
| 73 |
+
self.assertTrue(Path(env['TMPDIR']).is_relative_to(state))
|
| 74 |
+
|
| 75 |
+
def test_served_mode_is_the_stated_policy(self):
|
| 76 |
+
policy = pkg_check.SERVICE_POLICY
|
| 77 |
+
self.assertEqual(launch.SERVED_MODE, policy['mode'])
|
| 78 |
+
c = pkg_check.read('config.json')
|
| 79 |
+
self.assertEqual((c['routing']['mode'], c['routing']['quota'], c['routing']['target_slow_share']),
|
| 80 |
+
(policy['mode'], policy['quota'], policy['quota']))
|
| 81 |
+
self.assertEqual(pkg_check.policy_problems(policy), [])
|
| 82 |
+
|
| 83 |
+
def test_yes_no_constant(self):
|
| 84 |
+
p = pkg_check.SERVICE_POLICY['noul_p_cal']
|
| 85 |
+
self.assertTrue(0.81 <= p <= 0.95)
|
| 86 |
+
cal = pkg_check.read('calibration.json')['models'][pkg_check.MODEL_KEY]
|
| 87 |
+
for side in ('fast', 'slow', 'fallback'):
|
| 88 |
+
block = core.validate_calibration({'models': {'m': cal}}, 'm', side)
|
| 89 |
+
self.assertEqual(block['noul_calibration']['p_cal'], p)
|
| 90 |
+
out = core.probabilities(['no', 'yes'], {'no': 0.2, 'yes': 1.5}, 'noul', block)
|
| 91 |
+
self.assertEqual(out, {'no': 1 - p, 'yes': p})
|
| 92 |
+
out = core.probabilities(['no', 'yes'], {'no': 1.5, 'yes': 0.2}, 'noul', block)
|
| 93 |
+
self.assertEqual(out, {'no': p, 'yes': 1 - p})
|
| 94 |
+
for p_bad in (0.80, 0.951):
|
| 95 |
+
policy = dict(pkg_check.SERVICE_POLICY, noul_p_cal=p_bad)
|
| 96 |
+
self.assertIn('yes/no constant outside [0.81, 0.95]', pkg_check.policy_problems(policy))
|
| 97 |
+
|
| 98 |
+
def test_drafting_budget_inside_the_context(self):
|
| 99 |
+
policy = pkg_check.SERVICE_POLICY
|
| 100 |
+
c = pkg_check.read('config.json')
|
| 101 |
+
served = copy.deepcopy(c)
|
| 102 |
+
core.apply_profile(served)
|
| 103 |
+
core.apply_budget_tier(served, None)
|
| 104 |
+
identity = core.threshold_identity(served)
|
| 105 |
+
self.assertEqual((identity['max_new_tokens'], identity['reasoning_words']),
|
| 106 |
+
(policy['draft']['max_new_tokens'], policy['draft']['reasoning_words']))
|
| 107 |
+
self.assertEqual(served['generation']['max_new_tokens'], policy['draft']['max_new_tokens'])
|
| 108 |
+
self.assertEqual(served['routing']['slow_max_prompt_tokens'], policy['slow_max_prompt_tokens'])
|
| 109 |
+
if policy['mode'] == 'routed':
|
| 110 |
+
self.assertLessEqual(policy['slow_max_prompt_tokens'] + policy['draft']['max_new_tokens']
|
| 111 |
+
+ pkg_check.SLOW_READOUT_MARGIN, c['model']['vllm_options']['max_model_len'])
|
| 112 |
+
wide = dict(policy, slow_max_prompt_tokens=6653)
|
| 113 |
+
self.assertIn('drafting budget outside the vLLM context', pkg_check.policy_problems(wide))
|
| 114 |
+
|
| 115 |
+
def test_slow_type_exclusion(self):
|
| 116 |
+
"""Excluded types (v3 core patch) never draft; the other types do (uniform mode, synthetic readout)."""
|
| 117 |
+
policy = pkg_check.SERVICE_POLICY
|
| 118 |
+
c = pkg_check.read('config.json')
|
| 119 |
+
excluded = policy['slow_excluded_types']
|
| 120 |
+
self.assertEqual(c['routing'].get('slow_excluded_types', []), excluded)
|
| 121 |
+
if not excluded:
|
| 122 |
+
return
|
| 123 |
+
self.assertEqual(getattr(core, 'SLOW_TYPE_EXCLUSION', None), 'routing.slow_excluded_types')
|
| 124 |
+
|
| 125 |
+
class DraftingBackend:
|
| 126 |
+
def slow_session(self, ids, unparsed_policy, meter, max_new_tokens=None):
|
| 127 |
+
return None, False # a draft without answer marker: the first-pass answer is kept
|
| 128 |
+
|
| 129 |
+
def fast(backend, task, config, ids, labels, groups, meter):
|
| 130 |
+
return {label: -float(index) for index, label in enumerate(labels)}, 0, None
|
| 131 |
+
length = {key: False for key in ('truncated', 'character_precompressed', 'protected_content_exceeds_budget',
|
| 132 |
+
'physical_truncated', 'long_input', 'slow_prompt_exceeds_budget')}
|
| 133 |
+
length['original_prompt_tokens'] = 1
|
| 134 |
+
cal = pkg_check.read('calibration.json')
|
| 135 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 136 |
+
config = copy.deepcopy(c)
|
| 137 |
+
core.apply_profile(config)
|
| 138 |
+
config['routing'].update(state_dir=str(Path(tmp).resolve()), run_id='exclusion', resume=False,
|
| 139 |
+
mode='uniform', quota=1., fuse=1.)
|
| 140 |
+
import quota
|
| 141 |
+
service = core.Service(config, cal, DraftingBackend(), quota.RuntimeCounter(config))
|
| 142 |
+
routes = {}
|
| 143 |
+
with patch('core.prepare_task', return_value=(None, (None, {}, [1], [1]), length)), \
|
| 144 |
+
patch('core.fast_attempt_with_retry', side_effect=fast):
|
| 145 |
+
for kind, question in (('noul', {'type': 'noul', 'instructions': 'Is it late?'}),
|
| 146 |
+
('choice', {'type': 'choice', 'instructions': 'Which one?',
|
| 147 |
+
'criteria': {'red': 'r', 'blue': 'b'}})):
|
| 148 |
+
response = service.answer({'state': 'synthetic', 'questions': {'q': question}})
|
| 149 |
+
routes[kind] = response['usage']['questions']['q']
|
| 150 |
+
for kind in ('choice', 'noul', 'score'):
|
| 151 |
+
if kind not in routes:
|
| 152 |
+
continue
|
| 153 |
+
with self.subTest(kind=kind):
|
| 154 |
+
if kind in excluded:
|
| 155 |
+
self.assertEqual((routes[kind]['path'], routes[kind]['route_reason']), ('fast', 'type_excluded'))
|
| 156 |
+
else:
|
| 157 |
+
self.assertEqual(routes[kind]['path'], 'slow')
|
| 158 |
+
bad = copy.deepcopy(c)
|
| 159 |
+
bad['routing']['slow_excluded_types'] = ['noul', 'noul']
|
| 160 |
+
with self.assertRaisesRegex(ValueError, 'slow_excluded_types'):
|
| 161 |
+
core.validate_config(bad)
|
| 162 |
+
|
| 163 |
+
def test_threshold_identity(self):
|
| 164 |
+
c = pkg_check.read('config.json')
|
| 165 |
+
expected = core.threshold_identity(c)
|
| 166 |
+
self.assertEqual(expected['model_key'], pkg_check.MODEL_KEY)
|
| 167 |
+
self.assertEqual(expected['max_prompt_tokens'], pkg_check.INPUT_BUDGET)
|
| 168 |
+
c['routing']['margin_threshold_fitted_for'] = {'placeholder': False, **expected}
|
| 169 |
+
self.assertEqual(core.validate_threshold(c), [])
|
| 170 |
+
c['routing']['margin_threshold_fitted_for']['model_key'] = 'wrong'
|
| 171 |
+
if c['routing']['mode'] == 'routed':
|
| 172 |
+
with self.assertRaisesRegex(ValueError, 'identity mismatch'):
|
| 173 |
+
core.validate_threshold(c) # routed serving refuses a threshold fitted for another identity
|
| 174 |
+
else:
|
| 175 |
+
self.assertTrue(core.validate_threshold(c))
|
| 176 |
+
|
| 177 |
+
def test_adapter_reference_materializes_into_served_identity(self):
|
| 178 |
+
c = pkg_check.read('config.json')
|
| 179 |
+
identity = core.threshold_identity(c)
|
| 180 |
+
c['routing']['margin_threshold_fitted_for'] = {'placeholder': False, **identity}
|
| 181 |
+
c['profiles'][pkg_check.MODEL_KEY]['routing'] = copy.deepcopy(c['routing'])
|
| 182 |
+
folder = Path(tempfile.gettempdir()) / 'synthetic-model' / 'adapter'
|
| 183 |
+
self.assertEqual(c['model']['adapter'], launch.ADAPTER_REF)
|
| 184 |
+
served = launch.materialize_adapter(c, folder)
|
| 185 |
+
self.assertEqual(served['model']['adapter'], str(folder))
|
| 186 |
+
self.assertEqual(served['routing']['margin_threshold_fitted_for']['adapter'], str(folder))
|
| 187 |
+
self.assertEqual(core.validate_threshold(served), [])
|
| 188 |
+
self.assertEqual(c['model']['adapter'], launch.ADAPTER_REF)
|
| 189 |
+
with self.assertRaises(ValueError):
|
| 190 |
+
launch.materialize_adapter(c, None)
|
| 191 |
+
raw = copy.deepcopy(c)
|
| 192 |
+
raw['model']['adapter'] = str(folder)
|
| 193 |
+
with self.assertRaises(ValueError):
|
| 194 |
+
launch.materialize_adapter(raw, folder)
|
| 195 |
+
|
| 196 |
+
def test_adapter_folder_is_exactly_pinned(self):
|
| 197 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 198 |
+
model = Path(tmp)
|
| 199 |
+
folder = model / 'adapter'
|
| 200 |
+
folder.mkdir()
|
| 201 |
+
payload = {'adapter_config.json': b'{"r": 16}\n', 'adapter_model.safetensors': b'synthetic weights'}
|
| 202 |
+
pinned = {}
|
| 203 |
+
for name, data in payload.items():
|
| 204 |
+
(folder / name).write_bytes(data)
|
| 205 |
+
pinned[name] = {'sha256': hashlib.sha256(data).hexdigest(), 'size': len(data)}
|
| 206 |
+
with patch.object(launch, 'ADAPTER_FILES', pinned):
|
| 207 |
+
self.assertEqual(launch.check_adapter(model), folder.resolve())
|
| 208 |
+
(folder / 'extra.json').write_text('{}')
|
| 209 |
+
with self.assertRaisesRegex(ValueError, 'exactly'):
|
| 210 |
+
launch.check_adapter(model)
|
| 211 |
+
(folder / 'extra.json').unlink()
|
| 212 |
+
(folder / 'adapter_model.safetensors').write_bytes(b'tampered weights!')
|
| 213 |
+
with self.assertRaisesRegex(ValueError, 'differs'):
|
| 214 |
+
launch.check_adapter(model)
|
| 215 |
+
with patch.object(launch, 'ADAPTER_FILES', {}):
|
| 216 |
+
with self.assertRaisesRegex(ValueError, 'pending'):
|
| 217 |
+
launch.check_adapter(model)
|
| 218 |
+
|
| 219 |
+
def test_input_budget_6653_everywhere(self):
|
| 220 |
+
c = pkg_check.read('config.json')
|
| 221 |
+
served = copy.deepcopy(c)
|
| 222 |
+
core.apply_profile(served)
|
| 223 |
+
core.apply_budget_tier(served, None)
|
| 224 |
+
self.assertEqual(served['limits']['max_prompt_tokens'], 6653)
|
| 225 |
+
self.assertEqual(served['limits']['physical_max_prompt_tokens'], 6653)
|
| 226 |
+
for tier in ('normal', 'long'):
|
| 227 |
+
self.assertEqual(served['routing']['tiers'][tier]['max_prompt_tokens'], 6653)
|
| 228 |
+
self.assertLessEqual(6653 + 3, c['model']['vllm_options']['max_model_len'])
|
| 229 |
+
|
| 230 |
+
def test_long_inputs_are_compressed_not_refused(self):
|
| 231 |
+
"""80k- and 7k-token synthetic inputs are answered from at most 6653 prompt tokens."""
|
| 232 |
+
c = pkg_check.read('config.json')
|
| 233 |
+
core.apply_profile(c)
|
| 234 |
+
backend = backends.TokenizerMixin()
|
| 235 |
+
backend.tokenizer = WordTokenizer()
|
| 236 |
+
for words, precompressed in ((80000, True), (7000, False)):
|
| 237 |
+
task = synthetic_task(words)
|
| 238 |
+
_, prepared, length = core.prepare_task(task, backend, c)
|
| 239 |
+
self.assertTrue(length['long_input'])
|
| 240 |
+
self.assertEqual(length['character_precompressed'], precompressed)
|
| 241 |
+
if not precompressed:
|
| 242 |
+
self.assertGreater(length['original_prompt_tokens'], 6653)
|
| 243 |
+
self.assertLessEqual(length['kept_prompt_tokens'], 6653)
|
| 244 |
+
self.assertLessEqual(len(prepared[2]), 6653)
|
| 245 |
+
self.assertFalse(length['physical_truncated'])
|
| 246 |
+
self.assertFalse(length['protected_content_exceeds_budget'])
|
| 247 |
+
task = synthetic_task(3000)
|
| 248 |
+
_, prepared, length = core.prepare_task(task, backend, c)
|
| 249 |
+
self.assertFalse(length['long_input'])
|
| 250 |
+
self.assertFalse(length['truncated'])
|
| 251 |
+
|
| 252 |
+
def test_readout_probability_contract(self):
|
| 253 |
+
block = {'temperature_by_kind': {'choice': 1., 'score': 1., 'noul': 'one_bin'}, 'noul_calibration': {'p_cal': .81}}
|
| 254 |
+
for kind, labels in [('choice', ['A', 'B', 'C']), ('score', ['1', '2', '3']), ('noul', ['no', 'yes'])]:
|
| 255 |
+
logits = {label: float(i) for i, label in enumerate(labels)}
|
| 256 |
+
p = core.probabilities(labels, logits, kind, block)
|
| 257 |
+
self.assertEqual(set(p), set(labels))
|
| 258 |
+
self.assertAlmostEqual(sum(p.values()), 1.)
|
| 259 |
+
self.assertTrue(all(math.isfinite(v) and 0 <= v <= 1 for v in p.values()))
|
| 260 |
+
self.assertEqual(max(labels, key=p.__getitem__), labels[-1])
|
| 261 |
+
|
| 262 |
+
def test_readout_rejects_missing_and_nonfinite(self):
|
| 263 |
+
for logits in ({0: 0.}, {0: 0., 1: float('nan')}, {0: 0., 1: float('inf')}):
|
| 264 |
+
with self.assertRaises(RuntimeError):
|
| 265 |
+
core.aggregate(logits, {'a': [0], 'b': [1]})
|
| 266 |
+
|
| 267 |
+
def test_config_does_not_accept_invalid_quota(self):
|
| 268 |
+
c = copy.deepcopy(pkg_check.read('config.json'))
|
| 269 |
+
c['routing']['quota'] = .9
|
| 270 |
+
with self.assertRaises(ValueError):
|
| 271 |
+
core.validate_config(c)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def main():
|
| 275 |
+
p = argparse.ArgumentParser(description=__doc__)
|
| 276 |
+
p.add_argument('--offline', action='store_true', help='default; compatibility flag')
|
| 277 |
+
p.parse_args()
|
| 278 |
+
with patch.object(socket, 'socket', side_effect=AssertionError('network forbidden in offline selftest')):
|
| 279 |
+
result = unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(SelfTest))
|
| 280 |
+
print(json.dumps({'status': 'PASS' if result.wasSuccessful() else 'FAIL', 'tests': result.testsRun,
|
| 281 |
+
'synthetic': True, 'model_called': False, 'network': False}, ensure_ascii=False))
|
| 282 |
+
return 0 if result.wasSuccessful() else 1
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
if __name__ == '__main__':
|
| 286 |
+
raise SystemExit(main())
|
service/selftest.sh
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
PKG=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
| 4 |
+
export VLLM_USE_FLASHINFER_SAMPLER=0 HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
|
| 5 |
+
export PYTHONDONTWRITEBYTECODE=1
|
| 6 |
+
exec "${PYTHON:-python3}" -B "$PKG/selftest.py" --offline "$@"
|
service/serve.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Start the service in the foreground. MODEL must hold the base files and adapter/ (see README.md).
|
| 3 |
+
# bash serve.sh --model /abs/model --state-dir /abs/state --run-id NAME [--port 8000] [--bind 127.0.0.1]
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
PKG=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
| 6 |
+
export VLLM_USE_FLASHINFER_SAMPLER=0 VLLM_NO_USAGE_STATS=1 DO_NOT_TRACK=1
|
| 7 |
+
export HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 PYTHONDONTWRITEBYTECODE=1
|
| 8 |
+
export VLLM_BATCH_INVARIANT=1 VLLM_WORKER_MULTIPROC_METHOD=spawn
|
| 9 |
+
exec "${PYTHON:-python3}" -B "$PKG/launch.py" "$@"
|
service/server.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standard-library HTTP adapter. No model is loaded until main()."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
if __package__:
|
| 10 |
+
from .core import ItemFailure, ServiceFailure, UnsupportedRequest, Service, ROOT, load_json, validate_config, apply_profile, validate_calibration, validate_threshold
|
| 11 |
+
else:
|
| 12 |
+
from core import ItemFailure, ServiceFailure, UnsupportedRequest, Service, ROOT, load_json, validate_config, apply_profile, validate_calibration, validate_threshold
|
| 13 |
+
|
| 14 |
+
BINDS = ("127.0.0.1", "0.0.0.0")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def make_handler(service):
|
| 18 |
+
def service_dead():
|
| 19 |
+
check = getattr(service, 'is_dead', None)
|
| 20 |
+
return check() if callable(check) else getattr(service, 'dead', False)
|
| 21 |
+
|
| 22 |
+
class Handler(BaseHTTPRequestHandler):
|
| 23 |
+
protocol_version='HTTP/1.1'
|
| 24 |
+
|
| 25 |
+
def handle_expect_100(self):
|
| 26 |
+
if self.command == 'POST' and service_dead():
|
| 27 |
+
self.close_connection=True
|
| 28 |
+
self.respond(503, {'error':'service failure'})
|
| 29 |
+
return False
|
| 30 |
+
try:
|
| 31 |
+
n=int(self.headers.get('Content-Length','0'))
|
| 32 |
+
if n<=0 or n>service.config['server']['max_request_bytes'] or self.headers.get('Transfer-Encoding'):
|
| 33 |
+
raise ValueError
|
| 34 |
+
except (ValueError,TypeError):
|
| 35 |
+
self.close_connection=True
|
| 36 |
+
self.respond(400,{'error':'Invalid request size'})
|
| 37 |
+
return False
|
| 38 |
+
self.send_response_only(100)
|
| 39 |
+
self.end_headers()
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
def setup(self):
|
| 43 |
+
super().setup()
|
| 44 |
+
self.connection.settimeout(service.config["server"]["socket_timeout_seconds"])
|
| 45 |
+
|
| 46 |
+
def log_message(self, format, *args):
|
| 47 |
+
# Avoid logging request contents, model paths or network endpoints.
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
def respond(self, status, value):
|
| 51 |
+
data = json.dumps(value, ensure_ascii=False, allow_nan=False).encode("utf-8")
|
| 52 |
+
self.send_response(status)
|
| 53 |
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
| 54 |
+
self.send_header("Content-Length", str(len(data)))
|
| 55 |
+
self.end_headers()
|
| 56 |
+
self.wfile.write(data)
|
| 57 |
+
|
| 58 |
+
def do_GET(self):
|
| 59 |
+
if self.path.split('?', 1)[0].rstrip('/') == '/health':
|
| 60 |
+
if __package__:
|
| 61 |
+
from .attention import attention_health
|
| 62 |
+
else:
|
| 63 |
+
from attention import attention_health
|
| 64 |
+
health=service.health()
|
| 65 |
+
# Attention backend and runtime versions are information, never warnings.
|
| 66 |
+
info=health.setdefault('info',{})
|
| 67 |
+
info.update(attention_health())
|
| 68 |
+
info['runtime']=getattr(service.backend,'runtime_info',None)
|
| 69 |
+
self.respond(200, health)
|
| 70 |
+
elif self.path.split('?',1)[0].rstrip('/') == '/engine_stats':
|
| 71 |
+
# Read-only engine counters for maintainer usage reconciliation; not used by evaluation.
|
| 72 |
+
try: self.respond(200,service.engine_stats())
|
| 73 |
+
except Exception: self.respond(503,{'error':'Engine counters unavailable'})
|
| 74 |
+
else:
|
| 75 |
+
self.respond(404, {'error':'Unknown route'})
|
| 76 |
+
|
| 77 |
+
def do_POST(self):
|
| 78 |
+
# After an unrecoverable engine error or the third consecutive item failure,
|
| 79 |
+
# every POST is answered 503 without reading or processing the request.
|
| 80 |
+
if service_dead():
|
| 81 |
+
self.close_connection=True
|
| 82 |
+
self.respond(503, {'error':'service failure'})
|
| 83 |
+
return
|
| 84 |
+
if self.path.split("?", 1)[0].rstrip("/") != "/v1/systemone":
|
| 85 |
+
self.close_connection=True
|
| 86 |
+
self.respond(404, {"error": "Unknown route"})
|
| 87 |
+
return
|
| 88 |
+
try:
|
| 89 |
+
if self.headers.get("Transfer-Encoding"):
|
| 90 |
+
raise ValueError("Transfer-Encoding is unsupported")
|
| 91 |
+
length = int(self.headers.get("Content-Length", "0"))
|
| 92 |
+
if length <= 0 or length > service.config["server"]["max_request_bytes"]:
|
| 93 |
+
raise ValueError("Invalid request size")
|
| 94 |
+
def reject(value):
|
| 95 |
+
raise ValueError("Non-finite JSON value")
|
| 96 |
+
raw = self.rfile.read(length)
|
| 97 |
+
if len(raw) != length:
|
| 98 |
+
raise ValueError("Incomplete request body")
|
| 99 |
+
payload = json.loads(raw.decode("utf-8"), parse_constant=reject)
|
| 100 |
+
# Validate before inference so model ValueError is never a client error.
|
| 101 |
+
if __package__:
|
| 102 |
+
from .core import normalize_request
|
| 103 |
+
else:
|
| 104 |
+
from core import normalize_request
|
| 105 |
+
normalize_request(payload, service.config["server"]["max_questions"])
|
| 106 |
+
except UnsupportedRequest:
|
| 107 |
+
self.close_connection=True
|
| 108 |
+
self.respond(422,{"error":"Unsupported systemone request"})
|
| 109 |
+
return
|
| 110 |
+
except (ValueError, TypeError, UnicodeError, OSError):
|
| 111 |
+
self.close_connection=True
|
| 112 |
+
self.respond(400, {"error": "Invalid systemone request"})
|
| 113 |
+
return
|
| 114 |
+
# Failures never produce a substitute answer: 422 for one item, 503 for the service.
|
| 115 |
+
try:
|
| 116 |
+
result = service.answer(payload)
|
| 117 |
+
json.dumps(result, allow_nan=False)
|
| 118 |
+
except ItemFailure as exc:
|
| 119 |
+
print("item failure: "+type(exc.__cause__ or exc).__name__, flush=True)
|
| 120 |
+
self.respond(422, {"error": "item could not be processed"})
|
| 121 |
+
return
|
| 122 |
+
except ServiceFailure as exc:
|
| 123 |
+
print("service failure: "+str(exc), flush=True)
|
| 124 |
+
self.respond(503, {"error": "service failure"})
|
| 125 |
+
return
|
| 126 |
+
except Exception as exc:
|
| 127 |
+
print("service failure: "+type(exc).__name__, flush=True)
|
| 128 |
+
self.respond(503, {"error": "service failure"})
|
| 129 |
+
return
|
| 130 |
+
self.respond(200, result)
|
| 131 |
+
|
| 132 |
+
return Handler
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def install_extensions(service, config):
|
| 136 |
+
"""Serving extensions named in config.json, installed on the running service in this order:
|
| 137 |
+
item_deadline (per-decision deadline of the drafting pass, with the item rules of the
|
| 138 |
+
`item_rules` block: the question types that may take the drafting pass and the low-margin choice
|
| 139 |
+
re-read), then post_calibration (per-type calibration of the reported probabilities). A
|
| 140 |
+
configuration without these blocks serves the unchanged service."""
|
| 141 |
+
if __package__:
|
| 142 |
+
from . import backends, core, item_deadline, post_calibration
|
| 143 |
+
else:
|
| 144 |
+
import backends, core, item_deadline, post_calibration
|
| 145 |
+
if config.get('item_deadline') is not None:
|
| 146 |
+
rules = config.get('item_rules') or {}
|
| 147 |
+
types = rules.get('slow_types')
|
| 148 |
+
service.warmup() # the drafting speed model is timed on a warmed engine, as in the measured setup
|
| 149 |
+
item_deadline.install(core, backends, service.backend, config['item_deadline'],
|
| 150 |
+
system_prompt=config['prompt']['system']).wrap_service(
|
| 151 |
+
service, slow_types=None if types is None or set(types) == {'choice', 'noul', 'score'} else types,
|
| 152 |
+
reread_tau=rules.get('reread_tau') or None)
|
| 153 |
+
if config.get('post_calibration') is not None:
|
| 154 |
+
post_calibration.wrap_service(service, config['post_calibration'], core)
|
| 155 |
+
return service
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class QuietHTTPServer(ThreadingHTTPServer):
|
| 159 |
+
# Request workers must not keep a stopped service alive while draining.
|
| 160 |
+
daemon_threads = True
|
| 161 |
+
|
| 162 |
+
def handle_error(self, request, client_address):
|
| 163 |
+
# The base implementation prints a peer endpoint on disconnected clients.
|
| 164 |
+
print("HTTP connection failed", flush=True)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def main(argv=None):
|
| 168 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 169 |
+
parser.add_argument("--config", type=Path, default=ROOT / "config.json")
|
| 170 |
+
parser.add_argument("--model")
|
| 171 |
+
parser.add_argument("--quant", choices=("int8", "bf16", "awq"))
|
| 172 |
+
parser.add_argument("--adapter")
|
| 173 |
+
parser.add_argument("--backend", choices=("transformers", "vllm"))
|
| 174 |
+
parser.add_argument("--bind", choices=BINDS, help="Listening address; defaults to server.bind in config")
|
| 175 |
+
parser.add_argument("--port", type=int)
|
| 176 |
+
parser.add_argument("--mode", choices=("routed", "uniform", "fast_only"))
|
| 177 |
+
parser.add_argument("--calibration", type=Path)
|
| 178 |
+
parser.add_argument('--state-dir', type=Path, help='Absolute directory for run state (required)')
|
| 179 |
+
parser.add_argument('--run-id')
|
| 180 |
+
parser.add_argument('--resume', action='store_true')
|
| 181 |
+
parser.add_argument('--model-key')
|
| 182 |
+
parser.add_argument('--quota-tier',choices=('normal','hard','long'))
|
| 183 |
+
parser.add_argument('--cost-fuse',choices=('on','off'))
|
| 184 |
+
parser.add_argument('--no-prefix-caching',action='store_true')
|
| 185 |
+
parser.add_argument('--allow-placeholder-threshold',action='store_true')
|
| 186 |
+
args = parser.parse_args(argv)
|
| 187 |
+
if args.state_dir is None or not args.state_dir.is_absolute():
|
| 188 |
+
parser.error("--state-dir must be given as an absolute path")
|
| 189 |
+
c = load_json(args.config)
|
| 190 |
+
for arg, field in (("model", "path"), ("quant", "quant"), ("adapter", "adapter"), ("backend", "backend")):
|
| 191 |
+
if getattr(args, arg) is not None:
|
| 192 |
+
c["model"][field] = getattr(args, arg)
|
| 193 |
+
for key in ("bind", "port"):
|
| 194 |
+
if getattr(args, key) is not None:
|
| 195 |
+
c["server"][key] = getattr(args, key)
|
| 196 |
+
if args.mode is not None:
|
| 197 |
+
c["routing"]["mode"] = args.mode
|
| 198 |
+
if args.state_dir is not None:
|
| 199 |
+
c['routing']['state_dir'] = str(args.state_dir)
|
| 200 |
+
if args.run_id:
|
| 201 |
+
c['routing']['run_id'] = args.run_id
|
| 202 |
+
c['routing']['resume'] = args.resume
|
| 203 |
+
if args.model_key:
|
| 204 |
+
c['model']['model_key'] = args.model_key
|
| 205 |
+
apply_profile(c)
|
| 206 |
+
if __package__: from .core import apply_budget_tier
|
| 207 |
+
else: from core import apply_budget_tier
|
| 208 |
+
apply_budget_tier(c,args.quota_tier)
|
| 209 |
+
if args.mode is not None:c["routing"]["mode"]=args.mode
|
| 210 |
+
if args.quant is not None and args.quant!=c['model']['quant']:parser.error('Precision conflicts with model profile')
|
| 211 |
+
if args.backend is not None and args.backend!=c['model']['backend']:parser.error('Backend conflicts with model profile')
|
| 212 |
+
if args.no_prefix_caching: c["model"]["enable_prefix_caching"]=False
|
| 213 |
+
if args.cost_fuse:c['routing']['cost_fuse']['enabled']=args.cost_fuse=='on'
|
| 214 |
+
validate_config(c)
|
| 215 |
+
validate_threshold(c,args.allow_placeholder_threshold)
|
| 216 |
+
if not c["model"]["path"] or not c["server"]["bind"]:
|
| 217 |
+
parser.error("Set --model and --bind, or their config fields")
|
| 218 |
+
calibration = load_json(args.calibration or ROOT / c["calibration_file"])
|
| 219 |
+
validate_calibration(calibration, c["model"]["model_key"])
|
| 220 |
+
if __package__:
|
| 221 |
+
from .backends import create_backend
|
| 222 |
+
from .quota import RuntimeCounter
|
| 223 |
+
else:
|
| 224 |
+
from backends import create_backend
|
| 225 |
+
from quota import RuntimeCounter
|
| 226 |
+
counter = RuntimeCounter(c)
|
| 227 |
+
service = Service(c, calibration, create_backend(c), counter, allow_placeholder_threshold=args.allow_placeholder_threshold)
|
| 228 |
+
install_extensions(service, c)
|
| 229 |
+
if c["server"].get("warmup", True):
|
| 230 |
+
service.warmup()
|
| 231 |
+
httpd = QuietHTTPServer((c["server"]["bind"], c["server"]["port"]), make_handler(service))
|
| 232 |
+
print("systemone service ready", flush=True)
|
| 233 |
+
try:
|
| 234 |
+
httpd.serve_forever()
|
| 235 |
+
except KeyboardInterrupt:
|
| 236 |
+
pass
|
| 237 |
+
finally:
|
| 238 |
+
httpd.server_close()
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
main()
|