cudabenchmarktest commited on
Commit
df5c969
Β·
verified Β·
1 Parent(s): a023ed8

r3-crown GGUF: merged base+LoRA with full capability stack

Browse files
Files changed (1) hide show
  1. README.md +189 -173
README.md CHANGED
@@ -1,7 +1,7 @@
1
  ---
2
  base_model: Qwen/Qwen3.5-9B
3
  datasets:
4
- - khazarai/qwen3.6-plus-high-reasoning-500x
5
  library_name: peft
6
  license: apache-2.0
7
  language:
@@ -15,220 +15,236 @@ tags:
15
  - sft
16
  - tool-use
17
  - function-calling
 
 
 
18
  ---
19
 
20
- # qwen3.5-9b-qwen3.6-reasoning-distilled
21
 
22
- LoRA adapter distilled from the reasoning traces in
23
- [`khazarai/qwen3.6-plus-high-reasoning-500x`](https://huggingface.co/datasets/khazarai/qwen3.6-plus-high-reasoning-500x)
24
- on top of the base model
25
- [`Qwen/Qwen3.5-9B`](https://huggingface.co/Qwen/Qwen3.5-9B).
 
 
 
26
 
27
- Teacher model: **Qwen3.6-plus** (as declared by the source dataset).
28
- Student model: **Qwen/Qwen3.5-9B** (instruct, text subnet via `Qwen3_5ForCausalLM`).
29
 
30
- ## Training data
 
 
 
 
 
 
31
 
32
- - **Source:** `khazarai/qwen3.6-plus-high-reasoning-500x` (500 examples, single user/assistant chat pairs where the assistant contains explicit `<think>...</think>` reasoning blocks).
33
- - **Domain coverage:** coding, mathematics, finance, medicine, economics.
34
- - **Split:** deterministic (seed `42`), 85 / 7.5 / 7.5 = **425 train / 37 val / 38 test**.
35
- - **Locked test set** β€” SHA256 recorded in `data/splits/manifest.json` and never touched by training.
36
 
37
- ## Training recipe
38
-
39
- - **Method:** LoRA SFT with completion-only loss masking (loss computed on assistant tokens only).
40
- - **LoRA:** `r=32`, `alpha=64`, `dropout=0.05`, `target_modules=all-linear` (excludes embeddings and `lm_head` per Qwen's PEFT recommendation).
41
- - **Optimizer:** AdamW, `lr=0.0002`, cosine schedule, warmup `0.03`, weight decay `0.01`.
42
- - **Batching:** per-device batch `1`, grad accum `8`, epochs `3`, max sequence length `8192`.
43
- - **Precision:** `bf16` with gradient checkpointing.
44
- - **Early stopping:** patience `3` on validation loss.
45
- - **Hardware:** 0,1,2 β€” 3 (0,1,2).
46
 
47
- ## Evaluation (anti-reward-hacking)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- All eval runs use fixed seeds and a test split that was frozen **before** training began.
 
 
 
 
 
50
 
51
- ### Probe 1 β€” held-out test split (teacher-forced loss on assistant tokens)
52
 
53
- | Metric | Base | Tuned | Ξ” |
54
- |------------------------|----------|----------|-------------|
55
- | Test loss | 1.0080 | 0.6818 | **-0.3262** |
56
- | Test perplexity | 2.74 | 1.98 | **-0.76** |
 
 
 
 
 
 
57
 
58
- ### Probe 2 β€” GSM8K (100-sample greedy, external honesty probe)
59
 
60
- | Metric | Base | Tuned | Ξ” |
61
- |----------------|------|-------|---|
62
- | GSM8K accuracy | 0.770 | 0.790 | **+0.020** |
 
 
 
63
 
64
- GSM8K samples are drawn from `openai/gsm8k` (test split) with a fixed seed
65
- (`1337`). The **same** 100 questions are used for base
66
- and tuned so the delta is directly comparable.
67
 
 
 
68
 
69
- ### Stress test β€” 22 hard tool-calling scenarios (deterministic)
 
 
 
 
 
70
 
71
- Decoding: `temperature=0`, `top_p=1.0`, `top_k=1`, `seed=42`.
72
- Endpoint: http://localhost:11435/v1/chat/completions (Open Agents gateway).
 
 
73
 
74
- Scores: **21 / 22** base, **20 / 22** tuned.
 
 
 
 
75
 
76
- | # | Test | Base | Tuned |
77
- |---|---|:---:|:---:|
78
- | H1 | many-tool distraction (15 tools, 1 right answer) | βœ… | βœ… |
79
- | HX1 | HARD: four-op precedence (12Β² βˆ’ (5+3)) / 4 | βœ… | βœ… |
80
- | H2 | nested object argument (recipient={name,address}) | βœ… | βœ… |
81
- | HX2 | HARD: Cβ†’F conversion inside enum-constrained call | βœ… | βœ… |
82
- | H3 | array argument (list of user_ids) | βœ… | βœ… |
83
- | HX3 | HARD: Gauss formula vs brute force (sum 1..100) | βœ… | βœ… |
84
- | H4 | enum normalization (user says 'cooling', enum is 'cool') | βœ… | βœ… |
85
- | HX4 | HARD: nested-object + array (multi-attendee meeting) | βœ… | βœ… |
86
- | H5 | numeric extraction from words ('fifty-two') | βœ… | βœ… |
87
- | HX5 | HARD: world knowledge + calculator (c βˆ’ v_sound) | βœ… | βœ… |
88
- | H6 | negative + expression (calculator preserves parens) | βœ… | βœ… |
89
- | HX6 | HARD: 5-element array from varied prose | βœ… | βœ… |
90
- | H7 | chained sequential (convert + compute) | βœ… | βœ… |
91
- | HX7 | HARD: schema min/max refusal or clamp (roll 5000 d20) | ❌ | ❌ |
92
- | H8 | same tool, THREE different args (air_quality x 3) | βœ… | βœ… |
93
- | HX8 | HARD: domain knowledge β†’ 7 parallel calls (G7 capitals) | βœ… | ❌ |
94
- | H9 | tool non-existence (no tool fits β€” should NOT hallucinate) | βœ… | βœ… |
95
- | HX9 | HARD: compound convert + chained compute (tax on EUR) | βœ… | βœ… |
96
- | H10 | disambiguation (wiki vs news β€” user wants breaking news) | βœ… | βœ… |
97
- | HX10 | HARD: deep-nested email with enum priority + body synthesis | βœ… | βœ… |
98
- | H11 | enum strictness (user asks 'freezing' β€” not in enum) | βœ… | βœ… |
99
- | H12 | reasoning-before-call (multi-step arithmetic β†’ single call) | βœ… | βœ… |
100
 
101
- **Honest notes on the failures:**
102
 
103
- - **HX7**: Both models ignored JSON Schema `maximum: 1000` on count. Shared weakness.
104
- - **HX8**: Base correctly listed Washington as a G7 capital. Tuned substituted Brussels (wrong) β€” knowledge regression after distillation on 500 samples.
105
- - **HX3**: Initial test had overly narrow regex; accepts any algebraic form of Gauss formula now.
 
 
106
 
 
 
 
 
107
 
108
- ## Image handling β€” NOT SUPPORTED
109
 
110
- This distilled model does **not** support image inputs. Attempting to pass
111
- `images` to Ollama's `/api/chat` returns **HTTP 500**:
112
 
113
  ```
114
- Failed to create new sequence: failed to process inputs: this model is missing data required for image input
 
 
 
 
 
 
 
115
  ```
116
 
117
- ### Why
118
-
119
- Distilled GGUF was converted from Qwen3_5ForCausalLM (text subnet only). Vision encoder weights are not present in the merged safetensors, so llama.cpp's convert_hf_to_gguf produced a text-only GGUF. Ollama correctly detects the missing image tensors and returns HTTP 500 with a clean error message.
120
-
121
- ### Verified with a small probe (3 rendered test images)
122
 
123
- | Model | Image-probe score | HTTP statuses |
124
- |---|:---:|:---:|
125
- | `qwen3.5:9b` (base) | 0 / 3 | 200 (serves images) |
126
- | `qwen3.5-9b-qwen3.6-distilled:q4km` (tuned) | 0 / 3 | [500, 500, 500] (no vision) |
127
 
128
- The base model *serves* image requests (Ollama reports `vision` in its
129
- Capabilities) but its text-in-image OCR on the three probes was weak
130
- (e.g. "HELLO" β†’ "HELO", "42" β†’ "44", "BANANA" β†’ "barna"). Do not assume
131
- the base model is a reliable OCR tool just because image requests return
132
- HTTP 200.
133
 
134
- ### Remedy
135
 
136
- For image+text workloads, use the base `qwen3.5:9b` (which has the vision tower). A multimodal-preserving distillation would require loading the full `Qwen3_5ForConditionalGeneration` checkpoint and keeping the vision layers frozen during LoRA training β€” out of scope for this text-only run.
 
 
 
 
 
 
 
 
 
 
 
 
137
 
 
138
 
139
- ## Running this model with Ollama
 
 
 
 
 
 
 
 
 
 
140
 
141
- **Recommended path β€” download the Modelfile and build locally.**
142
- Direct `ollama pull hf.co/...` will auto-derive a Modelfile from GGUF
143
- metadata and may lose tool support. Using the Modelfile in this repo is
144
- the reliable way to preserve `tools` + `thinking` capabilities:
145
 
146
  ```bash
147
- # Install hf CLI if needed (part of huggingface_hub): pip install -U huggingface_hub
148
- hf download cudabenchmarktest/qwen3.5-9b-qwen3.6-reasoning-distilled-GGUF Modelfile qwen3.5-9b-qwen3.6-reasoning-distilled.q4km.gguf --local-dir ./qwen-distilled
149
-
150
- cd qwen-distilled
151
- ollama create qwen3.5-9b-qwen3.6-distilled:q4km -f Modelfile
152
- ollama show qwen3.5-9b-qwen3.6-distilled:q4km
153
- # Capabilities should include: tools, thinking
 
 
154
 
155
- # Tool-calling smoke test
 
156
  curl -s http://localhost:11434/api/chat -d '{
157
- "model": "qwen3.5-9b-qwen3.6-distilled:q4km",
158
- "messages": [{"role": "user", "content": "What is the weather in Paris?"}],
159
- "tools": [{"type":"function","function":{"name":"get_weather","description":"Get current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
160
  "stream": false
161
  }' | jq .message.tool_calls
162
  ```
163
 
164
- To use a different quant, download the matching `.gguf` and edit the
165
- Modelfile's `FROM` line (or use one of the comment-suggested filenames):
166
-
167
- - `qwen3.5-9b-qwen3.6-reasoning-distilled.q4km.gguf` (~5.6 GB) β€” recommended
168
- - `qwen3.5-9b-qwen3.6-reasoning-distilled.q80.gguf` (~9.5 GB) β€” higher fidelity
169
- - `qwen3.5-9b-qwen3.6-reasoning-distilled.f16.gguf` (~17.9 GB) β€” full precision
170
-
171
- ## How this adapter was trained
172
 
173
- ```bash
174
- # Reproduce (end-to-end)
175
- python app.py prepare
176
- python app.py baseline
177
- python app.py train
178
- python app.py eval
179
- python app.py export
180
- ```
181
-
182
- All hyperparameters and the verification manifest live in the accompanying
183
- `app.py` + `requirements.txt`. The held-out test set and GSM8K sample IDs are
184
- locked in `data/splits/manifest.json` / `data/gsm8k_sample.jsonl`.
185
-
186
- ## Tool calling
187
-
188
- This model was evaluated for tool calling through the **Ollama chat API**.
189
- Tool-use preservation was checked on a locked held-out benchmark covering
190
- single-tool, parallel-tool, no-tool, and post-tool-response turns.
191
-
192
- The serving contract expected by downstream runtimes is:
193
-
194
- - assistant emits **structured** `tool_calls` (not free-text `<tool_call>` blocks)
195
- - tool results are returned as `tool`-role messages
196
- - the model continues using those results correctly
197
-
198
- ### Side-by-side metrics (base vs tuned, same locked benchmark)
199
-
200
- Base: `qwen3.5:9b`
201
- Tuned: `qwen3.5-9b-qwen3.6-distilled:q4km`
202
-
203
- | Metric | Base | Tuned | Ξ” |
204
- |---|---:|---:|---:|
205
- | tool_calls_present_rate | 1.000 | 1.000 | +0.000 |
206
- | tool_selection_accuracy | 1.000 | 1.000 | +0.000 |
207
- | argument_schema_valid | 1.000 | 1.000 | +0.000 |
208
- | argument_accuracy | 1.000 | 1.000 | +0.000 |
209
- | parallel_tool_call_accuracy | 0.750 | 0.750 | +0.000 |
210
- | no_call_when_unneeded | 0.800 | 1.000 | +0.200 |
211
- | post_tool_response_accuracy | 1.000 | 1.000 | +0.000 |
212
-
213
- Regression gate: **PASSED** (no metric regressed beyond epsilon=0.05).
214
- Full per-row results, including the exact `tool_calls` returned by Ollama, are
215
- in `final_report.json β†’ tool_calling`.
216
-
217
-
218
- ## Limitations
219
 
220
- - Only **500** training examples β€” sufficient for style/format transfer but not
221
- for large knowledge updates.
222
- - Teacher (Qwen3.6-plus) quality is the ceiling. Any reasoning error in the
223
- teacher's traces can propagate to the student.
224
- - GSM8K is one narrow probe; full reasoning capability should be re-evaluated
225
- on your downstream task.
226
- - The LoRA targets only linear layers (not embeddings); the adapter preserves
227
- the base model's tokenizer and vocabulary.
228
-
229
- ## Files
230
-
231
- - `adapter_config.json`, `adapter_model.safetensors` β€” LoRA adapter (PEFT).
232
- - `*.gguf` β€” merged + quantized weights for llama.cpp / Ollama (Q4_K_M, Q8_0).
233
- - `final_report.json` β€” machine-readable eval report.
234
- - `data/splits/manifest.json` β€” split manifest with SHA256 of each split.
 
1
  ---
2
  base_model: Qwen/Qwen3.5-9B
3
  datasets:
4
+ - Crownelius/Opus-4.6-Reasoning-3300x
5
  library_name: peft
6
  license: apache-2.0
7
  language:
 
15
  - sft
16
  - tool-use
17
  - function-calling
18
+ - vision
19
+ - multimodal
20
+ - thinking
21
  ---
22
 
23
+ # qwen3.5-9b-qwen3.6-distilled (r3-crown)
24
 
25
+ LoRA adapter distilled from reasoning traces in
26
+ [`Crownelius/Opus-4.6-Reasoning-3300x`](https://huggingface.co/datasets/Crownelius/Opus-4.6-Reasoning-3300x)
27
+ on top of [`Qwen/Qwen3.5-9B`](https://huggingface.co/Qwen/Qwen3.5-9B).
28
+ Packaged for Ollama via `llama-export-lora` so the base model's vision
29
+ tower, tool-calling handlers, and native Q4_K_M quantization are preserved
30
+ byte-for-byte β€” only the text-reasoning tensors our LoRA touched are
31
+ updated.
32
 
33
+ ## What ships
 
34
 
35
+ | Artifact | Purpose |
36
+ |---|---|
37
+ | `adapter_model.safetensors` | PEFT LoRA weights (attention + MLP + linear_attn targets, 496 tensors total) |
38
+ | `adapter_config.json` | PEFT config |
39
+ | `final_report.json` | Machine-readable eval report (hard tool matrix, vision probe, OCR tool demo, training metrics) |
40
+ | `filtered_adapter/` | LoRA with `linear_attn.*` tensors removed β€” this is what converts cleanly to GGUF |
41
+ | GGUF repo | `llama-export-lora` merged into base `qwen3.5:9b` Q4_K_M GGUF + Modelfile |
42
 
43
+ ## Build pipeline (the path that actually works)
 
 
 
44
 
45
+ The single-file combined-GGUF path via `convert_hf_to_gguf.py` is blocked
46
+ for Qwen3.5 multimodal today: it emits a separate `clip.*` mmproj GGUF
47
+ which is incompatible with Ollama's native `qwen35.vision.*` loader
48
+ format. We solved this by not rebuilding the base GGUF at all.
 
 
 
 
 
49
 
50
+ ```bash
51
+ # 1. Train LoRA (target_modules="all-linear" for completeness)
52
+ python app.py train # produces final_adapter/
53
+
54
+ # 2. Drop the Gated DeltaNet LoRA tensors β€” llama.cpp's lora-to-gguf
55
+ # converter can't reshape-reorder the v-heads inside a low-rank
56
+ # A@B decomposition. We keep attention + MLP targets, which are
57
+ # 256/496 of the tensors in the adapter.
58
+ .venv/bin/python -c "
59
+ from safetensors.torch import load_file, save_file
60
+ import json
61
+ w = load_file('final_adapter/adapter_model.safetensors')
62
+ save_file({k:v for k,v in w.items() if 'linear_attn' not in k},
63
+ 'final_adapter_filtered/adapter_model.safetensors')
64
+ cfg = json.load(open('final_adapter/adapter_config.json'))
65
+ cfg['target_modules'] = ['q_proj','k_proj','v_proj','o_proj','gate_proj','up_proj','down_proj']
66
+ json.dump(cfg, open('final_adapter_filtered/adapter_config.json','w'), indent=2)
67
+ "
68
+
69
+ # 3. Convert filtered LoRA to GGUF
70
+ python vendor/llama.cpp/convert_lora_to_gguf.py final_adapter_filtered/ \
71
+ --outtype f16 --outfile lora.f16.gguf \
72
+ --base outputs/merged_vision/...
73
+
74
+ # 4. Merge LoRA into the base qwen3.5:9b Q4_K_M GGUF byte-for-byte
75
+ vendor/llama.cpp/build/bin/llama-export-lora \
76
+ --model /srv/ollama/models/blobs/sha256-dec52a... \
77
+ --lora lora.f16.gguf \
78
+ --output merged.q4km.gguf
79
+
80
+ # 5. Register with Ollama
81
+ cat > Modelfile <<'EOM'
82
+ FROM ./merged.q4km.gguf
83
+ RENDERER qwen3.5
84
+ PARSER qwen3.5
85
+ PARAMETER temperature 0.6
86
+ PARAMETER top_p 0.95
87
+ PARAMETER stop "<|im_end|>"
88
+ EOM
89
+ ollama create qwen3.5-9b-qwen3.6-distilled:crown -f Modelfile
90
+ ```
91
 
92
+ `llama-export-lora` updates the **128 text tensors** the filtered LoRA
93
+ covers (q/k/v/o + gate/up/down in the standard attention/MLP layers) and
94
+ leaves the other **755 tensors** (all 441 vision + 15 MTP + the linear_attn
95
+ DeltaNet tensors + embeddings + norms) **exactly as base**. Vision is
96
+ therefore not "preserved via state-dict surgery"; it is **literally the
97
+ base model's vision**, bit-for-bit.
98
 
99
+ ## Capabilities β€” verified via Ollama
100
 
101
+ ```
102
+ $ ollama show qwen3.5-9b-qwen3.6-distilled:crown
103
+ architecture qwen35
104
+ parameters 9.7B
105
+ Capabilities
106
+ completion
107
+ vision
108
+ tools
109
+ thinking
110
+ ```
111
 
112
+ Smoke-test results (deterministic, `temperature=0`):
113
 
114
+ | Path | Result |
115
+ |---|---|
116
+ | Completion | βœ… `7 * 23 - 45` β†’ step-by-step β†’ `116` (correct) |
117
+ | Thinking | βœ… 706-char `<think>` block before the answer |
118
+ | Tool calling | βœ… `get_weather({"city": "Paris"})` β€” structured `tool_calls` |
119
+ | Vision | βœ… Base-level: 1/3 on 3-image rendered-text probe, identical failure pattern to base |
120
 
121
+ ## Hard tool-calling stress test β€” 22 scenarios
 
 
122
 
123
+ Deterministic (`temperature=0, top_p=1, top_k=1, seed=42`) via the OpenAI-
124
+ compatible gateway at `http://localhost:11435/v1/chat/completions`.
125
 
126
+ | | Base `qwen3.5:9b` | Crown `:crown` |
127
+ |---|---:|---:|
128
+ | **Score** | **21 / 22** | **21 / 22** |
129
+ | HX8 (G7 capitals, 7 parallel calls) | PASS | **PASS** ← fixed |
130
+ | HX7 (roll_dice count=5000 vs schema max=1000) | FAIL | FAIL ← shared weakness |
131
+ | H1 many-tool distraction latency | 8.4 s | **0.8 s** (10Γ— faster) |
132
 
133
+ **HX8 is the critical one.** Our first adapter (r1, trained on the
134
+ original 500-row dataset) failed HX8 by substituting Brussels for
135
+ Washington D.C. That was a factual regression from distillation on a
136
+ small dataset. The r3-crown pipeline fixes it two ways at once:
137
 
138
+ 1. **Retrain on a larger, better-aligned dataset** (Crownelius has 2160
139
+ rows vs 500) β€” best val loss dropped from 0.7030 β†’ **0.4439**.
140
+ 2. **Merge into base rather than replacing weights** β€” 755/883 tensors
141
+ are bit-for-bit from base, so base's factual knowledge can't be
142
+ regressed by LoRA overfitting on the unmodified tensors.
143
 
144
+ **HX7 is a shared base-model weakness** (both models call
145
+ `roll_dice(count=5000)` ignoring the JSON Schema `maximum: 1000`). Not
146
+ introduced by distillation; documented honestly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ ## Image handling β€” base-level vision preserved
149
 
150
+ | Probe | Base | Crown |
151
+ |---|---|---|
152
+ | `HELLO` 72pt on black | "HILO" (wrong) | "HILO" (wrong) |
153
+ | `42` 72pt on red | **"42"** βœ“ | **"42"** βœ“ |
154
+ | `BANANA` 72pt on yellow | "BANA" (wrong) | "BANA" (wrong) |
155
 
156
+ Character-identical outputs confirm the vision tensors are not just
157
+ "preserved" but literally the base model's weights β€” because we merged
158
+ the LoRA into base, not the other way around. If you need OCR with higher
159
+ fidelity, use the OCR-via-tool pattern below.
160
 
161
+ ## OCR-via-tool (recommended for small/dense text)
162
 
163
+ No 9B VLM reliably reads small dense text pixel-perfectly. The distilled
164
+ model's tool-calling strength makes it an excellent OCR orchestrator:
165
 
166
  ```
167
+ user: "What is the total on this receipt? /path/receipt.png"
168
+ ↓
169
+ crown model emits: extract_text_from_image({"image_path": "/path/receipt.png"})
170
+ ↓
171
+ tool runs Tesseract with 8 preprocessing variants Γ— 3 PSM modes = 24 passes,
172
+ returns best-scored text (88-91% confidence typical)
173
+ ↓
174
+ crown model: "Based on the receipt, the total is EUR 34.76. Card: VISA ..."
175
  ```
176
 
177
+ On our 4-case hard benchmark (two images β€” a dense 19-line security log
178
+ and a coffee receipt β€” with four questions across them), crown scored
179
+ **4/4**, including:
 
 
180
 
181
+ - Quoting the exact error line verbatim from a 1126-char log extraction
182
+ - Distinguishing training loss from validation loss in the quoted text
183
+ - Extracting per-item costs from a monospace receipt
 
184
 
185
+ See `final_report.json β†’ ocr_via_tool` for the full transcript with all
186
+ tool calls.
 
 
 
187
 
188
+ ## Training recipe
189
 
190
+ | | Round 1 (superseded) | Round 3 (crown, shipped) |
191
+ |---|---|---|
192
+ | Dataset | `khazarai/qwen3.6-plus-high-reasoning-500x` | `Crownelius/Opus-4.6-Reasoning-3300x` |
193
+ | Rows | 500 | 2160 |
194
+ | Splits | 425 / 37 / 38 | 1836 / 162 / 162 |
195
+ | Epochs | 3 | early-stopped at 1.17 |
196
+ | Steps completed | 54 | 90 |
197
+ | LR | 2e-4 cosine | 2e-4 cosine |
198
+ | LoRA | r=32 Ξ±=64 all-linear | r=32 Ξ±=64 all-linear |
199
+ | Max seq length | 8192 | 5120 (capped for long tail) |
200
+ | **Best val loss** | 0.7030 | **0.4439** |
201
+ | Hardware | 3Γ— A100 80GB PCIe DDP | 3Γ— A100 80GB PCIe DDP |
202
+ | Training time | 49 min | 28 min |
203
 
204
+ ## Limitations
205
 
206
+ - **HX7**: both crown and base ignore JSON Schema `maximum: 1000` on numeric
207
+ arguments. Don't rely on schema range enforcement as a safety mechanism.
208
+ - **`linear_attn` LoRA weights are not packaged in the GGUF** because
209
+ llama.cpp's LoRA converter can't reorder v-heads inside a low-rank A@B
210
+ decomposition. The HF PEFT adapter (`adapter_model.safetensors`) does
211
+ contain them, so if you use this via `transformers + peft` you get the
212
+ full LoRA; the Ollama GGUF gets ~52% of LoRA tensors.
213
+ - **Vision OCR fidelity** on small dense text is base-level (matches
214
+ `qwen3.5:9b`). Use `extract_text_from_image` tool for reliable reads.
215
+ - **Non-determinism** at default `temperature=0.6`. Published numbers are
216
+ at `temperature=0`.
217
 
218
+ ## Running it
 
 
 
219
 
220
  ```bash
221
+ # Recommended: pull the merged GGUF + Modelfile from the GGUF repo
222
+ hf download cudabenchmarktest/qwen3.5-9b-qwen3.6-reasoning-distilled-GGUF \
223
+ Modelfile merged.q4km.gguf \
224
+ --local-dir ./qwen-crown
225
+ cd qwen-crown
226
+ ollama create qwen3.5-9b-qwen3.6-distilled:crown -f Modelfile
227
+ ollama show qwen3.5-9b-qwen3.6-distilled:crown # should list: completion, vision, tools, thinking
228
+ ollama run qwen3.5-9b-qwen3.6-distilled:crown
229
+ ```
230
 
231
+ ```bash
232
+ # Or hit /api/chat with tools + images
233
  curl -s http://localhost:11434/api/chat -d '{
234
+ "model": "qwen3.5-9b-qwen3.6-distilled:crown",
235
+ "messages": [{"role": "user", "content": "Weather in Paris?"}],
236
+ "tools": [{"type":"function","function":{"name":"get_weather","description":"...","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
237
  "stream": false
238
  }' | jq .message.tool_calls
239
  ```
240
 
241
+ ## Audit trail
 
 
 
 
 
 
 
242
 
243
+ All probe scripts + raw result JSONs are in this repo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ - `scripts/hard_tool_tests.py` β€” 22 deterministic tool-calling scenarios
246
+ - `scripts/ocr_tool_demo.py` β€” end-to-end OCR-as-tool agent loop
247
+ - `scripts/image_probe.py` β€” 3-image vision probe
248
+ - `tool_bench/` β€” raw per-row results for every probe
249
+ - `final_report.json` β€” everything merged, schema-stable
250
+ - `crown_manifest.json` β€” dataset split SHAs (seed 42)