Instructions to use yuta24/ui-test-vision-2b-alpha with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use yuta24/ui-test-vision-2b-alpha with PEFT:
Task type is invalid.
- MLX
How to use yuta24/ui-test-vision-2b-alpha with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("yuta24/ui-test-vision-2b-alpha") config = load_config("yuta24/ui-test-vision-2b-alpha") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
File size: 12,667 Bytes
e379257 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | # Glint Decision Model PoC
Screenshot + natural-language questions + options → probability per option, computed in one forward pass
by a **Clef-style Joint Schema Head** on top of a **Qwen3.5 VLM backbone with LoRA** (no text generation).
The student is distilled from a teacher's option distributions (Clef / Clef-Flash) with a temperature KL loss,
plus cross-entropy (+ Brier) when a gold label exists.
> `glint/joint_schema_model.py` is Cloudflare's Clef-Flash code (Apache-2.0), see `NOTICE.md` and
> `LICENSE-CLEF-APACHE-2.0.txt`. The student head is freshly initialised (no Clef weights in the student). The released Clef-Flash weights
> (`Cloudflare/clef-flash`, Apache-2.0) are only run locally as the *teacher* by `scripts/label_local_clef.py`.
## Layout
| Path | What |
| --- | --- |
| `glint/joint_schema_model.py` | Clef `JointSchemaHead`, `ClefModel`, `encode_record`, `collate_records`, `systemone_answer` (Apache-2.0, unmodified) |
| `glint/model.py` | build student (Qwen3.5 + LoRA + head), `max_pixels` control, save/load checkpoint |
| `glint/data.py` | JSONL loader, teacher normalisation (Workers AI `noul` = p(true) is expanded), augmentation |
| `glint/losses.py` | `α·T²·KL(teacher_T‖student_T) + (1−α)·CE_ls(gold) + β·Brier` (+ box loss, see below) |
| `glint/box.py` | Jev-style `box` questions: `BoxHead`, DFL bins, IoU/GIoU, box loss, metrics, answer formatting |
| `glint/schema.py` | `encode_glint_record`: Clef `encode_record` + box fields (identical output when there are none) |
| `glint/glint_model.py` | `GlintModel(ClefModel)`: joint head + optional box head on one backbone pass |
| `make_synthetic.py` | tiny PIL dataset of iPhone-sized dialog screenshots, ja/en mixed, **DUMMY teacher** |
| `train.py` | training loop (batch 1 + grad accumulation, cosine LR, JSON log lines) |
| `infer.py` | load checkpoint → JSON with per-option probabilities + latency |
| `bench.py` | train step time / inference latency / peak memory for several `max_pixels` |
| `scripts/evaluate.py` | accuracy, NLL, Brier, ECE(15 bins), teacher agreement |
| `scripts/label_workers_ai.py` | replace dummy teacher with Clef via Workers AI (**billable**, dry-run by default) |
| `scripts/label_local_clef.py` | replace dummy teacher with locally-run Clef-Flash (open weights, bf16 on the iGPU, no cost); stores probs + logits + latency/memory stats |
| `scripts/teacher_report.py` | teacher-label accuracy / NLL / ECE vs gold, per-question accuracy, errors, example distributions |
| `tests/` | CPU unit tests (no model download): `python -m pytest tests` |
| `RESULTS.md` | measured numbers on the Windows + ROCm PC |
## Environment (Windows + ROCm, AMD Ryzen AI 9 HX 370 iGPU)
```powershell
$env:HF_HOME = "<HF_CACHE_DIR>"
$py = "<VENV>\Scripts\python.exe"
cd <REPO_DIR>
$env:HF_HUB_OFFLINE = "1" # models are already cached
# TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 is set by default inside the scripts (faster SDPA, see RESULTS.md).
# To disable: $env:TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL = "0"
```
Never `pip install` anything that pulls in `torch` into `venv-rocm` (it would replace the ROCm build).
Use `pip install --no-deps <pkg>`. No extra packages are required beyond what the venv already has
(`transformers 5.20.2`, `peft 0.21.2`, `safetensors`, `pillow`, `requests`).
## Quick start
```powershell
& $py make_synthetic.py --out data\synthetic --n-train 60 --n-eval 20
& $py train.py --data data\synthetic\train.jsonl --out checkpoints\syn-2b --model Qwen/Qwen3.5-2B --max-pixels 430080 --steps 200
& $py infer.py --ckpt checkpoints\syn-2b --jsonl data\synthetic\eval.jsonl --limit 3
& $py scripts\evaluate.py --ckpt checkpoints\syn-2b --data data\synthetic\eval.jsonl
& $py bench.py --data data\synthetic\eval.jsonl --pixels 430080,1048576
```
Single screenshot:
```powershell
& $py infer.py --ckpt checkpoints\syn-2b --image shot.png --questions questions.json --state '{"app":"MyApp"}'
```
`questions.json` uses the Clef / SystemOne schema:
```json
{"has_error": {"type": "noul", "instructions": "エラーダイアログが表示されているか?"},
"screen": {"type": "choice", "instructions": "What is the current screen state?",
"criteria": {"error_dialog": "An error alert is shown", "home": "Normal home screen", "loading": "Loading"}},
"severity": {"type": "score", "instructions": "How severe?", "criteria": ["none", "minor", "major"]}}
```
### Image token budget (`--max-pixels`)
Qwen3.5 processor: patch 16, merge 2 → 1 token per 32×32 px after resizing to ≤ `max_pixels`.
For a 1179×2556 iPhone portrait screenshot: `430080` → 390 tokens, `1048576` → 987 tokens, `244608` → 220 tokens.
The value is stored in the checkpoint (`glint_config.json`) and can be overridden at inference with `--max-pixels`.
### Main training options
`--temperature 2.0` (KD temperature), `--alpha 0.7` (KD weight when both teacher and gold exist; teacher-only → pure KD,
gold-only → pure CE), `--beta 0.1` (Brier), `--lora-r 32`, `--lr-lora 2e-4`, `--lr-head 5e-4`, `--accum 1`,
`--grad-ckpt` (gradient checkpointing for larger images), `--dtype bf16|fp16|fp32`.
LoRA targets only the language model (`q/k/v/o_proj`, MLP, and Gated-DeltaNet `in_proj_qkv/in_proj_z/out_proj`);
the vision encoder is frozen.
## Data format (JSONL, one record per line)
```json
{"id": "syn-000001",
"images": ["images/syn-000001.png"],
"state": {"app": "SampleApp", "locale": "ja-JP", "a11y": ["Alert: 通信エラー", "Button: OK"]},
"questions": {"screen": {"type": "choice", "instructions": "現在の画面状態は?",
"criteria": {"home": "ホーム画面", "error_dialog": "エラーダイアログ", "loading": "読み込み中"}},
"has_error": {"type": "noul", "instructions": "エラーメッセージが見えているか?"}},
"teacher": {"screen": {"error_dialog": 0.91, "home": 0.06, "loading": 0.03}, "has_error": 0.97},
"gold": {"screen": "error_dialog", "has_error": "true"},
"meta": {"source": "synthetic", "teacher_model": "@cf/cloudflare/clef-flash", "split_group": "SampleApp"}}
```
* `images` paths are relative to the JSONL file. `state` may be any string/JSON (put a11y tree / OCR text here).
* `teacher[qid]` is `{option_id: prob}`; for `noul` a bare float p(true) is accepted (Workers AI response shape).
SystemOne answer objects (`{"probabilities": ...}` / `{"noul": p}`) are accepted too.
* `gold` is optional; `teacher` is optional. Option ids for `noul` are `"true"/"false"`, for `score` `"0","1",...`.
* **The synthetic data's `teacher` is a DUMMY** (gold + noise, `meta.teacher_is_dummy=true`).
## Box questions
Like Jev 1.14, a question of type `box` asks for one rectangle in the (first) screenshot:
```json
{"primary_button_box": {"type": "box", "instructions": "the primary button in `screen`"},
"primary_button_box_visible": {"type": "noul", "instructions": "Is the primary button visible?"}}
```
Answer (`infer.py`), edges as fractions of image height (`top`/`bottom`) and width (`left`/`right`) from the top-left,
always `top < bottom`, `left < right`:
```json
{"primary_button_box": {"type": "box", "box": {"top": 0.534, "bottom": 0.589, "left": 0.500, "right": 0.859}, "confidence": 0.83}}
```
There is no "not found" answer: ask presence as a separate `noul` question (convention: `<qid>_visible`).
* **Data.** Box gold comes from ground truth (capture / annotation), never from the teacher; `teacher` entries for box
questions are ignored and the label scripts do not send box questions to Clef.
`gold[qid] = {"top": .., "bottom": .., "left": .., "right": .., "present": true}` (0–1; `{"box": {...}}` is accepted too;
inverted edges are swapped, values clamped). `{"present": false}` skips the box loss for that record, and if a
`<qid>_visible` noul question exists without gold it gets `"false"` (`"true"` when present).
* **Head.** `BoxHead` reads the same backbone hidden states as the joint head: the question span is pooled and 4 edge
queries (top/bottom/left/right) cross-attend to the record's tokens; image tokens get a 2D Fourier encoding of their
patch centre. Each edge outputs logits over `--box-bins 64` bins (DFL/GFL style); the edge is the expectation over
bin centres `i/(K−1)`, then top/bottom and left/right are sorted. `confidence` is a separate sigmoid head trained to
predict the IoU of its own box (BCE against the detached IoU); `evaluate.py` also reports the bin max-prob alternative.
* **Loss.** `box = dfl·DFL + l1·L1 + giou·(1−GIoU) + conf·BCE(conf, IoU)` (`--box-dfl 1 --box-l1 5 --box-giou 2 --box-conf 1`),
added to the total as `--box-weight 1.0 · box`. `--box-head auto` (default) builds the head only when the training data
contains box questions; `on`/`off` force it. Without box questions nothing changes (same encoding, losses, logs, checkpoint).
* **Eval.** `scripts/evaluate.py` adds a `box` block: `mean_iou`, `iou_ge_0.5`, `iou_ge_0.75`, `mean_confidence`,
`ece_15bin_conf_vs_iou50` (confidence vs. IoU≥0.5) and `ece_15bin_maxprob_vs_iou50`.
`bench.py` builds the box head (and adds the box loss) only when its data contains box questions.
* **Synthetic.** `make_synthetic.py --box` adds `primary_button_box` + `primary_button_box_visible` with gold from the drawn
button rectangle (absent on loading/home). Without `--box` the output is byte-identical to before.
```powershell
& $py make_synthetic.py --out data\synthetic-box --n-train 60 --n-eval 20 --box
& $py train.py --data data\synthetic-box\train.jsonl --out checkpoints\syn-box-2b --steps 200
& $py scripts\evaluate.py --ckpt checkpoints\syn-box-2b --data data\synthetic-box\eval.jsonl
```
## Swapping in real teacher labels
```powershell
# dry run (no requests, no cost): prints request sizes
& $py scripts\label_workers_ai.py data\synthetic\train.jsonl data\synthetic\train.clef.jsonl --model clef-flash
# real (billable) run
$env:CLOUDFLARE_ACCOUNT_ID = "..."; $env:CLOUDFLARE_API_TOKEN = "..."
& $py scripts\label_workers_ai.py data\real\train.jsonl data\real\train.clef.jsonl --model clef-flash --run
```
Image files are re-encoded to JPEG ≤ ~190 KB and sent as base64 data URLs (max 4 images per request).
Alternatively run the open-weight teacher locally (tested on the Windows + ROCm PC, see RESULTS.md; ~3.1 s/image at 390 image tokens, 18.0 GB peak):
```powershell
$env:HF_HUB_OFFLINE = "0" # first run only: downloads Cloudflare/clef-flash (~19.1 GB) into HF_HOME
& $py scripts\label_local_clef.py data\synthetic\train.jsonl data\synthetic\train.clef.jsonl --max-pixels 430080 --stats runs\clef-label-train-390.json
& $py scripts\label_local_clef.py data\synthetic\eval.jsonl data\synthetic\eval.clef.jsonl --max-pixels 430080
& $py scripts\teacher_report.py data\synthetic\train.clef.jsonl data\synthetic\eval.clef.jsonl
& $py train.py --data data\synthetic\train.clef.jsonl --out checkpoints\clef-2b-390 --steps 200 --max-pixels 430080
& $py scripts\evaluate.py --ckpt checkpoints\clef-2b-390 --data data\synthetic\eval.clef.jsonl
```
Output records get `teacher` (`{qid: {option: prob}}`), `teacher_logits`, and `meta.teacher_model/teacher_max_pixels/teacher_image_tokens`,
with `meta.teacher_is_dummy=false`. Options: `--limit N` (smoke test), `--max-pixels` (Clef-Flash release default is
`longest_edge=16777216`, i.e. effectively native resolution, ~2.9k tokens for 1179x2556), `--offload` (accelerate
`device_map=auto` CPU-offload fallback; not needed on that PC, untested).
## Checkpoint layout
```
checkpoints/<run>/adapter/ # PEFT LoRA adapter (adapter_config.json, adapter_model.safetensors)
checkpoints/<run>/head.safetensors # JointSchemaHead weights
checkpoints/<run>/box_head.safetensors # BoxHead weights (only when trained with box questions)
checkpoints/<run>/glint_config.json # base model, max_pixels, dtype, head/LoRA config, train args, loss history
```
`infer.py` merges LoRA into the backbone by default (`--no-merge` to keep it separate).
## Known limitations (Windows + ROCm)
* `flash-linear-attention` needs Triton; `triton-windows` targets NVIDIA only, so the Gated-DeltaNet layers run the reference PyTorch path (slow). Not installed.
* Training is batch 1 (+ `--accum`); `collate_records` supports batching but is untested here.
* Synthetic data ships with a DUMMY teacher (`train.jsonl`/`eval.jsonl`); real Clef-Flash labels are in `*.clef.jsonl`
(generated by `scripts/label_local_clef.py`; `data/` is git-ignored, so regenerate them on another machine).
* The synthetic task is easy: Clef-Flash is 100% correct on it, so it does not yet tell us much about real iOS screens.
|