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
|
Download code/README.md from yuta24/ui-test-vision-2b-alpha: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/yuta24/ui-test-vision-2b-alpha/resolve/main/code/README.md
- Command line
-
hf download hf://yuta24/ui-test-vision-2b-alpha/code/README.md
-
curl -L -o README.md https://huggingface.co/yuta24/ui-test-vision-2b-alpha/resolve/main/code/README.md
12.7 kB
| # 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. | |