Instructions to use schneewolflabs/Compactor-Qwen3.5-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use schneewolflabs/Compactor-Qwen3.5-4B with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("Lazarus-Ai/ReAligned-Qwen3.5-4B") model = PeftModel.from_pretrained(base_model, "schneewolflabs/Compactor-Qwen3.5-4B") - Notebooks
- Google Colab
- Kaggle
Compaction summariser: 0/45 -> 45/45 task lines, fact recall 0.157 -> 0.661
Browse files- README.md +105 -0
- adapter_config.json +46 -0
- adapter_model.safetensors +3 -0
- build_train.py +69 -0
- eval_compaction.py +133 -0
README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: Lazarus-Ai/ReAligned-Qwen3.5-4B
|
| 3 |
+
library_name: peft
|
| 4 |
+
license: apache-2.0
|
| 5 |
+
tags:
|
| 6 |
+
- lora
|
| 7 |
+
- sft
|
| 8 |
+
- summarization
|
| 9 |
+
- context-compaction
|
| 10 |
+
- agents
|
| 11 |
+
- egirl
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Compactor-Qwen3.5-4B
|
| 15 |
+
|
| 16 |
+
A context-compaction summariser for [egirl](https://github.com/Schneewolf-Labs/egirl): given a run
|
| 17 |
+
of agentic conversation about to be dropped, write the summary that lets the agent continue.
|
| 18 |
+
|
| 19 |
+
Meant to run as the **auxiliary model** — the operator keeps its slot and its context, and the
|
| 20 |
+
summary is produced by something small, deterministic, and trained for it.
|
| 21 |
+
|
| 22 |
+
## The failure it fixes
|
| 23 |
+
|
| 24 |
+
egirl compacts by dropping middle messages and replacing them with a summary. When that summary
|
| 25 |
+
loses the task, the agent does not get confused — **it invents a new task**:
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
Interior compaction: dropped 19 middle messages, kept head + 0 tail groups
|
| 29 |
+
Generated summary (396 chars) from 19 messages
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Sixteen web searches of real research compressed to 396 characters, after which the agent answered
|
| 33 |
+
a question about agent-harness architecture with *"Fresh project scaffolded at
|
| 34 |
+
~/projects/fresh_project."* The request was gone from its context.
|
| 35 |
+
|
| 36 |
+
So the objective is not fluency. It is **preservation**: the task, the findings, the state.
|
| 37 |
+
|
| 38 |
+
## Measured
|
| 39 |
+
|
| 40 |
+
45 held-out compaction windows, from transcripts that contributed no training data. Deterministic
|
| 41 |
+
checks, no judge:
|
| 42 |
+
|
| 43 |
+
| | base | **+ Compactor** |
|
| 44 |
+
|---|---|---|
|
| 45 |
+
| has a `Task:` line | **0/45 (0%)** | **45/45 (100%)** |
|
| 46 |
+
| task overlap (word F1 vs reference) | 0.000 | **0.509** |
|
| 47 |
+
| **fact recall** (paths, identifiers, numbers, errors) | **0.157** | **0.661** |
|
| 48 |
+
| compression | 0.043 | **0.129** |
|
| 49 |
+
| mean output | 884 chars | 2,689 chars |
|
| 50 |
+
|
| 51 |
+
Reference summaries (GPT-5) compress to ~0.12×. The base compresses to 0.043× — it is not
|
| 52 |
+
summarising, it is discarding: **84% of the distinctive facts are gone**, and it never states the
|
| 53 |
+
task at all.
|
| 54 |
+
|
| 55 |
+
**Fact recall is the number to read.** A summary can be fluent, plausible, correctly shaped, and
|
| 56 |
+
still have the specifics sanded off — which is what a small model does by default, and what makes
|
| 57 |
+
the agent lose the thread three turns later.
|
| 58 |
+
|
| 59 |
+
## Use
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 63 |
+
"Lazarus-Ai/ReAligned-Qwen3.5-4B", dtype=torch.bfloat16, device_map="auto")
|
| 64 |
+
model = PeftModel.from_pretrained(model, "schneewolflabs/Compactor-Qwen3.5-4B")
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Render prompts with `enable_thinking=False` — training used the pre-closed think block, and the
|
| 68 |
+
model answers directly rather than deliberating. Serving with thinking open puts it in a state it
|
| 69 |
+
never saw.
|
| 70 |
+
|
| 71 |
+
In egirl, point `[local.auxiliary]` at a server running this and compaction routes to it
|
| 72 |
+
automatically.
|
| 73 |
+
|
| 74 |
+
## Training
|
| 75 |
+
|
| 76 |
+
| | |
|
| 77 |
+
|---|---|
|
| 78 |
+
| base | `Lazarus-Ai/ReAligned-Qwen3.5-4B` |
|
| 79 |
+
| method | SFT, LoRA **r=64 α=128** dropout 0.05 |
|
| 80 |
+
| data | [egirl-compaction-SFT](https://huggingface.co/datasets/schneewolflabs/egirl-compaction-SFT) — 1,059 train / 45 val |
|
| 81 |
+
| tokens | 9.34M |
|
| 82 |
+
| schedule | 1 epoch, 133 steps, lr 1e-4 cosine |
|
| 83 |
+
| sequence | 16,384 — an 8192 cap would have dropped 55% of rows |
|
| 84 |
+
| hardware | one RTX A6000, 11h44m |
|
| 85 |
+
|
| 86 |
+
Train loss 0.738 → 0.584; held-out 0.754 → **0.561**, below train at every checkpoint. No
|
| 87 |
+
memorisation, despite r=64 on 1,059 samples — which the transcript-level split makes a meaningful
|
| 88 |
+
statement rather than an artefact.
|
| 89 |
+
|
| 90 |
+
**This architecture is expensive to train.** 265 s/step at 16k sequence, against 15.7 s/step for a
|
| 91 |
+
same-size Qwen3-VL under identical batch settings — the 24 linear-attention layers have no fused
|
| 92 |
+
kernel in transformers 5.3.0 and run a sequential scan whose cost grows with sequence length.
|
| 93 |
+
Inference is cheap; training is not.
|
| 94 |
+
|
| 95 |
+
## Limits
|
| 96 |
+
|
| 97 |
+
- **One epoch, one checkpoint.** No epoch-2 comparison, which has mattered elsewhere in this
|
| 98 |
+
family: a sibling model won overall at epoch 2 while losing a specific behaviour it had at
|
| 99 |
+
epoch 1.
|
| 100 |
+
- **Targets are one model's judgement.** GPT-5 decided what mattered in each window. Where it
|
| 101 |
+
dropped something, this learned to drop it.
|
| 102 |
+
- **Task overlap is 0.509, not 0.9.** The task line is always present and usually about the right
|
| 103 |
+
thing; it is not word-for-word the reference. Treat it as "states the task" rather than "states
|
| 104 |
+
it identically".
|
| 105 |
+
- **One operator's transcripts**, 120 conversations of agentic coding. Other domains are untested.
|
adapter_config.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"alora_invocation_tokens": null,
|
| 3 |
+
"alpha_pattern": {},
|
| 4 |
+
"arrow_config": null,
|
| 5 |
+
"auto_mapping": null,
|
| 6 |
+
"base_model_name_or_path": "Lazarus-Ai/ReAligned-Qwen3.5-4B",
|
| 7 |
+
"bias": "none",
|
| 8 |
+
"corda_config": null,
|
| 9 |
+
"ensure_weight_tying": false,
|
| 10 |
+
"eva_config": null,
|
| 11 |
+
"exclude_modules": null,
|
| 12 |
+
"fan_in_fan_out": false,
|
| 13 |
+
"inference_mode": true,
|
| 14 |
+
"init_lora_weights": true,
|
| 15 |
+
"layer_replication": null,
|
| 16 |
+
"layers_pattern": null,
|
| 17 |
+
"layers_to_transform": null,
|
| 18 |
+
"loftq_config": {},
|
| 19 |
+
"lora_alpha": 128,
|
| 20 |
+
"lora_bias": false,
|
| 21 |
+
"lora_dropout": 0.05,
|
| 22 |
+
"megatron_config": null,
|
| 23 |
+
"megatron_core": "megatron.core",
|
| 24 |
+
"modules_to_save": null,
|
| 25 |
+
"peft_type": "LORA",
|
| 26 |
+
"peft_version": "0.18.1",
|
| 27 |
+
"qalora_group_size": 16,
|
| 28 |
+
"r": 64,
|
| 29 |
+
"rank_pattern": {},
|
| 30 |
+
"revision": null,
|
| 31 |
+
"target_modules": [
|
| 32 |
+
"down_proj",
|
| 33 |
+
"gate_proj",
|
| 34 |
+
"o_proj",
|
| 35 |
+
"up_proj",
|
| 36 |
+
"v_proj",
|
| 37 |
+
"k_proj",
|
| 38 |
+
"q_proj"
|
| 39 |
+
],
|
| 40 |
+
"target_parameters": null,
|
| 41 |
+
"task_type": "CAUSAL_LM",
|
| 42 |
+
"trainable_token_indices": null,
|
| 43 |
+
"use_dora": false,
|
| 44 |
+
"use_qalora": false,
|
| 45 |
+
"use_rslora": false
|
| 46 |
+
}
|
adapter_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:195427fa658f09867e777ec9b97674b52122f240bd98e4b6fc3283c2ca9dece0
|
| 3 |
+
size 339776632
|
build_train.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render the compaction dataset for ReAligned-Qwen3.5-4B.
|
| 2 |
+
|
| 3 |
+
Two things this has to get right, both learned on sibling runs in this project.
|
| 4 |
+
|
| 5 |
+
**Thinking is closed in the prompt.** Qwen3.5's template opens `<think>` on every generation.
|
| 6 |
+
Rendered naively the target would begin `\n</think>\n\n`, teaching the model to emit a closing think
|
| 7 |
+
tag as the first thing in its answer — the broken hybrid-thinking gate. This template does honour
|
| 8 |
+
`enable_thinking=False`, which emits the empty block as part of the prompt, where the trainer masks
|
| 9 |
+
it. The summariser should answer directly anyway: egirl calls it for a summary, not a deliberation.
|
| 10 |
+
|
| 11 |
+
**Over-length rows are dropped, not truncated.** The trainer truncates prompts from the right,
|
| 12 |
+
which here removes the most recent messages in the window — the current state, the part a summary
|
| 13 |
+
most needs. A row that cannot fit is worth less than a row that teaches summarising from a
|
| 14 |
+
mutilated transcript.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import statistics as st
|
| 20 |
+
|
| 21 |
+
from transformers import AutoTokenizer
|
| 22 |
+
|
| 23 |
+
MODEL = "Lazarus-Ai/ReAligned-Qwen3.5-4B"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def main() -> None:
|
| 27 |
+
ap = argparse.ArgumentParser()
|
| 28 |
+
ap.add_argument("--max-tokens", type=int, default=16384)
|
| 29 |
+
args = ap.parse_args()
|
| 30 |
+
|
| 31 |
+
tok = AutoTokenizer.from_pretrained(MODEL)
|
| 32 |
+
|
| 33 |
+
for split in ("train", "val"):
|
| 34 |
+
rows = [json.loads(line) for line in open(f"{split}.jsonl")]
|
| 35 |
+
out, lens, dropped = [], [], 0
|
| 36 |
+
for r in rows:
|
| 37 |
+
prompt = tok.apply_chat_template(
|
| 38 |
+
[{"role": "system", "content": r["system"]},
|
| 39 |
+
{"role": "user", "content": r["prompt"]}],
|
| 40 |
+
add_generation_prompt=True, enable_thinking=False, tokenize=False)
|
| 41 |
+
response = r["chosen"] + tok.eos_token
|
| 42 |
+
n = len(tok(prompt)["input_ids"]) + len(
|
| 43 |
+
tok(response, add_special_tokens=False)["input_ids"])
|
| 44 |
+
lens.append(n)
|
| 45 |
+
if n > args.max_tokens:
|
| 46 |
+
dropped += 1
|
| 47 |
+
continue
|
| 48 |
+
out.append({"prompt": prompt, "chosen": response,
|
| 49 |
+
"source_group": r["source_group"]})
|
| 50 |
+
|
| 51 |
+
with open(f"compaction_{split}.jsonl", "w") as f:
|
| 52 |
+
for o in out:
|
| 53 |
+
f.write(json.dumps(o) + "\n")
|
| 54 |
+
|
| 55 |
+
lens.sort()
|
| 56 |
+
print(f"{split}: {len(rows)} -> {len(out)} kept, {dropped} over {args.max_tokens}")
|
| 57 |
+
print(f" tokens mean {st.mean(lens):.0f} median {lens[len(lens)//2]} "
|
| 58 |
+
f"p95 {lens[int(.95*len(lens))]} max {max(lens)}")
|
| 59 |
+
for cap in (8192, 12288, 16384, 24576):
|
| 60 |
+
print(f" cap {cap:6d} would drop {sum(1 for x in lens if x > cap):4d}/{len(lens)}")
|
| 61 |
+
print(f" total tokens: {sum(x for x in lens if x <= args.max_tokens):,}")
|
| 62 |
+
|
| 63 |
+
sample = json.loads(open("compaction_train.jsonl").readline())
|
| 64 |
+
print("\nprompt tail:", repr(sample["prompt"][-70:]))
|
| 65 |
+
print("response head:", repr(sample["chosen"][:90]))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
eval_compaction.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Does the summary preserve what is needed to continue?
|
| 2 |
+
|
| 3 |
+
Loss is the wrong headline. The failure this tune exists to fix is specific: a summary that drops
|
| 4 |
+
the task, after which the agent invents a new one — sixteen searches of research compacted into 396
|
| 5 |
+
characters, answered with "fresh project scaffolded". So the metric is preservation, not fluency.
|
| 6 |
+
|
| 7 |
+
Four checks, all deterministic, no judge:
|
| 8 |
+
|
| 9 |
+
- **has_task** — is there a `Task:` line at all. The reference summaries all have one.
|
| 10 |
+
- **task_overlap** — word-level F1 between the generated task line and the reference's. Catches a
|
| 11 |
+
summary that states *a* task, just not this one.
|
| 12 |
+
- **fact_recall** — fraction of the reference's distinctive tokens (paths, identifiers, numbers,
|
| 13 |
+
errors) that survive into the generated summary. This is the one that catches a fluent summary
|
| 14 |
+
with the specifics sanded off, which is what a small model tends to produce.
|
| 15 |
+
- **compression** — length ratio. A "summary" that is 90% of the input has not compacted anything,
|
| 16 |
+
and one that is 2% has thrown the state away.
|
| 17 |
+
|
| 18 |
+
Run base and tuned through the identical path; the difference is the adapter.
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
python3 eval_compaction.py --out base_eval.json
|
| 22 |
+
python3 eval_compaction.py --adapter out/adapter --out tuned_eval.json
|
| 23 |
+
python3 eval_compaction.py --compare base_eval.json tuned_eval.json
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import argparse
|
| 27 |
+
import json
|
| 28 |
+
import re
|
| 29 |
+
import sys
|
| 30 |
+
|
| 31 |
+
MODEL = "Lazarus-Ai/ReAligned-Qwen3.5-4B"
|
| 32 |
+
|
| 33 |
+
# Tokens worth preserving: paths, dotted identifiers, CamelCase, numbers with units, errors.
|
| 34 |
+
DISTINCTIVE = re.compile(
|
| 35 |
+
r"(?:/[\w.\-/]{4,})"
|
| 36 |
+
r"|(?:\b\w+\.(?:ts|py|json|jsonl|toml|md|sh|jinja|gguf|safetensors)\b)"
|
| 37 |
+
r"|(?:\b[A-Z][a-z]+[A-Z]\w+\b)"
|
| 38 |
+
r"|(?:\b\d{2,}(?:\.\d+)?[kKmMgG]?\b)"
|
| 39 |
+
r"|(?:\b[A-Z][A-Z_]{3,}\b)"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def task_line(text: str) -> str:
|
| 44 |
+
m = re.search(r"^\s*task:\s*(.+)$", text, re.I | re.M)
|
| 45 |
+
return m.group(1).strip() if m else ""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def f1(a: str, b: str) -> float:
|
| 49 |
+
ta = {w.lower() for w in re.findall(r"\w+", a) if len(w) > 2}
|
| 50 |
+
tb = {w.lower() for w in re.findall(r"\w+", b) if len(w) > 2}
|
| 51 |
+
if not ta or not tb:
|
| 52 |
+
return 0.0
|
| 53 |
+
hit = len(ta & tb)
|
| 54 |
+
p, r = hit / len(ta), hit / len(tb)
|
| 55 |
+
return 0.0 if p + r == 0 else 2 * p * r / (p + r)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def score(generated: str, reference: str, prompt: str) -> dict:
|
| 59 |
+
ref_facts = set(DISTINCTIVE.findall(reference))
|
| 60 |
+
gen_facts = set(DISTINCTIVE.findall(generated))
|
| 61 |
+
return {
|
| 62 |
+
"has_task": bool(task_line(generated)),
|
| 63 |
+
"task_overlap": round(f1(task_line(generated), task_line(reference)), 3),
|
| 64 |
+
"fact_recall": round(len(ref_facts & gen_facts) / len(ref_facts), 3) if ref_facts else None,
|
| 65 |
+
"compression": round(len(generated) / max(len(prompt), 1), 4),
|
| 66 |
+
"chars": len(generated),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def run(args) -> None:
|
| 71 |
+
import torch
|
| 72 |
+
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 73 |
+
|
| 74 |
+
tok = AutoTokenizer.from_pretrained(MODEL)
|
| 75 |
+
tok.padding_side = "left"
|
| 76 |
+
if tok.pad_token_id is None:
|
| 77 |
+
tok.pad_token = tok.eos_token
|
| 78 |
+
|
| 79 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 80 |
+
MODEL, dtype=torch.bfloat16, device_map="cuda:0")
|
| 81 |
+
if args.adapter:
|
| 82 |
+
from peft import PeftModel
|
| 83 |
+
model = PeftModel.from_pretrained(model, args.adapter)
|
| 84 |
+
model.eval()
|
| 85 |
+
|
| 86 |
+
rows = [json.loads(l) for l in open(args.val)][: args.limit]
|
| 87 |
+
results = []
|
| 88 |
+
for i, r in enumerate(rows):
|
| 89 |
+
enc = tok(r["prompt"], return_tensors="pt").to(model.device)
|
| 90 |
+
with torch.no_grad():
|
| 91 |
+
out = model.generate(**enc, max_new_tokens=1200, do_sample=False,
|
| 92 |
+
pad_token_id=tok.pad_token_id)
|
| 93 |
+
gen = tok.decode(out[0][enc["input_ids"].shape[1]:], skip_special_tokens=True).strip()
|
| 94 |
+
s = score(gen, r["chosen"], r["prompt"])
|
| 95 |
+
s["text"] = gen[:1500]
|
| 96 |
+
results.append(s)
|
| 97 |
+
print(f"[{i+1}/{len(rows)}] task={s['has_task']} overlap={s['task_overlap']} "
|
| 98 |
+
f"facts={s['fact_recall']} chars={s['chars']}", flush=True)
|
| 99 |
+
|
| 100 |
+
summarise(results, args.adapter or "base")
|
| 101 |
+
json.dump({"adapter": args.adapter, "results": results}, open(args.out, "w"), indent=1)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def summarise(results: list, label: str) -> None:
|
| 105 |
+
n = len(results)
|
| 106 |
+
has = sum(r["has_task"] for r in results)
|
| 107 |
+
ov = [r["task_overlap"] for r in results]
|
| 108 |
+
fr = [r["fact_recall"] for r in results if r["fact_recall"] is not None]
|
| 109 |
+
comp = [r["compression"] for r in results]
|
| 110 |
+
print(f"\n--- {label} ({n} held-out windows) ---")
|
| 111 |
+
print(f" has Task: line {has}/{n} ({100*has/n:.0f}%)")
|
| 112 |
+
print(f" task overlap {sum(ov)/n:.3f} mean")
|
| 113 |
+
print(f" fact recall {sum(fr)/len(fr):.3f} mean" if fr else " fact recall n/a")
|
| 114 |
+
print(f" compression {sum(comp)/n:.3f} mean (reference ~0.12)")
|
| 115 |
+
print(f" mean chars {sum(r['chars'] for r in results)//n}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def compare(a: str, b: str) -> int:
|
| 119 |
+
for path in (a, b):
|
| 120 |
+
d = json.load(open(path))
|
| 121 |
+
summarise(d["results"], d.get("adapter") or "base")
|
| 122 |
+
return 0
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
ap = argparse.ArgumentParser()
|
| 127 |
+
ap.add_argument("--adapter")
|
| 128 |
+
ap.add_argument("--val", default="compaction_val.jsonl")
|
| 129 |
+
ap.add_argument("--limit", type=int, default=45)
|
| 130 |
+
ap.add_argument("--out", default="compaction_eval.json")
|
| 131 |
+
ap.add_argument("--compare", nargs=2)
|
| 132 |
+
a = ap.parse_args()
|
| 133 |
+
sys.exit(compare(*a.compare) if a.compare else run(a))
|