Feature Extraction
Transformers
Safetensors
English
multilingual
laya_browser
laya
custom_code
system-1
browser-agent
web-navigation
decision-model
mmbert
mind2web
tilelang
Instructions to use cklxx/laya-browser with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use cklxx/laya-browser with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="cklxx/laya-browser", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("cklxx/laya-browser", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
laya-browser v10 / v10s: laya fine-tuned as a browser-agent decision head + code + results
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- README.md +111 -0
- code/README.md +35 -0
- code/apps/BROWSER_AGENT.md +34 -0
- code/apps/browser_diag.py +56 -0
- code/apps/browser_suite.py +88 -0
- code/apps/browser_task.py +18 -0
- code/apps/common.py +61 -0
- code/apps/fast_batch.py +89 -0
- code/apps/hn_radar.py +44 -0
- code/apps/inbox_triage.py +43 -0
- code/apps/moderator.py +47 -0
- code/apps/profile_step.py +17 -0
- code/apps/systemone_server.py +169 -0
- code/apps/web_console.py +71 -0
- code/env.sh +6 -0
- code/finetune/README.md +83 -0
- code/finetune/build_items.py +67 -0
- code/finetune/calibrate.py +43 -0
- code/finetune/collect_pages.py +107 -0
- code/finetune/common_ft.py +77 -0
- code/finetune/convert_mind2web.py +116 -0
- code/finetune/dagger.py +121 -0
- code/finetune/eval.py +33 -0
- code/finetune/gen_goals.py +85 -0
- code/finetune/gen_step2.py +50 -0
- code/finetune/make_done_cases.py +47 -0
- code/finetune/rollouts.py +109 -0
- code/finetune/run_after_v6.sh +30 -0
- code/finetune/run_all.sh +16 -0
- code/finetune/run_final.sh +20 -0
- code/finetune/run_gated.sh +17 -0
- code/finetune/run_train.sh +9 -0
- code/finetune/run_v10.sh +32 -0
- code/finetune/run_v10s.sh +24 -0
- code/finetune/run_v2.sh +11 -0
- code/finetune/run_v3.sh +11 -0
- code/finetune/run_v4.sh +13 -0
- code/finetune/run_v5.sh +14 -0
- code/finetune/run_v6.sh +14 -0
- code/finetune/run_v7.sh +20 -0
- code/finetune/run_v8.sh +18 -0
- code/finetune/run_v9.sh +29 -0
- code/finetune/train.py +73 -0
- code/jev-ultrafast.patch +536 -0
- code/kernels/bench_fast.py +110 -0
- code/kernels/fast.py +195 -0
- code/kernels/test_fast.py +85 -0
- code/kernels/tl_kernels.py +214 -0
- results/fast_english_rtx4070.json +93 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
v10s/tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: convaiinnovations/laya
|
| 4 |
+
language:
|
| 5 |
+
- en
|
| 6 |
+
- multilingual
|
| 7 |
+
tags:
|
| 8 |
+
- laya
|
| 9 |
+
- system-1
|
| 10 |
+
- browser-agent
|
| 11 |
+
- web-navigation
|
| 12 |
+
- decision-model
|
| 13 |
+
- modernbert
|
| 14 |
+
- mind2web
|
| 15 |
+
- tilelang
|
| 16 |
+
datasets:
|
| 17 |
+
- osunlp/Mind2Web
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# laya-browser — laya fine-tuned as a browser-agent decision head (drop-in replacement for TypeSafe Jev)
|
| 21 |
+
|
| 22 |
+
**laya** ([convaiinnovations/laya](https://huggingface.co/convaiinnovations/laya)) is a non-autoregressive "System 1" decision model:
|
| 23 |
+
one bidirectional encoder pass answers several typed questions (`choice` / `score` / `noul`) with calibrated probabilities, no text generation.
|
| 24 |
+
Out of the box it is near chance at browser decisions ("which element should I click for this goal?" — top-1 0.10 among ~45 candidates).
|
| 25 |
+
|
| 26 |
+
This repo is what it took to turn it into a usable decision head for [browser-use/jev-ultrafast](https://github.com/browser-use/jev-ultrafast),
|
| 27 |
+
whose `/v1/systemone` request format is identical to laya's `predict(state, questions)`. Everything was done locally on one RTX 4070 Ti SUPER (16 GB),
|
| 28 |
+
no paid API: the text helper and the DAgger teacher are a local Qwen3-8B-AWQ served by sglang.
|
| 29 |
+
|
| 30 |
+
## What changed relative to the original laya
|
| 31 |
+
|
| 32 |
+
| | original laya (typed-decisions) | this repo |
|
| 33 |
+
|---|---|---|
|
| 34 |
+
| browser decision quality (16 real tasks × 3 runs) | 0 % | **v10: 58 %**, v10s: 50 % |
|
| 35 |
+
| element top-1 on held-out pages (2,734 decisions, ~45 candidates) | 0.10 | **0.66** (v10), 0.63 (v10s) |
|
| 36 |
+
| operation accuracy (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88 |
|
| 37 |
+
| latency per browser step (3 questions, 30–65 candidates) | 50–200 ms | 41–50 ms (v10), **17–23 ms** (v10s) |
|
| 38 |
+
| backbone | ModernBERT-large 421M | v10: same; v10s: mmBERT-base 322M |
|
| 39 |
+
| input format | jev's state verbatim (element table as JSON inside the state, truncated by the 1024-token window) | **format v2/v3**: elements live only in the option list (full label + role + current value), state keeps title / URL / history / 1.2–1.5k chars of text, `head_max_len` 512 → 768 |
|
| 40 |
+
| training data | LocalLLaMA/typed-decisions | 5,244 reverse-generated goals on 421 crawled pages (Qwen writes "the goal a user would state to need this element"), 700 real DONE states (clicks actually executed), 659 step-2 negatives, [Mind2Web](https://huggingface.co/datasets/osunlp/Mind2Web) train (7,296 steps, candidates re-rendered as an element table), 177 on-policy DAgger corrections |
|
| 41 |
+
| training | — | laya's RLCD recipe (noisy-logit policy gradient + soft CE), single GPU, no gradient checkpointing, 4 epochs (~2 h for v10, ~1 h for v10s), post-hoc temperature |
|
| 42 |
+
| inference | HF eager + autocast | optional TileLang fast path ([PR #25 to laya](https://github.com/NandhaKishorM/laya/pull/25)): fused GEMM/GEGLU/LayerNorm/RoPE, sliding-window flash attention, bf16-resident weights, CUDA graphs — 4–5× lower per-call latency, identical answers |
|
| 43 |
+
|
| 44 |
+
### Things that did **not** work (so you don't repeat them)
|
| 45 |
+
- Templated DONE goals ("Open the page titled X, stop once it is open") leak phrasing: the model learns *stop when ⇒ DONE*. DONE samples must be real landing pages after an executed action.
|
| 46 |
+
- If every DONE sample has exactly one prior action and every click sample has none, the model learns *any history ⇒ DONE*. Add mid-task negatives (step-2 goals on landing pages).
|
| 47 |
+
- Mind2Web alone kills DONE / TYPE_TEXT (no DONE there, CLICK dominates): re-weight rare operations (DONE ×4, TYPE_TEXT/SELECT ×3).
|
| 48 |
+
- Cutting page text to 3,000 chars saved nothing (the sequence is dominated by the head) and cost 0.04 top-1.
|
| 49 |
+
- `torch.compile` on variable-length batches recompiles per shape: 6× slower.
|
| 50 |
+
- Confidence-gated escalation to Qwen3-8B (System 2) made things *worse* (58 % → 42 %): on these pages the fine-tuned 322M/421M model is a better decider than an 8B general LLM. Use a stronger System 2 or none.
|
| 51 |
+
- jev's DOM reader hides password fields by design (login tasks are impossible) and never sees collapsed menus (Wikipedia's "Random article").
|
| 52 |
+
|
| 53 |
+
### What still fails
|
| 54 |
+
Multi-step "type then submit / pick a suggestion" flows, anything that needs scrolling first (the training data had no scroll samples), `<select>` on unseen sites, and long flows like Google Flights. Scripted trajectories for scroll / search-submit / select are the next data step (`code/finetune/rollouts.py`).
|
| 55 |
+
|
| 56 |
+
## Files
|
| 57 |
+
|
| 58 |
+
```
|
| 59 |
+
v10/ ModernBERT-large 421M, format v2, head_max_len 768 (best accuracy)
|
| 60 |
+
v10s/ mmBERT-base 322M, format v3, head_max_len 768 (17–23 ms per step)
|
| 61 |
+
code/ finetune pipeline, laya systemone server, task suite, TileLang kernels, jev-ultrafast patch
|
| 62 |
+
results/ per-run suite JSONs and logs behind every number above
|
| 63 |
+
```
|
| 64 |
+
Each checkpoint is a laya checkpoint directory (`model.safetensors`, `encoder/`, `tokenizer/`, `rl_agent_config.json`); the config records
|
| 65 |
+
`laya_fmt` and `head_max_len_train` so the server applies the matching input format automatically.
|
| 66 |
+
|
| 67 |
+
## Use
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
huggingface-cli download cklxx/laya-browser --local-dir laya-browser
|
| 71 |
+
cd laya-browser/code && uv sync --extra fast # pinned uv.lock (Python 3.12, torch 2.11, tilelang 0.1.14)
|
| 72 |
+
uv run python verify.py v10s # downloads v10s if needed, answers one recorded browser step
|
| 73 |
+
uv run python verify.py v10s --fast # same through the TileLang fast path
|
| 74 |
+
```
|
| 75 |
+
Verified from a clean environment on 2026-09-21 (RTX 4070 Ti SUPER): `TYPE_TEXT → [2] Search Wikipedia (searchbox)`, 35 ms per step
|
| 76 |
+
stock / 28 ms with the fast path on a 65-option, 2.5k-token step. Extras: `--extra data` (Mind2Web conversion, dataset eval),
|
| 77 |
+
`--extra browser` (live suite / crawling; also needs jev-ultrafast with `code/jev-ultrafast.patch` applied and a Chromium with
|
| 78 |
+
`--remote-debugging-port=9222`).
|
| 79 |
+
|
| 80 |
+
```python
|
| 81 |
+
import laya
|
| 82 |
+
agent = laya.load("laya-browser/v10s") # a local laya checkpoint dir
|
| 83 |
+
agent.cfg["head_max_len"] = agent.cfg["head_max_len_train"]
|
| 84 |
+
# state / questions exactly as jev-ultrafast's model.choose() builds them, after the format-v3 transform in code/apps/systemone_server.py
|
| 85 |
+
result = agent.predict(state, questions)
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
As a TypeSafe replacement for jev-ultrafast:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
# in code/: laya systemone-compatible server (format transform + optional gating + DAgger logging)
|
| 92 |
+
python apps/systemone_server.py 8791 /path/to/laya-browser/v10s 999
|
| 93 |
+
# in jev-ultrafast (apply code/jev-ultrafast.patch): TYPESAFE_BASE_URL=http://127.0.0.1:8791
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
`code/apps/browser_suite.py` runs the 16-task real-browser suite with automatic outcome checks (`REPEATS=3`).
|
| 97 |
+
|
| 98 |
+
## Reproduce
|
| 99 |
+
|
| 100 |
+
`code/finetune/README.md` documents every step (crawl → reverse-generate goals → execute clicks for DONE → step-2 → Mind2Web conversion →
|
| 101 |
+
DAgger → build → train → calibrate → eval → suite) with the exact scripts (`run_v10.sh`, `run_v10s.sh`, `run_final.sh`) and all intermediate
|
| 102 |
+
numbers from v1 to v10s.
|
| 103 |
+
|
| 104 |
+
## GPU cost
|
| 105 |
+
|
| 106 |
+
v10s: ~0.65 GB weights, ~1.5 GB VRAM resident with CUDA graphs, 17–23 ms per 3-question browser step, 3 ms for a single-question call.
|
| 107 |
+
v10: ~0.85 GB weights, ~1.8 GB VRAM, 41–50 ms per step. The Qwen text helper (only needed for TYPE_TEXT values) is separate.
|
| 108 |
+
|
| 109 |
+
## License
|
| 110 |
+
|
| 111 |
+
Apache-2.0, same as laya. Mind2Web is used under its own license for training only.
|
code/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Laya 本机加速 + 应用
|
| 2 |
+
|
| 3 |
+
[convaiinnovations/laya](https://huggingface.co/convaiinnovations/laya):非自回归“System 1”决策模型(ModernBERT/mmBERT 编码器 + 决策头),
|
| 4 |
+
一次前向同时回答多个 `choice / score / noul` 问题,输出校准概率,不生成文本。
|
| 5 |
+
|
| 6 |
+
## 环境
|
| 7 |
+
|
| 8 |
+
```fish
|
| 9 |
+
source env.sh # HF 镜像 + 清华 PyPI + 关闭代理
|
| 10 |
+
.venv/bin/python ... # Python 3.12, torch 2.11+cu130, tilelang 0.1.14
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## 应用(apps/)
|
| 14 |
+
|
| 15 |
+
| 脚本 | 说明 |
|
| 16 |
+
|---|---|
|
| 17 |
+
| `apps/inbox_triage.py [正文]` | 多语言工单分诊:部门 / 紧急度 / 流失风险 / 情绪 |
|
| 18 |
+
| `apps/moderator.py [file]` | 评论审核台:有害 / 垃圾 / 主题 / 严重度,按风险排序 |
|
| 19 |
+
| `apps/hn_radar.py [N]` | 拉 Hacker News 热帖实时打标签 |
|
| 20 |
+
| `apps/web_console.py [port]` | 零依赖 Web 决策台,自定义问题 JSON,实时概率条 |
|
| 21 |
+
|
| 22 |
+
在任何应用里加两行即可启用加速:
|
| 23 |
+
```python
|
| 24 |
+
import sys; sys.path.insert(0, "kernels")
|
| 25 |
+
from fast_laya import accelerate; accelerate(agent)
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## TileLang 加速(kernels/)
|
| 29 |
+
|
| 30 |
+
* `tl_kernels.py` — GEMM(+bias/act 融合)、GEMM+GEGLU 融合、残差+LayerNorm 融合、原地 RoPE、
|
| 31 |
+
带 padding 掩码与滑动窗口的 flash attention。行数 M 为运行时符号,每个 kernel 只编译一次。
|
| 32 |
+
* `fast_laya.py` — 用上述 kernel 重写整个编码器 + 决策头前向,bf16 权重常驻,按 (batch, L) 桶捕获 CUDA Graph。
|
| 33 |
+
`accelerate(agent)` 原地替换 `agent.model.forward`。
|
| 34 |
+
* `test_kernels.py` / `verify_and_bench.py` — 单 kernel 对拍、端到端数值一致性与延迟对比。
|
| 35 |
+
* `tune.py` — tile 参数扫描。
|
code/apps/BROWSER_AGENT.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# laya 作为浏览器 agent 的决策模型(替代 TypeSafe Jev)
|
| 2 |
+
|
| 3 |
+
[jev-ultrafast](https://github.com/browser-use/jev-ultrafast) 的 `/v1/systemone` 请求/响应格式与 `laya.predict(state, questions)` 完全一致,
|
| 4 |
+
所以本地 laya(+TileLang 加速)可以直接替代 TypeSafe API。克隆在 `../jev-ultrafast`,`model.py` 已打补丁支持
|
| 5 |
+
`TYPESAFE_BASE_URL` 和 `TEXT_MODEL_EXTRA_JSON`。
|
| 6 |
+
|
| 7 |
+
```fish
|
| 8 |
+
# 1. 无头 Chromium(远程调试端口 9222)
|
| 9 |
+
chromium --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile about:blank &
|
| 10 |
+
# 2. 本地 laya systemone 服务(typed | multilingual | english;第 3 个参数是选项分块宽度)
|
| 11 |
+
source env.sh; .venv/bin/python apps/systemone_server.py 8791 typed 12 &
|
| 12 |
+
# 3. TYPE_TEXT 用本地 sglang + Qwen(可选)
|
| 13 |
+
HF_HUB_OFFLINE=1 ~/sglang-venv/bin/python -m sglang.launch_server --model-path Qwen/Qwen3-8B-AWQ --port 30000 \
|
| 14 |
+
--mem-fraction-static 0.5 --context-length 8192 --reasoning-parser qwen3 &
|
| 15 |
+
# 4. 跑任务
|
| 16 |
+
cd ../jev-ultrafast; .venv/bin/python ../laya/apps/browser_task.py "https://en.wikipedia.org/wiki/Main_Page" "Click the 'Random article' link."
|
| 17 |
+
# 诊断:真实元素表上直接问 laya 该点哪个(格式 jev|compact,后面是打乱平均次数)
|
| 18 |
+
.venv/bin/python ../laya/apps/browser_diag.py compact 4
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
`systemone_server.py` 做了两件适配:把 jev 的 dict 型元素描述压成一行字符串;超过 N 个选项的 choice 问题
|
| 22 |
+
分块粗筛 + 决赛(两次前向),53 个元素约 150 ms。
|
| 23 |
+
|
| 24 |
+
## 结论(2026-09-20,零样本,Wikipedia 首页 46 个可点元素)
|
| 25 |
+
|
| 26 |
+
| checkpoint | jev 原格式 top-1 | 精简格式 top-1 | 精简+4 次打乱平均 top-1 | 平均排名 |
|
| 27 |
+
|---|---|---|---|---|
|
| 28 |
+
| laya (english) | 0/3 | 0/3 | 1/3 | 2.7 / 46 |
|
| 29 |
+
| laya-multilingual | 0/3 | 0/3 | 0/3 | 5.3 / 46 |
|
| 30 |
+
| laya-typed-decisions | 0/3 | 1/3 | 1/3 | 3.7 / 46 |
|
| 31 |
+
|
| 32 |
+
管线(Chromium → browser-harness → jev 循环 → 本地 laya)跑通且每步 60-150 ms,但三个 checkpoint 零样本都
|
| 33 |
+
无法可靠选中目标元素(强烈偏向第 1 个选项),端到端任务全部失败。这与 laya 自己的说明一致:typed-decisions
|
| 34 |
+
零样本 0.36,需要针对任务微调。要真正替代 Jev,下一步是用 Qwen 当 teacher 在录制的页面状态上生成决策数据,微调 laya。
|
code/apps/browser_diag.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Direct diagnostic: can laya pick the right element on a real Wikipedia element table, under different prompt formats?"""
|
| 2 |
+
import json, os, random, sys, time
|
| 3 |
+
import httpx
|
| 4 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 5 |
+
os.environ["BU_CDP_URL"] = "http://127.0.0.1:9222"
|
| 6 |
+
from jev_ultrafast.browser import Browser
|
| 7 |
+
from jev_ultrafast.model import action_space
|
| 8 |
+
from jev_ultrafast.questions import NEXT_ACTION, TARGET
|
| 9 |
+
|
| 10 |
+
S1 = "http://127.0.0.1:8791/v1/systemone"
|
| 11 |
+
page = None
|
| 12 |
+
def get_page():
|
| 13 |
+
global page
|
| 14 |
+
if page is None:
|
| 15 |
+
b = Browser("https://en.wikipedia.org/wiki/Main_Page"); page = b.observe(screenshot=False); b.close()
|
| 16 |
+
return page
|
| 17 |
+
|
| 18 |
+
GOALS = [("Click the 'Random article' link in the navigation.", "Random article"),
|
| 19 |
+
("Open the site's search box so a query can be typed.", "Search"),
|
| 20 |
+
("Log in to Wikipedia.", "Log in"),
|
| 21 |
+
("Open the Talk page for the main page.", "Talk"),
|
| 22 |
+
("Read the community portal.", "Community portal")]
|
| 23 |
+
|
| 24 |
+
def ask(state, questions):
|
| 25 |
+
r = httpx.post(S1, json={"model": "x", "state": state, "questions": questions}, timeout=120).json()
|
| 26 |
+
return r["answers"]
|
| 27 |
+
|
| 28 |
+
def run(fmt, K=1):
|
| 29 |
+
p = get_page(); elements, targets, controls = action_space(p["actions"])
|
| 30 |
+
cands = targets["CLICK"]
|
| 31 |
+
hits, ranks = 0, []
|
| 32 |
+
for goal, want in GOALS:
|
| 33 |
+
gold = [i for i, a in cands.items() if a["label"].split(" → ")[0].strip().lower() == want.lower()]
|
| 34 |
+
if not gold: print(" no gold for", want); continue
|
| 35 |
+
if fmt == "jev":
|
| 36 |
+
crit = {i: {"element": f"[{i}] {a['label']}", "current_value": a.get("current_value", a.get("value", "")), **{k: a[k] for k in ("role", "checked", "selected", "expanded") if k in a}} for i, a in cands.items()}
|
| 37 |
+
state = {"page": {k: p[k] for k in ("url", "title", "text")}, "elements": elements, "recent_actions": []}
|
| 38 |
+
ins = {"goal": goal, "operation": "CLICK", "rules": [NEXT_ACTION, TARGET]}
|
| 39 |
+
else:
|
| 40 |
+
crit = {i: a["label"].split(" → ")[0][:60] for i, a in cands.items()}
|
| 41 |
+
state = {"goal": goal, "page_title": p["title"], "url": p["url"]}
|
| 42 |
+
ins = f"Goal: {goal} Which element should be clicked next?"
|
| 43 |
+
probs = {i: 0.0 for i in crit}
|
| 44 |
+
for k in range(K):
|
| 45 |
+
keys = list(crit); random.Random(k).shuffle(keys) if K > 1 else None
|
| 46 |
+
a = ask(state, {"t": {"type": "choice", "instructions": ins, "criteria": {i: crit[i] for i in keys}}})["t"]
|
| 47 |
+
for i, v in a["probabilities"].items(): probs[i] += v / K
|
| 48 |
+
order = sorted(probs, key=probs.get, reverse=True)
|
| 49 |
+
rank = min(order.index(g) for g in gold) + 1
|
| 50 |
+
ranks.append(rank); hits += rank == 1
|
| 51 |
+
print(f" {want:18s} gold={gold} top={order[:3]} p_gold={max(probs[g] for g in gold):.3f} rank={rank}")
|
| 52 |
+
print(f" => top1 {hits}/{len(ranks)} mean rank {sum(ranks)/len(ranks):.1f} of {len(cands)}")
|
| 53 |
+
|
| 54 |
+
fmt = sys.argv[1]; K = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
| 55 |
+
print(f"== format={fmt} permutations={K} variant={httpx.get('http://127.0.0.1:8791/').json()['variant']}")
|
| 56 |
+
run(fmt, K)
|
code/apps/browser_suite.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Real-task suite for the browser agent with automatic outcome checks (URL / title / page text).
|
| 2 |
+
|
| 3 |
+
python apps/browser_suite.py [name-filter] (services: chromium 9222, laya systemone 8791, sglang 30000)
|
| 4 |
+
|
| 5 |
+
Prints one line per task: PASS/FAIL, steps, wall time, and a summary table.
|
| 6 |
+
"""
|
| 7 |
+
import json, os, re, sys, time
|
| 8 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 9 |
+
os.environ.update(BU_CDP_URL="http://127.0.0.1:9222", TYPESAFE_BASE_URL="http://127.0.0.1:8791", TYPESAFE_API_KEY="local",
|
| 10 |
+
TEXT_MODEL_API_KEY="local", TEXT_MODEL_BASE_URL="http://127.0.0.1:30000/v1", TEXT_MODEL="Qwen/Qwen3-8B-AWQ",
|
| 11 |
+
TEXT_MODEL_EXTRA_JSON='{"chat_template_kwargs": {"enable_thinking": false}}')
|
| 12 |
+
from jev_ultrafast import Agent
|
| 13 |
+
|
| 14 |
+
# (name, url, goal, check(url, title, text) -> bool)
|
| 15 |
+
TASKS = [
|
| 16 |
+
# NOTE: Wikipedia's 'Random article' link sits in a collapsed menu that the DOM reader never observes -> search task instead
|
| 17 |
+
("wiki-einstein", "https://en.wikipedia.org/wiki/Main_Page", "Open the Wikipedia article about Albert Einstein.",
|
| 18 |
+
lambda u, t, x: "Albert_Einstein" in u),
|
| 19 |
+
("wiki-search", "https://en.wikipedia.org/wiki/Main_Page", "Search Wikipedia for 'Python programming language' and open the article about the Python language.",
|
| 20 |
+
lambda u, t, x: "Python" in t),
|
| 21 |
+
("hn-new", "https://news.ycombinator.com/", "Open the 'new' page that lists the newest submissions.",
|
| 22 |
+
lambda u, t, x: u.rstrip("/").endswith("/newest")),
|
| 23 |
+
("hn-login-page", "https://news.ycombinator.com/", "Go to the login page.",
|
| 24 |
+
lambda u, t, x: "login" in u),
|
| 25 |
+
("gh-issues", "https://github.com/tile-ai/tilelang", "Open the Issues tab of this repository.",
|
| 26 |
+
lambda u, t, x: "/issues" in u),
|
| 27 |
+
("py-downloads", "https://www.python.org/", "Go to the Downloads page.",
|
| 28 |
+
lambda u, t, x: "/downloads" in u),
|
| 29 |
+
("books-travel", "https://books.toscrape.com/", "Open the 'Travel' category.",
|
| 30 |
+
lambda u, t, x: "travel" in u),
|
| 31 |
+
("books-open-book", "https://books.toscrape.com/", "Open the product page of the book 'A Light in the Attic'.",
|
| 32 |
+
lambda u, t, x: "a-light-in-the-attic" in u),
|
| 33 |
+
# NOTE: jev's snapshot.js hides password fields by design, so password logins are impossible in this framework.
|
| 34 |
+
("internet-dropdown", "https://the-internet.herokuapp.com/dropdown", "Select 'Option 2' in the dropdown.",
|
| 35 |
+
lambda u, t, x: False), # checked via page state below
|
| 36 |
+
("internet-checkbox", "https://the-internet.herokuapp.com/checkboxes", "Tick the first checkbox.",
|
| 37 |
+
lambda u, t, x: False),
|
| 38 |
+
("books-page2", "https://books.toscrape.com/", "Go to page 2 of the catalogue.",
|
| 39 |
+
lambda u, t, x: "page-2" in u),
|
| 40 |
+
("quotes-tag-love", "https://quotes.toscrape.com/", "Show the quotes tagged 'love'.",
|
| 41 |
+
lambda u, t, x: "/tag/love" in u),
|
| 42 |
+
("hn-past", "https://news.ycombinator.com/", "Open the 'past' page (front pages from previous days).",
|
| 43 |
+
lambda u, t, x: "/front" in u),
|
| 44 |
+
("ddg-search", "https://duckduckgo.com/", "Search for 'tilelang github' and show the results.",
|
| 45 |
+
lambda u, t, x: "q=" in u and "tilelang" in u.lower()),
|
| 46 |
+
("arxiv-search", "https://arxiv.org/", "Search arXiv for 'flash attention' papers and show the results list.",
|
| 47 |
+
lambda u, t, x: "search" in u and "flash" in u.lower()),
|
| 48 |
+
("flights", "https://www.google.com/travel/flights?hl=en", "Find one-way flights from Zurich to London on September 28, 2026, for one adult in economy. Stop when matching flight options are visible.",
|
| 49 |
+
lambda u, t, x: ("ZRH" in x or "Zurich" in x or "Zürich" in x) and "London" in x and re.search(r"\b\d{1,2}:\d{2}\b", x) is not None and "one way" in x.lower()),
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
def run(name, url, goal, check, max_steps=20):
|
| 53 |
+
t0 = time.time(); steps = 0; status = "error"; page = None
|
| 54 |
+
try:
|
| 55 |
+
with Agent(url, goal) as agent:
|
| 56 |
+
page = agent.state["page"]
|
| 57 |
+
for state in agent.run():
|
| 58 |
+
steps = len(state["history"]); status = state["status"]; page = state["page"]
|
| 59 |
+
if steps >= max_steps: break
|
| 60 |
+
except Exception as e:
|
| 61 |
+
status = f"error:{type(e).__name__}"
|
| 62 |
+
wall = time.time() - t0
|
| 63 |
+
ok = bool(page and check(page["url"], page["title"], page.get("text", "")))
|
| 64 |
+
if page and name == "internet-dropdown":
|
| 65 |
+
ok = any(a.get("kind") == "select" and a.get("current_value") == "2" for a in page["actions"]) or \
|
| 66 |
+
any(a.get("kind") == "select" and a.get("value") == "2" and a.get("selected") for a in page["actions"])
|
| 67 |
+
if page and name == "internet-checkbox":
|
| 68 |
+
cbs = [a for a in page["actions"] if a.get("role") == "checkbox"]
|
| 69 |
+
ok = bool(cbs) and bool(cbs[0].get("checked"))
|
| 70 |
+
return ok, steps, status, wall, (page or {}).get("url", "")
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
flt = sys.argv[1] if len(sys.argv) > 1 else ""
|
| 74 |
+
repeats = int(os.environ.get("REPEATS", "1"))
|
| 75 |
+
rows = []
|
| 76 |
+
for name, url, goal, check in TASKS:
|
| 77 |
+
if flt and flt not in name: continue
|
| 78 |
+
for rep in range(repeats):
|
| 79 |
+
ok, steps, status, wall, final = run(name, url, goal, check)
|
| 80 |
+
rows.append((name, ok, steps, status, wall))
|
| 81 |
+
print(f"{'PASS' if ok else 'FAIL'} {name:16s} steps={steps:2d} status={status:9s} {wall:5.1f}s {final[:70]}", flush=True)
|
| 82 |
+
n = sum(r[1] for r in rows)
|
| 83 |
+
print(f"\n== {n}/{len(rows)} passed ({100*n/len(rows):.0f}%, {repeats} run(s) per task) | median wall {sorted(r[4] for r in rows)[len(rows)//2]:.1f}s")
|
| 84 |
+
if repeats > 1:
|
| 85 |
+
per = {}
|
| 86 |
+
for r in rows: per.setdefault(r[0], []).append(r[1])
|
| 87 |
+
print(" per task: " + " ".join(f"{k}={sum(v)}/{len(v)}" for k, v in per.items()))
|
| 88 |
+
json.dump([dict(zip(("name", "pass", "steps", "status", "wall"), r)) for r in rows], open(os.environ.get("SUITE_OUT", "/tmp/suite.json"), "w"), indent=1)
|
code/apps/browser_task.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys, json, time
|
| 2 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 3 |
+
os.environ.update(BU_CDP_URL="http://127.0.0.1:9222", TYPESAFE_BASE_URL="http://127.0.0.1:8791", TYPESAFE_API_KEY="local",
|
| 4 |
+
TEXT_MODEL_API_KEY="local", TEXT_MODEL_BASE_URL="http://127.0.0.1:30000/v1", TEXT_MODEL="Qwen/Qwen3-4B-AWQ",
|
| 5 |
+
TEXT_MODEL_EXTRA_JSON='{"chat_template_kwargs": {"enable_thinking": false}}')
|
| 6 |
+
from jev_ultrafast import Agent
|
| 7 |
+
url = sys.argv[1]; goal = sys.argv[2]
|
| 8 |
+
t = time.time()
|
| 9 |
+
with Agent(url, goal) as agent:
|
| 10 |
+
print("elements on first page:", len(agent.snapshot()["elements"]), "| title:", agent.state["page"]["title"])
|
| 11 |
+
for state in agent.run():
|
| 12 |
+
h = state["history"][-1] if state["history"] else None
|
| 13 |
+
d = state["decisions"][-1] if state["decisions"] else None
|
| 14 |
+
print(f"{state['elapsed_ms']:6d} ms status={state['status']:9s} op={d['operation'] if d else None:9s} "
|
| 15 |
+
f"conf={d['confidence'] if d else 0:.2f} model={d['latency_ms'] if d else 0:4d}ms "
|
| 16 |
+
f"action={h['action'][:60] if h else None} text={h['text'] if h else None}")
|
| 17 |
+
if len(state["history"]) > 25: break
|
| 18 |
+
print("FINAL:", state["status"], "| url:", state["page"]["url"], "| title:", state["page"]["title"], "| wall", round(time.time() - t, 1), "s")
|
code/apps/common.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared helpers: model loading + pretty printing."""
|
| 2 |
+
import os, sys, time
|
| 3 |
+
os.environ.setdefault("USE_TF", "0")
|
| 4 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 5 |
+
|
| 6 |
+
_AGENTS = {}
|
| 7 |
+
|
| 8 |
+
def get_agent(variant="english"):
|
| 9 |
+
"""variant: english | multilingual | typed"""
|
| 10 |
+
if variant in _AGENTS:
|
| 11 |
+
return _AGENTS[variant]
|
| 12 |
+
import laya
|
| 13 |
+
t = time.time()
|
| 14 |
+
if os.path.isdir(variant):
|
| 15 |
+
a = laya.load(variant)
|
| 16 |
+
elif variant == "english":
|
| 17 |
+
# download only the root checkpoint (the other subfolders are several GB each)
|
| 18 |
+
from huggingface_hub import snapshot_download
|
| 19 |
+
path = snapshot_download("convaiinnovations/laya", ignore_patterns=["multilingual/*", "typed-decisions/*"])
|
| 20 |
+
a = laya.load(path)
|
| 21 |
+
elif variant == "multilingual":
|
| 22 |
+
a = laya.load("convaiinnovations/laya", subfolder="multilingual")
|
| 23 |
+
else:
|
| 24 |
+
from huggingface_hub import snapshot_download
|
| 25 |
+
path = snapshot_download("convaiinnovations/laya", allow_patterns=["typed-decisions/*"])
|
| 26 |
+
a = laya.load(path, subfolder="typed-decisions")
|
| 27 |
+
print(f"[laya] loaded {variant} in {time.time()-t:.1f}s", file=sys.stderr)
|
| 28 |
+
if os.environ.get("LAYA_FAST", "1") == "1":
|
| 29 |
+
try:
|
| 30 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "kernels"))
|
| 31 |
+
try:
|
| 32 |
+
from fast_laya import accelerate # laya/kernels layout
|
| 33 |
+
accelerate(a)
|
| 34 |
+
except ImportError:
|
| 35 |
+
from fast import FastLaya # code/kernels layout (this repo)
|
| 36 |
+
fl = FastLaya(a.model, max_len=a.cfg.get("max_len", 1024)); a._fast = fl; a.model.forward = fl.forward
|
| 37 |
+
print("[laya] TileLang fast path enabled (LAYA_FAST=0 to disable)", file=sys.stderr)
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"[laya] fast path unavailable: {e}", file=sys.stderr)
|
| 40 |
+
_AGENTS[variant] = a
|
| 41 |
+
return a
|
| 42 |
+
|
| 43 |
+
def bar(p, width=20):
|
| 44 |
+
n = int(round(p * width))
|
| 45 |
+
return "█" * n + "░" * (width - n)
|
| 46 |
+
|
| 47 |
+
def show(result, indent=" "):
|
| 48 |
+
"""Pretty-print a laya predict() result."""
|
| 49 |
+
for name, a in result["answers"].items():
|
| 50 |
+
t = a["type"]
|
| 51 |
+
if t == "choice":
|
| 52 |
+
print(f"{indent}{name}: {a['choice']} (conf {a['confidence']:.2f})")
|
| 53 |
+
for k, v in sorted(a["probabilities"].items(), key=lambda kv: -kv[1]):
|
| 54 |
+
print(f"{indent} {bar(v)} {v:5.2f} {k}")
|
| 55 |
+
elif t == "score":
|
| 56 |
+
print(f"{indent}{name}: score={a['score']:.2f} (conf {a['confidence']:.2f})")
|
| 57 |
+
for k, v in a["probabilities"].items():
|
| 58 |
+
print(f"{indent} {bar(v)} {v:5.2f} {a['legend'][k]}")
|
| 59 |
+
else:
|
| 60 |
+
p = a["noul"]
|
| 61 |
+
print(f"{indent}{name}: P(true)={p:.2f} {bar(p)}")
|
code/apps/fast_batch.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One-shot batched predict for laya: tokenize the shared state once, build every question's sequence from cached ids,
|
| 2 |
+
single forward, vectorised post-processing. Same outputs as agent.predict() (up to float rounding).
|
| 3 |
+
|
| 4 |
+
from fast_batch import predict_fast, profile_step
|
| 5 |
+
"""
|
| 6 |
+
import json, time
|
| 7 |
+
import numpy as np, torch
|
| 8 |
+
from laya.common import QTYPES, collate_items, confidence_from_probs, render_options, serialize_state, temp_bucket
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def build_items(agent, state, questions):
|
| 12 |
+
tok = agent.tok
|
| 13 |
+
max_len, head_max_len = agent.cfg.get("max_len", 512), agent.cfg.get("head_max_len", 192)
|
| 14 |
+
mask_tok, mask_id = tok.mask_token, tok.mask_token_id
|
| 15 |
+
st_ids = None # shared state tokens, computed lazily once
|
| 16 |
+
items, meta = [], []
|
| 17 |
+
for qid, qdef in questions.items():
|
| 18 |
+
q = agent._to_internal(qdef)
|
| 19 |
+
opts = render_options(q)
|
| 20 |
+
ins = str(q["ins"]).replace(mask_tok, " ")
|
| 21 |
+
head_ids = tok("%s question: %s" % (q["t"], ins), add_special_tokens=False)["input_ids"]
|
| 22 |
+
opt_txt = [" " + o.replace(mask_tok, " ") for o in opts]
|
| 23 |
+
opt_enc = tok(opt_txt, add_special_tokens=False)["input_ids"] # one batched tokenizer call for all options
|
| 24 |
+
opt_ids = [[mask_id] + o[:48] for o in opt_enc]
|
| 25 |
+
opt_budget = head_max_len - sum(len(o) for o in opt_ids)
|
| 26 |
+
if opt_budget < 16:
|
| 27 |
+
per = max(4, (head_max_len - 16) // max(1, len(opt_ids)))
|
| 28 |
+
opt_ids = [o[:per] for o in opt_ids]
|
| 29 |
+
opt_budget = head_max_len - sum(len(o) for o in opt_ids)
|
| 30 |
+
head_ids = head_ids[: max(8, opt_budget)]
|
| 31 |
+
ids = [tok.cls_token_id] + head_ids + [tok.sep_token_id]
|
| 32 |
+
markers = []
|
| 33 |
+
for o in opt_ids:
|
| 34 |
+
markers.append(len(ids)); ids.extend(o)
|
| 35 |
+
ids.append(tok.sep_token_id)
|
| 36 |
+
room = max(0, max_len - len(ids) - 1)
|
| 37 |
+
if st_ids is None:
|
| 38 |
+
st_ids = tok(serialize_state(state).replace(mask_tok, " "), add_special_tokens=False)["input_ids"]
|
| 39 |
+
ids = (ids + st_ids[:room] + [tok.sep_token_id])[:max_len]
|
| 40 |
+
markers = [m for m in markers if m < max_len]
|
| 41 |
+
if len(markers) != len(opts):
|
| 42 |
+
raise ValueError("question %r options exceed head_max_len=%d" % (qid, head_max_len))
|
| 43 |
+
items.append({"ids": ids, "markers": markers, "qtype": QTYPES[q["t"]]})
|
| 44 |
+
meta.append((qid, q, len(markers)))
|
| 45 |
+
return items, meta
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@torch.no_grad()
|
| 49 |
+
def predict_fast(agent, state, questions, timing=None):
|
| 50 |
+
t0 = time.perf_counter()
|
| 51 |
+
items, meta = build_items(agent, state, questions)
|
| 52 |
+
b = collate_items([items], agent.tok.pad_token_id)
|
| 53 |
+
t1 = time.perf_counter()
|
| 54 |
+
dev = agent.device
|
| 55 |
+
with torch.autocast(device_type=dev.type, dtype=agent.dtype, enabled=dev.type == "cuda"):
|
| 56 |
+
logits, act = agent.model(b["input_ids"].to(dev, non_blocking=True), b["attention_mask"].to(dev, non_blocking=True),
|
| 57 |
+
b["marker_pos"].to(dev, non_blocking=True), b["marker_mask"].to(dev, non_blocking=True), b["qtype"].to(dev, non_blocking=True))
|
| 58 |
+
logits = logits.float().cpu().numpy(); act = torch.softmax(act.float(), -1).cpu().numpy()
|
| 59 |
+
t2 = time.perf_counter()
|
| 60 |
+
answers = {}
|
| 61 |
+
for r, (qid, q, k) in enumerate(meta):
|
| 62 |
+
qt = QTYPES[q["t"]]
|
| 63 |
+
t_scale = agent.temperature_by_options.get(temp_bucket(qt, k), agent.temperature[qt])
|
| 64 |
+
z = logits[r, :k] / max(1e-3, float(t_scale)); p = np.exp(z - z.max()); p /= p.sum()
|
| 65 |
+
conf = round(confidence_from_probs(p, k), 4); ext = {"act_probability": round(float(act[r, 0]), 4)}
|
| 66 |
+
if q["t"] == "choice":
|
| 67 |
+
keys = list(q["crit"].keys())
|
| 68 |
+
answers[qid] = {"type": "choice", "choice": keys[int(p.argmax())], "probabilities": {kk: round(float(v), 4) for kk, v in zip(keys, p)}, "confidence": conf, "action": ext}
|
| 69 |
+
elif q["t"] == "score":
|
| 70 |
+
answers[qid] = {"type": "score", "score": round(float((np.arange(k) * p).sum()), 4), "legend": {str(i): c for i, c in enumerate(q["crit"])},
|
| 71 |
+
"probabilities": {str(i): round(float(v), 4) for i, v in enumerate(p)}, "confidence": conf, "action": ext}
|
| 72 |
+
else:
|
| 73 |
+
answers[qid] = {"type": "noul", "noul": round(float(p[1]), 4), "confidence": round(max(float(p[1]), 1 - float(p[1])), 4), "action": ext}
|
| 74 |
+
t3 = time.perf_counter()
|
| 75 |
+
if timing is not None:
|
| 76 |
+
timing.update(tokenize_ms=(t1 - t0) * 1e3, forward_ms=(t2 - t1) * 1e3, post_ms=(t3 - t2) * 1e3, tokens=int(b["attention_mask"].sum()))
|
| 77 |
+
return {"model": "laya-rl-agent", "answers": answers, "usage": {"input_tokens": int(b["attention_mask"].sum()), "output_tokens": 0}}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def profile_step(agent, state, questions, n=20):
|
| 81 |
+
"""Compare agent.predict vs predict_fast on one recorded browser step."""
|
| 82 |
+
for _ in range(3): agent.predict(state, questions); predict_fast(agent, state, questions)
|
| 83 |
+
torch.cuda.synchronize(); t = time.perf_counter()
|
| 84 |
+
for _ in range(n): agent.predict(state, questions)
|
| 85 |
+
torch.cuda.synchronize(); slow = (time.perf_counter() - t) / n * 1e3
|
| 86 |
+
tm = {}; torch.cuda.synchronize(); t = time.perf_counter()
|
| 87 |
+
for _ in range(n): predict_fast(agent, state, questions, tm)
|
| 88 |
+
torch.cuda.synchronize(); fast = (time.perf_counter() - t) / n * 1e3
|
| 89 |
+
return {"predict_ms": slow, "predict_fast_ms": fast, **tm}
|
code/apps/hn_radar.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hacker News 雷达:拉取实时热帖标题,用 laya 给每条打 主题/是否硬核技术/是否值得读 标签。
|
| 2 |
+
用法: python apps/hn_radar.py [N=30]
|
| 3 |
+
"""
|
| 4 |
+
import os, sys, json, time, urllib.request
|
| 5 |
+
from common import get_agent, bar
|
| 6 |
+
|
| 7 |
+
QUESTIONS = {
|
| 8 |
+
"topic": {"type": "choice", "instructions": "What is this Hacker News story about?",
|
| 9 |
+
"criteria": {"ai": "machine learning, LLMs, models", "systems": "OS, compilers, databases, hardware, GPUs",
|
| 10 |
+
"web": "frontend, browsers, web frameworks", "security": "vulnerabilities, hacking, privacy",
|
| 11 |
+
"business": "startups, funding, layoffs, policy", "science": "physics, biology, space, math",
|
| 12 |
+
"other": "anything else"}},
|
| 13 |
+
"technical_depth": {"type": "score", "instructions": "How technically deep is this likely to be?",
|
| 14 |
+
"criteria": ["fluff", "medium", "deep dive"]},
|
| 15 |
+
"showhn": {"type": "noul", "instructions": "Is this a project someone built and is showing off?"},
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
def fetch(n):
|
| 19 |
+
"""One request to the HN Algolia API (front page)."""
|
| 20 |
+
r = json.load(urllib.request.urlopen(f"https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage={n}", timeout=20))
|
| 21 |
+
return [{"title": h["title"], "url": h.get("url") or "", "score": h.get("points", 0)} for h in r["hits"] if h.get("title")]
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
n = int(sys.argv[1]) if len(sys.argv) > 1 else 30
|
| 25 |
+
print(f"fetching {n} HN top stories...")
|
| 26 |
+
items = fetch(n)
|
| 27 |
+
agent = get_agent(os.environ.get("LAYA_VARIANT", "multilingual"))
|
| 28 |
+
t = time.time()
|
| 29 |
+
res = [agent.predict({"title": it["title"], "url": it.get("url", "")}, QUESTIONS) for it in items]
|
| 30 |
+
dt = time.time() - t
|
| 31 |
+
print(f"classified {len(items)} in {dt*1000:.0f} ms\n")
|
| 32 |
+
by_topic = {}
|
| 33 |
+
for it, r in zip(items, res):
|
| 34 |
+
a = r["answers"]
|
| 35 |
+
by_topic.setdefault(a["topic"]["choice"], []).append((round(a["technical_depth"]["score"],1), a["showhn"]["noul"], it))
|
| 36 |
+
for topic, lst in sorted(by_topic.items(), key=lambda kv: -len(kv[1])):
|
| 37 |
+
print(f"## {topic} ({len(lst)})")
|
| 38 |
+
for depth, show_p, it in sorted(lst, key=lambda x: -(x[0] or 0)):
|
| 39 |
+
tag = " [show]" if show_p > 0.5 else ""
|
| 40 |
+
print(f" depth={depth} ↑{it.get('score',0):<4} {it['title'][:70]}{tag}")
|
| 41 |
+
print()
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
main()
|
code/apps/inbox_triage.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""多语言工单/邮件分诊:一次前向传播同时回答 部门 / 紧急度 / 流失风险 / 情绪。
|
| 2 |
+
用法: python apps/inbox_triage.py # 跑内置样例(中/英/日/西/印地语)
|
| 3 |
+
python apps/inbox_triage.py "你的邮件正文"
|
| 4 |
+
"""
|
| 5 |
+
import sys, json, time
|
| 6 |
+
from common import get_agent, show
|
| 7 |
+
|
| 8 |
+
QUESTIONS = {
|
| 9 |
+
"department": {"type": "choice", "instructions": "Which team should handle this message?",
|
| 10 |
+
"criteria": {"billing": "invoices, payments, refunds, double charge",
|
| 11 |
+
"technical": "bugs, crashes, outages, login failures",
|
| 12 |
+
"sales": "pricing, quotes, upgrades, enterprise plans",
|
| 13 |
+
"shipping": "delivery, tracking, lost or damaged package"}},
|
| 14 |
+
"urgency": {"type": "score", "instructions": "How urgent is this?",
|
| 15 |
+
"criteria": ["not urgent", "soon", "blocking"]},
|
| 16 |
+
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel or leave?"},
|
| 17 |
+
"sentiment": {"type": "choice", "instructions": "What is the customer's tone?",
|
| 18 |
+
"criteria": {"angry": "furious, threatening, insulting",
|
| 19 |
+
"frustrated": "annoyed but civil", "neutral": "matter of fact",
|
| 20 |
+
"happy": "grateful, positive"}},
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
SAMPLES = [
|
| 24 |
+
{"subject": "Duplicate charge on invoice 4411",
|
| 25 |
+
"body": "We were billed twice for March. Please refund the duplicate or we're moving to a competitor."},
|
| 26 |
+
{"subject": "登录一直失败", "body": "从昨天开始整个团队都登录不了后台,报 502,我们的上线被卡住了,急!"},
|
| 27 |
+
{"subject": "見積もりのお願い", "body": "エンタープライズプランの料金と年間契約の割引について教えてください。"},
|
| 28 |
+
{"subject": "Paquete perdido", "body": "El rastreo dice entregado pero no recibí nada. Llevo una semana esperando, estoy muy molesto."},
|
| 29 |
+
{"subject": "धन्यवाद", "body": "आपकी टीम ने मेरी समस्या बहुत जल्दी हल कर दी। बहुत बहुत धन्यवाद!"},
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
agent = get_agent("multilingual")
|
| 34 |
+
states = [{"subject": "(cli)", "body": " ".join(sys.argv[1:])}] if len(sys.argv) > 1 else SAMPLES
|
| 35 |
+
for s in states:
|
| 36 |
+
t = time.time()
|
| 37 |
+
r = agent.predict(s, QUESTIONS)
|
| 38 |
+
dt = (time.time() - t) * 1000
|
| 39 |
+
print(f"\n=== {s['subject']} [{dt:.0f} ms]\n {s['body'][:80]}")
|
| 40 |
+
show(r)
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
main()
|
code/apps/moderator.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""内容审核台:给一批评论打 有害/垃圾/需人工复核 的校准概率,并按风险排序。
|
| 2 |
+
用法: python apps/moderator.py [file.txt] # 每行一条评论;无参数用内置样例
|
| 3 |
+
"""
|
| 4 |
+
import sys, time
|
| 5 |
+
from common import get_agent, bar
|
| 6 |
+
|
| 7 |
+
QUESTIONS = {
|
| 8 |
+
"toxic": {"type": "noul", "instructions": "Is this comment abusive, hateful or harassing toward someone?"},
|
| 9 |
+
"spam": {"type": "noul", "instructions": "Is this comment spam or unsolicited advertising?"},
|
| 10 |
+
"topic": {"type": "choice", "instructions": "What is the comment mainly about?",
|
| 11 |
+
"criteria": {"product": "the product or service itself", "politics": "political opinion",
|
| 12 |
+
"personal": "attacks or remarks about a person", "offtopic": "unrelated chatter"}},
|
| 13 |
+
"severity": {"type": "score", "instructions": "How severe is the policy violation, if any?",
|
| 14 |
+
"criteria": ["none", "mild", "serious", "ban-worthy"]},
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
SAMPLES = [
|
| 18 |
+
"This update is great, the new editor is so much faster!",
|
| 19 |
+
"Buy cheap followers now!!! visit my profile link, 50% off today only",
|
| 20 |
+
"You are a worthless idiot and everyone here knows it.",
|
| 21 |
+
"这个功能真的太难用了,建议回滚到上个版本。",
|
| 22 |
+
"滚出去,你这种垃圾不配在这发言。",
|
| 23 |
+
"Honestly both parties are the same, nothing will change.",
|
| 24 |
+
"Does anyone know if the API supports webhooks?",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
def main():
|
| 28 |
+
agent = get_agent("multilingual")
|
| 29 |
+
texts = [l.strip() for l in open(sys.argv[1], encoding="utf-8") if l.strip()] if len(sys.argv) > 1 else SAMPLES
|
| 30 |
+
t = time.time()
|
| 31 |
+
results = [agent.predict({"comment": c}, QUESTIONS) for c in texts]
|
| 32 |
+
dt = time.time() - t
|
| 33 |
+
rows = []
|
| 34 |
+
for c, r in zip(texts, results):
|
| 35 |
+
a = r["answers"]
|
| 36 |
+
risk = max(a["toxic"]["noul"], a["spam"]["noul"])
|
| 37 |
+
rows.append((risk, a["toxic"]["noul"], a["spam"]["noul"],
|
| 38 |
+
a["topic"]["choice"], round(a["severity"]["score"],1), c))
|
| 39 |
+
rows.sort(reverse=True)
|
| 40 |
+
print(f"{len(texts)} comments in {dt*1000:.0f} ms ({dt*1000/len(texts):.0f} ms each)\n")
|
| 41 |
+
print(f"{'risk':>5} {'toxic':>5} {'spam':>5} {'topic':9} {'sev':>4} comment")
|
| 42 |
+
for risk, tox, spam, topic, sev, c in rows:
|
| 43 |
+
flag = "🚨" if risk > 0.7 else ("⚠️ " if risk > 0.4 else " ")
|
| 44 |
+
print(f"{flag}{risk:5.2f} {tox:5.2f} {spam:5.2f} {topic:9} {str(sev):>4} {c[:60]}")
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
code/apps/profile_step.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Time one real browser step (recorded in finetune/out/dagger_cases.jsonl) end to end inside the server process.
|
| 2 |
+
python apps/profile_step.py <checkpoint dir>"""
|
| 3 |
+
import json, os, sys
|
| 4 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); sys.path.insert(0, "/home/ckl/projects/S/laya/finetune")
|
| 5 |
+
os.environ.setdefault("LAYA_FMT", "v2")
|
| 6 |
+
from common import get_agent
|
| 7 |
+
from common_ft import build_request
|
| 8 |
+
from fast_batch import profile_step
|
| 9 |
+
agent = get_agent(sys.argv[1])
|
| 10 |
+
if agent.cfg.get("head_max_len_train"): agent.cfg["head_max_len"] = agent.cfg["head_max_len_train"]
|
| 11 |
+
cases = [json.loads(l) for l in open("/home/ckl/projects/S/laya/finetune/out/dagger_cases.jsonl")]
|
| 12 |
+
for c in cases[:4]:
|
| 13 |
+
page = c["page_obj"]; state, questions, _, _ = build_request(page, c["goal"], c.get("history", []))
|
| 14 |
+
n_opts = sum(len(q["criteria"]) for q in questions.values())
|
| 15 |
+
r = profile_step(agent, state, questions)
|
| 16 |
+
print(f"{len(questions)} questions / {n_opts:3d} options / {r['tokens']:4d} tokens: agent.predict {r['predict_ms']:6.1f} ms | fast {r['predict_fast_ms']:6.1f} ms "
|
| 17 |
+
f"(tokenize {r['tokenize_ms']:.1f} + forward {r['forward_ms']:.1f} + post {r['post_ms']:.1f})")
|
code/apps/systemone_server.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TypeSafe-compatible /v1/systemone endpoint backed by a local laya checkpoint (+ TileLang fast path).
|
| 2 |
+
|
| 3 |
+
python apps/systemone_server.py [port=8791] [variant=typed|multilingual|english]
|
| 4 |
+
|
| 5 |
+
Request body: {"model": ..., "state": {...}, "questions": {...}} -> {"answers": ..., "model": ..., "usage": ...}
|
| 6 |
+
"""
|
| 7 |
+
import json, os, sys, time, traceback
|
| 8 |
+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
| 9 |
+
from common import get_agent
|
| 10 |
+
from fast_batch import predict_fast
|
| 11 |
+
|
| 12 |
+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8791
|
| 13 |
+
VARIANT = sys.argv[2] if len(sys.argv) > 2 else "typed"
|
| 14 |
+
MAXOPT = int(sys.argv[3]) if len(sys.argv) > 3 else 12 # laya's option budget: keep choice questions at most this wide
|
| 15 |
+
agent = get_agent(VARIANT)
|
| 16 |
+
FMT = os.environ.get("LAYA_FMT", agent.cfg.get("laya_fmt", "v1")) # fine-tuned checkpoints record their format in rl_agent_config.json
|
| 17 |
+
if agent.cfg.get("head_max_len_train"):
|
| 18 |
+
agent.cfg["head_max_len"] = agent.cfg["head_max_len_train"]
|
| 19 |
+
print(f"[systemone] format={FMT} head_max_len={agent.cfg.get('head_max_len')}", flush=True)
|
| 20 |
+
LOG = []
|
| 21 |
+
|
| 22 |
+
# ---- System 1 / System 2 gating: below ESCALATE_TAU confidence, ask the LLM teacher (same element table) and return its
|
| 23 |
+
# decision in laya's answer format. Every escalation is also appended to ESCALATE_LOG as a DAgger case.
|
| 24 |
+
TAU = float(os.environ.get("ESCALATE_TAU", "0")) # 0 = off
|
| 25 |
+
ESC_URL = os.environ.get("TEXT_MODEL_BASE_URL", "http://127.0.0.1:30000/v1") + "/chat/completions"
|
| 26 |
+
ESC_MODEL = os.environ.get("TEXT_MODEL", "Qwen/Qwen3-8B-AWQ")
|
| 27 |
+
ESC_LOG = os.environ.get("ESCALATE_LOG", "")
|
| 28 |
+
STATS = {"calls": 0, "escalated": 0}
|
| 29 |
+
ESC_SYS = """You are the System-2 fallback for a browser agent. Given the goal, the actions so far, the page and a numbered list of
|
| 30 |
+
controls (each with the operations it supports), pick the single best NEXT step. Answer JSON:
|
| 31 |
+
{"operation": "CLICK"|"TYPE_TEXT"|"SELECT"|"DONE"|"BLOCKED"|"WAIT"|"SCROLL_DOWN"|"SCROLL_UP", "target": "<option key of the chosen control, or null>"}
|
| 32 |
+
DONE only if the goal is already visibly satisfied. A field that already shows the requested value is done; do not re-type it."""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def escalate(state, questions, answers):
|
| 36 |
+
import httpx
|
| 37 |
+
ops = questions["operation"]["criteria"]
|
| 38 |
+
controls = []
|
| 39 |
+
for qid, q in questions.items():
|
| 40 |
+
if qid.endswith("_target"):
|
| 41 |
+
op = qid[:-7].upper()
|
| 42 |
+
for key, desc in q["criteria"].items():
|
| 43 |
+
controls.append({"op": op, "target": key, "control": desc})
|
| 44 |
+
user = {"goal": (questions["operation"]["instructions"] or {}).get("goal") if isinstance(questions["operation"]["instructions"], dict) else "",
|
| 45 |
+
"actions_so_far": state.get("recent_actions", [])[-8:], "page": state.get("page", {}), "operations": list(ops),
|
| 46 |
+
"controls": controls[:150], "system1_guess": {k: v.get("choice") for k, v in answers.items()}}
|
| 47 |
+
body = {"model": ESC_MODEL, "max_tokens": 80, "temperature": 0.0, "response_format": {"type": "json_object"},
|
| 48 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 49 |
+
"messages": [{"role": "system", "content": ESC_SYS}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}]}
|
| 50 |
+
r = httpx.post(ESC_URL, json=body, timeout=120).json()
|
| 51 |
+
v = json.loads(r["choices"][0]["message"]["content"])
|
| 52 |
+
op = str(v.get("operation", "")).upper(); tgt = v.get("target")
|
| 53 |
+
if op not in ops:
|
| 54 |
+
return answers, False
|
| 55 |
+
def one_hot(keys, k, p=0.97):
|
| 56 |
+
rest = (1 - p) / max(1, len(keys) - 1)
|
| 57 |
+
return {kk: (p if kk == k else rest) for kk in keys}
|
| 58 |
+
answers["operation"] = {**answers["operation"], "choice": op, "probabilities": one_hot(list(ops), op), "confidence": 0.9, "system2": True}
|
| 59 |
+
tq = op.lower() + "_target"
|
| 60 |
+
if tq in questions:
|
| 61 |
+
keys = list(questions[tq]["criteria"]); tgt = str(tgt)
|
| 62 |
+
if tgt not in keys:
|
| 63 |
+
return answers, False
|
| 64 |
+
answers[tq] = {**answers[tq], "choice": tgt, "probabilities": one_hot(keys, tgt), "confidence": 0.9, "system2": True}
|
| 65 |
+
if ESC_LOG:
|
| 66 |
+
with open(ESC_LOG, "a") as f:
|
| 67 |
+
f.write(json.dumps({"state": state, "operation": op, "target": tgt if tq in questions else None}, ensure_ascii=False) + "\n")
|
| 68 |
+
return answers, True
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def compact(v):
|
| 72 |
+
"""Shrink jev-ultrafast element criteria ({'element': '[3] Search', 'role': 'button', ...}) into one short string
|
| 73 |
+
so more options fit laya's head token budget."""
|
| 74 |
+
if isinstance(v, dict) and "element" in v:
|
| 75 |
+
s = str(v["element"])[:50 if FMT == "v3" else 10000]
|
| 76 |
+
if v.get("role"):
|
| 77 |
+
s += f" ({v['role']})"
|
| 78 |
+
if v.get("current_value"):
|
| 79 |
+
s += f" = {str(v['current_value'])[:30]!r}"
|
| 80 |
+
for k in ("checked", "selected", "expanded"):
|
| 81 |
+
if k in v:
|
| 82 |
+
s += f" {k}={v[k]}"
|
| 83 |
+
return s
|
| 84 |
+
return v
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def predict(state, questions):
|
| 88 |
+
"""agent.predict with coarse-to-fine handling of wide choice questions.
|
| 89 |
+
|
| 90 |
+
A choice with more than MAXOPT options is split into interleaved chunks; every chunk is a question in the same
|
| 91 |
+
forward pass as the normal questions, then the chunk winners compete in a second pass.
|
| 92 |
+
p(option) = p_final(winner of its chunk) * p_chunk(option)."""
|
| 93 |
+
qs, plan = {}, {}
|
| 94 |
+
if isinstance(state, dict) and isinstance(state.get("page"), dict) and isinstance(state["page"].get("text"), str):
|
| 95 |
+
if FMT in ("v2", "v3"): # mirror finetune/common_ft.py
|
| 96 |
+
state = {"page": {**state["page"], "text": state["page"]["text"][:1500 if FMT == "v2" else 1200]}, "recent_actions": state.get("recent_actions", [])}
|
| 97 |
+
else:
|
| 98 |
+
state = {**state, "page": {**state["page"], "text": state["page"]["text"][:6000]}}
|
| 99 |
+
for qid, q in questions.items():
|
| 100 |
+
q = dict(q)
|
| 101 |
+
if isinstance(q.get("criteria"), dict):
|
| 102 |
+
q["criteria"] = {k: compact(v) for k, v in q["criteria"].items()}
|
| 103 |
+
keys = list(q["criteria"]) if q["type"] == "choice" and isinstance(q.get("criteria"), dict) else []
|
| 104 |
+
if len(keys) <= MAXOPT:
|
| 105 |
+
qs[qid] = q
|
| 106 |
+
continue
|
| 107 |
+
n = -(-len(keys) // MAXOPT)
|
| 108 |
+
chunks = [keys[i::n] for i in range(n)]
|
| 109 |
+
plan[qid] = (q, chunks)
|
| 110 |
+
for ci, ch in enumerate(chunks):
|
| 111 |
+
qs[f"{qid}__chunk{ci}"] = {**q, "criteria": {k: q["criteria"][k] for k in ch}}
|
| 112 |
+
r = predict_fast(agent, state, qs)
|
| 113 |
+
r["passes"] = 1
|
| 114 |
+
if plan:
|
| 115 |
+
chunk_ans = {qid: [r["answers"].pop(f"{qid}__chunk{ci}") for ci in range(len(chunks))] for qid, (q, chunks) in plan.items()}
|
| 116 |
+
finals = {qid: {**q, "criteria": {a["choice"]: q["criteria"][a["choice"]] for a in chunk_ans[qid]}} for qid, (q, _) in plan.items()}
|
| 117 |
+
r2 = predict_fast(agent, state, finals)
|
| 118 |
+
r["passes"] = 2
|
| 119 |
+
r["usage"]["input_tokens"] += r2["usage"]["input_tokens"]
|
| 120 |
+
for qid, (q, chunks) in plan.items():
|
| 121 |
+
fa = r2["answers"][qid]
|
| 122 |
+
probs = {}
|
| 123 |
+
for ca, ch in zip(chunk_ans[qid], chunks):
|
| 124 |
+
pf = fa["probabilities"][ca["choice"]]
|
| 125 |
+
for k in ch:
|
| 126 |
+
probs[k] = pf * ca["probabilities"][k]
|
| 127 |
+
tot = sum(probs.values()) or 1.0
|
| 128 |
+
probs = {k: round(v / tot, 6) for k, v in probs.items()}
|
| 129 |
+
choice = max(probs, key=probs.get)
|
| 130 |
+
r["answers"][qid] = {"type": "choice", "choice": choice, "probabilities": probs, "confidence": fa["confidence"],
|
| 131 |
+
"action": fa.get("action", {}), "coarse_to_fine": {"chunks": len(chunks), "winners": [a["choice"] for a in chunk_ans[qid]]}}
|
| 132 |
+
return r
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class H(BaseHTTPRequestHandler):
|
| 136 |
+
def log_message(self, *a): pass
|
| 137 |
+
def _send(self, code, body):
|
| 138 |
+
data = json.dumps(body, ensure_ascii=False).encode()
|
| 139 |
+
self.send_response(code); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data)
|
| 140 |
+
def do_GET(self):
|
| 141 |
+
self._send(200, {"ok": True, "variant": VARIANT, "calls": len(LOG), "tau": TAU, "escalated": STATS["escalated"], "recent": LOG[-5:]})
|
| 142 |
+
def do_POST(self):
|
| 143 |
+
n = int(self.headers.get("Content-Length", 0)); req = json.loads(self.rfile.read(n) or b"{}")
|
| 144 |
+
try:
|
| 145 |
+
t = time.perf_counter()
|
| 146 |
+
r = predict(req["state"], req["questions"])
|
| 147 |
+
STATS["calls"] += 1
|
| 148 |
+
if TAU > 0:
|
| 149 |
+
a = r["answers"]; op = a["operation"]["choice"]; tq = op.lower() + "_target"
|
| 150 |
+
conf = min(a["operation"]["confidence"], a[tq]["confidence"] if tq in a else 1.0)
|
| 151 |
+
if conf < TAU:
|
| 152 |
+
try:
|
| 153 |
+
r["answers"], esc = escalate(req["state"], req["questions"], a)
|
| 154 |
+
STATS["escalated"] += esc
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print("[escalate] failed:", str(e)[:80], flush=True)
|
| 157 |
+
ms = (time.perf_counter() - t) * 1000
|
| 158 |
+
r["model"] = f"laya-{VARIANT}"
|
| 159 |
+
nq = len(req["questions"]); nopt = sum(len(q.get("criteria") or []) for q in req["questions"].values())
|
| 160 |
+
LOG.append({"ms": round(ms, 1), "questions": nq, "options": nopt, "tokens": r["usage"]["input_tokens"], "passes": r["passes"]})
|
| 161 |
+
print(f"[systemone] {nq} q / {nopt} opts / {r['usage']['input_tokens']} tok / {r['passes']} pass -> {ms:.1f} ms " +
|
| 162 |
+
", ".join(f"{k}={v.get('choice', v.get('score', v.get('noul')))}({v['confidence']:.2f})" for k, v in r["answers"].items()), flush=True)
|
| 163 |
+
self._send(200, r)
|
| 164 |
+
except Exception as e:
|
| 165 |
+
traceback.print_exc(); self._send(400, {"error": f"{type(e).__name__}: {e}"})
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
print(f"laya systemone server on http://127.0.0.1:{PORT}/v1/systemone ({VARIANT})", flush=True)
|
| 169 |
+
ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
|
code/apps/web_console.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""决策台 Web UI(零依赖,stdlib http.server):粘贴任意文本,自定义问题,实时看概率条。
|
| 2 |
+
用法: python apps/web_console.py [port=7860] 然后打开 http://127.0.0.1:7860
|
| 3 |
+
"""
|
| 4 |
+
import sys, json, time
|
| 5 |
+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
| 6 |
+
from common import get_agent
|
| 7 |
+
|
| 8 |
+
HTML = r"""<!doctype html><meta charset=utf-8><title>Laya 决策台</title>
|
| 9 |
+
<style>
|
| 10 |
+
body{font:14px system-ui;margin:0;background:#0f1115;color:#e6e6e6;display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:16px;height:100vh;box-sizing:border-box}
|
| 11 |
+
textarea{width:100%;box-sizing:border-box;background:#1a1d24;color:#eee;border:1px solid #333;border-radius:6px;padding:8px;font:13px ui-monospace,monospace}
|
| 12 |
+
button{background:#4f8cff;color:#fff;border:0;padding:8px 16px;border-radius:6px;font-size:14px;cursor:pointer}
|
| 13 |
+
select{background:#1a1d24;color:#eee;border:1px solid #333;padding:6px;border-radius:6px}
|
| 14 |
+
.q{margin:10px 0;padding:10px;background:#171a21;border-radius:8px}.q h3{margin:0 0 6px;font-size:14px}
|
| 15 |
+
.row{display:flex;align-items:center;gap:8px;margin:2px 0}.bar{height:10px;background:#4f8cff;border-radius:3px}
|
| 16 |
+
.lab{width:120px;text-align:right;color:#aaa}.p{width:40px;color:#aaa}
|
| 17 |
+
#out{overflow:auto}.meta{color:#888;font-size:12px}
|
| 18 |
+
</style>
|
| 19 |
+
<div><h2>Laya 决策台 <span class=meta>单次前向传播 · 不生成文本 · 校准概率</span></h2>
|
| 20 |
+
<label>模型 <select id=model><option value=multilingual>multilingual (100+ 语言)</option><option value=english>english</option><option value=typed>typed-decisions</option></select></label>
|
| 21 |
+
<p><b>状态 (JSON 或纯文本)</b><br><textarea id=state rows=8>{"subject": "登录一直失败", "body": "从昨天开始整个团队都登录不了后台,报 502,我们的上线被卡住了。再不解决就退订。"}</textarea></p>
|
| 22 |
+
<p><b>问题 (JSON)</b><br><textarea id=questions rows=16>{
|
| 23 |
+
"department": {"type": "choice", "instructions": "Which team should handle this?",
|
| 24 |
+
"criteria": {"billing": "invoices, refunds", "technical": "bugs, outages, login", "sales": "pricing, plans"}},
|
| 25 |
+
"urgency": {"type": "score", "instructions": "How urgent is this?", "criteria": ["not urgent", "soon", "blocking"]},
|
| 26 |
+
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel?"}
|
| 27 |
+
}</textarea></p>
|
| 28 |
+
<button onclick=run()>预测 (Ctrl+Enter)</button> <span id=t class=meta></span></div>
|
| 29 |
+
<div id=out></div>
|
| 30 |
+
<script>
|
| 31 |
+
async function run(){
|
| 32 |
+
const body={model:model.value,state:state.value,questions:questions.value};
|
| 33 |
+
t.textContent='...';
|
| 34 |
+
const r=await fetch('/predict',{method:'POST',body:JSON.stringify(body)});const j=await r.json();
|
| 35 |
+
if(j.error){out.innerHTML='<pre style="color:#f66">'+j.error+'</pre>';t.textContent='';return}
|
| 36 |
+
t.textContent=j.ms.toFixed(0)+' ms';
|
| 37 |
+
let h='';for(const [k,a] of Object.entries(j.answers)){
|
| 38 |
+
h+='<div class=q><h3>'+k+' → <span style=color:#8fd>'+(a.choice??(a.score!==undefined?'score '+a.score.toFixed(2):'')??'')+(a.noul!==undefined?'P(true)='+a.noul.toFixed(2):'')+'</span>'+(a.confidence!==undefined?' <span class=meta>conf '+a.confidence.toFixed(2)+'</span>':'')+'</h3>';
|
| 39 |
+
let d=a.probabilities||(a.noul!==undefined?{true:a.noul,false:1-a.noul}:{});if(a.legend)d=Object.fromEntries(Object.entries(d).map(([k,v])=>[k+' '+a.legend[k],v]));
|
| 40 |
+
if(Array.isArray(d))d=Object.fromEntries(d.map((v,i)=>[i,v]));
|
| 41 |
+
for(const [l,p] of Object.entries(d).sort((x,y)=>y[1]-x[1]))h+='<div class=row><span class=lab>'+l+'</span><div class=bar style=width:'+(p*300)+'px></div><span class=p>'+p.toFixed(2)+'</span></div>';
|
| 42 |
+
h+='</div>'}
|
| 43 |
+
h+='<pre class=meta>'+JSON.stringify(j.raw,null,1)+'</pre>';out.innerHTML=h}
|
| 44 |
+
document.addEventListener('keydown',e=>{if(e.ctrlKey&&e.key=='Enter')run()});
|
| 45 |
+
</script>"""
|
| 46 |
+
|
| 47 |
+
class H(BaseHTTPRequestHandler):
|
| 48 |
+
def log_message(self, *a): pass
|
| 49 |
+
def do_GET(self):
|
| 50 |
+
self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8"); self.end_headers()
|
| 51 |
+
self.wfile.write(HTML.encode())
|
| 52 |
+
def do_POST(self):
|
| 53 |
+
n = int(self.headers.get("Content-Length", 0)); req = json.loads(self.rfile.read(n))
|
| 54 |
+
try:
|
| 55 |
+
st = req["state"].strip()
|
| 56 |
+
try: st = json.loads(st)
|
| 57 |
+
except Exception: st = {"text": st}
|
| 58 |
+
qs = json.loads(req["questions"])
|
| 59 |
+
agent = get_agent(req.get("model", "multilingual"))
|
| 60 |
+
t = time.time(); r = agent.predict(st, qs); ms = (time.time() - t) * 1000
|
| 61 |
+
body = {"answers": r["answers"], "raw": r, "ms": ms}
|
| 62 |
+
except Exception as e:
|
| 63 |
+
body = {"error": f"{type(e).__name__}: {e}"}
|
| 64 |
+
data = json.dumps(body, ensure_ascii=False, default=str).encode()
|
| 65 |
+
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(data)
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 7860
|
| 69 |
+
get_agent("multilingual")
|
| 70 |
+
print(f"open http://127.0.0.1:{port}")
|
| 71 |
+
ThreadingHTTPServer(("127.0.0.1", port), H).serve_forever()
|
code/env.sh
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export HF_ENDPOINT=https://hf-mirror.com
|
| 2 |
+
export USE_TF=0
|
| 3 |
+
export UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
| 4 |
+
export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
| 5 |
+
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY
|
| 6 |
+
export PATH="$HOME/.local/bin:$PATH"
|
code/finetune/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 微调 laya 做浏览器 agent 决策头(替代 TypeSafe Jev)
|
| 2 |
+
|
| 3 |
+
流水线(全部本地、无付费 API):
|
| 4 |
+
|
| 5 |
+
| 步骤 | 脚本 | 说明 |
|
| 6 |
+
|---|---|---|
|
| 7 |
+
| 1 抓页面 | `collect_pages.py` | headless Chromium + browser-harness,90 个真实页面的元素表 + 正文 |
|
| 8 |
+
| 2 反向生成目标 | `gen_goals.py` | 随机选一个元素当 gold,本地 Qwen3-8B-AWQ(sglang)写出"要用它的用户目标",1121 条 |
|
| 9 |
+
| 3 真实 DONE 样本 | `make_done_cases.py` | 在浏览器里真的执行点击,落地页 + 历史 = DONE,250 条 |
|
| 10 |
+
| 4 构建样本 | `build_items.py` | 复刻 jev-ultrafast 的 systemone 请求格式(`common_ft.py`),head_max_len 512;页面按 index%5 留出 |
|
| 11 |
+
| 5 训练 | `train.py` | laya 官方 RLCD 配方(噪声 logit 策略梯度 + soft CE)单卡版,4 轮约 10 分钟 |
|
| 12 |
+
| 6 评测 | `eval.py` | 留出页面上的操作准确率 / 目标 top-1 |
|
| 13 |
+
|
| 14 |
+
```fish
|
| 15 |
+
bash finetune/run_v2.sh # 4-6 步
|
| 16 |
+
bash finetune/run_all.sh # 含零样本基线
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
## 加入魔搭 Mind2Web 后(v3/v4)
|
| 20 |
+
|
| 21 |
+
`osunlp/Mind2Web`(魔搭,6.7 GB,12-25 MB/s 不走代理)→ `convert_mind2web.py`:按 backend_node_id 从 cleaned_html 还原元素文本/角色,
|
| 22 |
+
正例 + 44 个负例打乱成 jev 格式元素表,历史用 action_reprs,7296 条;按网站哈希留出 20%。v4 = 自采 + 真实 DONE + 全部 Mind2Web,
|
| 23 |
+
DONE / TYPE_TEXT / SELECT 操作样本 3 倍过采样,16020 条训练样本,3 轮 64 分钟。`bash finetune/run_v4.sh`。
|
| 24 |
+
|
| 25 |
+
评测 1642 条(自采留出页 232 + Mind2Web 未见网站 1410):
|
| 26 |
+
|
| 27 |
+
| 模型 | 操作准确率 | 目标 top-1 | live DONE | m2w CLICK 目标 | m2w TYPE_TEXT 操作 | m2w SELECT 操作 |
|
| 28 |
+
|---|---|---|---|---|---|---|
|
| 29 |
+
| typed-decisions 零样本 | 0.26 | 0.13 | 0.39 | 0.03 | 0.00 | 0.00 |
|
| 30 |
+
| v3(无过采样) | 0.790 | 0.496 | 0.58 | 0.40 | 0.04 | 0.20 |
|
| 31 |
+
| **v4(过采样)** | **0.795** | **0.524** | **0.82** | **0.43** | **0.80** | **0.55** |
|
| 32 |
+
|
| 33 |
+
真实浏览器 6 任务:零样本 0/6 → v2 2/6 → **v4 3/6**(python.org Downloads 2.0 s、Travel 分类 0.3 s、Wikipedia 随机文章),
|
| 34 |
+
Wikipedia 搜索任务首次正确选 TYPE_TEXT 并由本地 Qwen 填入 "Python programming language",但之后重复填同一字段而没有提交——
|
| 35 |
+
训练数据里缺"字段已填好 → 下一步提交"的样本。GitHub Issues 点成 Releases、HN new 提前 DONE。
|
| 36 |
+
|
| 37 |
+
## 最终对比(2026-09-21,16 个真实任务 × 3 次,apps/browser_suite.py)
|
| 38 |
+
|
| 39 |
+
| 模型 | 底座 | 一步延迟 | 留出目标 top-1 | 真实任务通过率 | +门控 τ=0.7(Qwen3-8B 兜底) |
|
| 40 |
+
|---|---|---|---|---|---|
|
| 41 |
+
| v10 | ModernBERT-large 421M | 41–50 ms | 0.656 | **58%**(28/48) | 42%,升级率 78% |
|
| 42 |
+
| v10s | mmBERT-base 322M,格式 v3 | **17–23 ms** | 0.631 | 50%(24/48) | 52%,升级率 85% |
|
| 43 |
+
|
| 44 |
+
结果高度双峰:9 个任务 3/3 稳定通过(导航、分类、勾选、HN 各页、DuckDuckGo 搜索),7 个任务 0/3 稳定失败
|
| 45 |
+
(Wikipedia 两个搜索任务、GitHub Issues、下拉选择、翻页需滚动、arXiv 搜索、Google Flights)。
|
| 46 |
+
门控把低置信步骤交给 Qwen3-8B 反而更差:在这些页面上微调后的 laya 比 8B 通用模型判断得更准,System 2 需要更强的模型或专门提示。
|
| 47 |
+
失败任务的共性是"输入后提交 / 选建议"和"需要先滚动",训练数据里几乎没有滚动样本。
|
| 48 |
+
v10 数据:5244 条自采目标(421 页)+ 700 真实 DONE + step-2 + Mind2Web 全量 + DAgger,格式 v2,head 768,4 轮 2.2 小时。
|
| 49 |
+
|
| 50 |
+
## 输入格式 v2(v9 起)
|
| 51 |
+
|
| 52 |
+
`LAYA_FMT=v2 LAYA_HEAD=768`:元素表不再塞进 state(1024 token 里会被截掉),只在选项里保留完整标签 + 角色 + 已填值,
|
| 53 |
+
state 只留标题 / URL / 历史 / 1500 字正文,head_max_len 512→768。v9 只用 Mind2Web + 该格式,真实任务从 6/16 到 **10/16**,
|
| 54 |
+
Mind2Web 点击目标 0.44→0.51。checkpoint 的 `rl_agent_config.json` 记录 `laya_fmt` / `head_max_len_train`,服务端自动跟随。
|
| 55 |
+
|
| 56 |
+
注意:jev 的 `snapshot.js` 有意隐藏密码框(`!['password','file','hidden'].includes(e.type)`),密码登录任务在该框架里不可能完成,任务集已移除。
|
| 57 |
+
|
| 58 |
+
## 真实任务集(apps/browser_suite.py,16 个任务,自动验收)
|
| 59 |
+
|
| 60 |
+
| 版本 | 通过 | 主要失败模式 |
|
| 61 |
+
|---|---|---|
|
| 62 |
+
| v4 | 3/14 | 过早 DONE(6 个),填完不提交(2 个),点错(3 个) |
|
| 63 |
+
| v5(DONE 过采样 2x + 字段带已填值 + 温度 1.40) | 4/14 | 登录/搜索仍在填一个字段后 DONE |
|
| 64 |
+
|
| 65 |
+
v5 的根因:真实 DONE 样本历史恰好都是 1 步、自采点击样本历史都是 0 步,模型学到"有历史 ⇒ DONE"。
|
| 66 |
+
v6 加 `gen_step2.py`:在 250 个落地页上保留历史再生成 659 条新目标作为负样本。
|
| 67 |
+
|
| 68 |
+
## 结果(v1/v2,仅自采数据)
|
| 69 |
+
|
| 70 |
+
留出 18 个未见页面、232 条:
|
| 71 |
+
|
| 72 |
+
| 模型 | 操作准确率 | 目标 top-1 | DONE 判断 | 每条延迟 |
|
| 73 |
+
|---|---|---|---|---|
|
| 74 |
+
| typed-decisions 零样本 | 0.543 | 0.103(随机) | 0.39 | 254 ms |
|
| 75 |
+
| multilingual ��样本 | 0.127 | 0.124 | 0.00 | 249 ms |
|
| 76 |
+
| **微调 v2(1886 样本)** | **0.875** | **0.665** | 0.66 | 55 ms |
|
| 77 |
+
|
| 78 |
+
真实浏览器 6 个任务(jev-ultrafast 循环 + 本地 laya 服务):python.org Downloads 2.4 s 完成、books.toscrape Travel
|
| 79 |
+
分类 0.3 s 完成;HN "new"、GitHub Issues、Wikipedia 两个任务失败(点错元素或提前 DONE)。零样本时 6 个全失败。
|
| 80 |
+
|
| 81 |
+
已知问题:置信度全是 1.00(未做温度校准);数据只有 90 个页面,泛化有限。下一步是把页面扩到 500+、
|
| 82 |
+
每轮把线上失败样本加回训练集、拟合温度。v1 的教训:模板化的 DONE 样本会让模型学到"看到 stop when 就答 DONE",
|
| 83 |
+
DONE 样本必须来自真实执行后的落地页。
|
code/finetune/build_items.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""cases.jsonl + pages.jsonl -> tokenized training items (train split) and eval cases (held-out pages).
|
| 2 |
+
|
| 3 |
+
python finetune/build_items.py out/pages.jsonl out/cases.jsonl out/
|
| 4 |
+
"""
|
| 5 |
+
import json, os, sys
|
| 6 |
+
import torch
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 8 |
+
from common_ft import build_request, gold_for
|
| 9 |
+
from transformers import AutoTokenizer
|
| 10 |
+
from laya.common import QTYPES, build_sequence, render_options
|
| 11 |
+
|
| 12 |
+
MAX_LEN, HEAD_MAX_LEN = 1024, int(os.environ.get('LAYA_HEAD', '512'))
|
| 13 |
+
EVAL_EVERY = 5 # pages with index % 5 == 0 are held out
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
pages_f, cases_f, out = sys.argv[1:4]
|
| 18 |
+
pages = [json.loads(l) for l in open(pages_f)]
|
| 19 |
+
cases = [json.loads(l) for l in open(cases_f)]
|
| 20 |
+
for extra in sys.argv[4:]:
|
| 21 |
+
cases += [json.loads(l) for l in open(extra)]
|
| 22 |
+
snap = os.environ.get("LAYA_BASE")
|
| 23 |
+
tok = AutoTokenizer.from_pretrained(os.path.join(snap, "tokenizer"))
|
| 24 |
+
items, n_skip, ev = [], 0, []
|
| 25 |
+
import hashlib
|
| 26 |
+
STOPS = [" Stop when it is open.", " Stop once that page is visible.", " Then stop.", " Finish when it has loaded.", ""]
|
| 27 |
+
for c in cases:
|
| 28 |
+
if c["kind"] == "done" and "page_obj" not in c:
|
| 29 |
+
continue # template DONE cases leak phrasing; real ones come from make_done_cases.py
|
| 30 |
+
page = c.get("page_obj") or pages[c["page"]]
|
| 31 |
+
h = int(hashlib.md5(c["goal"].encode()).hexdigest(), 16)
|
| 32 |
+
goal = c["goal"] + STOPS[h % len(STOPS)]
|
| 33 |
+
c = {**c, "goal": goal}
|
| 34 |
+
state, questions, targets, controls = build_request(page, goal, c.get("history", []))
|
| 35 |
+
gop, gidx = gold_for(c, targets, controls)
|
| 36 |
+
if gop is None:
|
| 37 |
+
n_skip += 1; continue
|
| 38 |
+
held = (hashlib.md5(c["website"].encode()).digest()[0] % EVAL_EVERY == 0) if c.get("source") == "mind2web" else (c["page"] % EVAL_EVERY == 0)
|
| 39 |
+
if held:
|
| 40 |
+
ev.append({**c, "gold_index": gidx}); continue
|
| 41 |
+
golds = {"operation": gop}
|
| 42 |
+
if gidx is not None:
|
| 43 |
+
golds[gop.lower() + "_target"] = gidx
|
| 44 |
+
for qid, gold in golds.items():
|
| 45 |
+
q = questions[qid]; keys = list(q["criteria"])
|
| 46 |
+
target = [1.0 if k == gold else 0.0 for k in keys]
|
| 47 |
+
qq = {"t": "choice", "ins": json.dumps(q["instructions"]), "crit": q["criteria"]}
|
| 48 |
+
seq, markers = build_sequence(tok, state, qq, MAX_LEN, HEAD_MAX_LEN)
|
| 49 |
+
if len(markers) != len(render_options(qq)):
|
| 50 |
+
n_skip += 1; continue
|
| 51 |
+
item = {"ids": seq, "markers": markers, "qtype": QTYPES["choice"], "target": target, "label": keys.index(gold), "qid": qid, "gold_op": gop}
|
| 52 |
+
# class balance: CLICK dominates the operation question, so repeat the rare operations
|
| 53 |
+
reps = {"DONE": 4, "TYPE_TEXT": 3, "SELECT": 3}.get(gop, 1) if qid == "operation" else 1
|
| 54 |
+
if c.get("source") == "dagger":
|
| 55 |
+
reps *= 5 # on-policy teacher corrections from real tasks: few but exactly where the policy fails
|
| 56 |
+
items.extend([item] * reps)
|
| 57 |
+
torch.save(items, os.path.join(out, "train_items.pt"))
|
| 58 |
+
with open(os.path.join(out, "eval_cases.jsonl"), "w") as f:
|
| 59 |
+
for c in ev: f.write(json.dumps(c, ensure_ascii=False) + "\n")
|
| 60 |
+
lens = [len(i["ids"]) for i in items]
|
| 61 |
+
import collections
|
| 62 |
+
print("operation label counts:", dict(collections.Counter(i["gold_op"] for i in items if i["qid"] == "operation")))
|
| 63 |
+
print(f"train items {len(items)} (op {sum(i['qid']=='operation' for i in items)}, target {sum(i['qid']!='operation' for i in items)}), "
|
| 64 |
+
f"eval cases {len(ev)}, skipped {n_skip}, seq len mean {sum(lens)/len(lens):.0f} max {max(lens)}")
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
code/finetune/calibrate.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fit per-question-type temperatures on held-out cases (NLL), write them into rl_agent_config.json.
|
| 2 |
+
|
| 3 |
+
python finetune/calibrate.py out/pages.jsonl out/eval_cases.jsonl <checkpoint dir>
|
| 4 |
+
"""
|
| 5 |
+
import json, os, sys
|
| 6 |
+
import numpy as np, torch
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); sys.path.insert(0, "/home/ckl/projects/S/laya-upstream")
|
| 8 |
+
from common_ft import build_request, gold_for
|
| 9 |
+
import laya
|
| 10 |
+
from laya.common import QTYPES, build_sequence, collate_items
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
pages = [json.loads(l) for l in open(sys.argv[1])]; cases = [json.loads(l) for l in open(sys.argv[2])]; ck = sys.argv[3]
|
| 14 |
+
agent = laya.load(ck); agent.cfg["max_len"], agent.cfg["head_max_len"] = 1024, int(os.environ.get("LAYA_HEAD", agent.cfg.get("head_max_len_train", 512))); agent.accelerate()
|
| 15 |
+
Z, T, K = [], [], []
|
| 16 |
+
for c in cases[::2]:
|
| 17 |
+
state, questions, targets, controls = build_request(c.get("page_obj") or pages[c["page"]], c["goal"], c.get("history", []))
|
| 18 |
+
gop, gidx = gold_for(c, targets, controls)
|
| 19 |
+
golds = {"operation": gop}
|
| 20 |
+
if gidx is not None: golds[gop.lower() + "_target"] = gidx
|
| 21 |
+
items, keys = [], []
|
| 22 |
+
for qid, gold in golds.items():
|
| 23 |
+
q = questions[qid]; qq = {"t": "choice", "ins": json.dumps(q["instructions"]), "crit": q["criteria"]}
|
| 24 |
+
seq, markers = build_sequence(agent.tok, state, qq, 1024, agent.cfg['head_max_len'])
|
| 25 |
+
items.append({"ids": seq, "markers": markers, "qtype": QTYPES["choice"]}); keys.append((list(q["criteria"]), gold))
|
| 26 |
+
b = collate_items([items], agent.tok.pad_token_id)
|
| 27 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=agent.dtype):
|
| 28 |
+
logits, _ = agent.model(b["input_ids"].cuda(), b["attention_mask"].cuda(), b["marker_pos"].cuda(), b["marker_mask"].cuda(), b["qtype"].cuda())
|
| 29 |
+
for r, (ks, gold) in enumerate(keys):
|
| 30 |
+
z = logits[r, :len(ks)].float().cpu(); Z.append(z); T.append(ks.index(gold))
|
| 31 |
+
kmax = max(len(z) for z in Z); M = torch.full((len(Z), kmax), -1e4)
|
| 32 |
+
for i, z in enumerate(Z): M[i, :len(z)] = z
|
| 33 |
+
y = torch.tensor(T)
|
| 34 |
+
def nll(t): return torch.nn.functional.cross_entropy(M / t, y).item()
|
| 35 |
+
ts = np.exp(np.linspace(np.log(0.2), np.log(10), 200)); best = min(ts, key=nll)
|
| 36 |
+
acc = (M.argmax(-1) == y).float().mean().item()
|
| 37 |
+
conf0 = torch.softmax(M, -1).max(-1).values.mean().item(); conf1 = torch.softmax(M / best, -1).max(-1).values.mean().item()
|
| 38 |
+
print(f"n={len(Z)} acc={acc:.3f} T=1: nll {nll(1.0):.3f} mean conf {conf0:.3f} | T={best:.2f}: nll {nll(best):.3f} mean conf {conf1:.3f}")
|
| 39 |
+
cfgp = os.path.join(ck, "rl_agent_config.json"); cfg = json.load(open(cfgp)); cfg["temperature"] = [float(best), 1.0, 1.0]
|
| 40 |
+
json.dump(cfg, open(cfgp, "w"), indent=2); print("wrote temperature", best, "->", cfgp)
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
main()
|
code/finetune/collect_pages.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Crawl real pages with browser-harness and dump their jev-ultrafast observations (element tables + page text).
|
| 2 |
+
|
| 3 |
+
python finetune/collect_pages.py out/pages.jsonl [max_pages=80]
|
| 4 |
+
|
| 5 |
+
Starts from SEEDS, follows a few random same-site links from each page to diversify. One JSON line per page.
|
| 6 |
+
"""
|
| 7 |
+
import json, os, random, sys, time
|
| 8 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 9 |
+
os.environ.setdefault("BU_CDP_URL", "http://127.0.0.1:9222")
|
| 10 |
+
from jev_ultrafast.browser import Browser, StalePage
|
| 11 |
+
from jev_ultrafast.model import action_space
|
| 12 |
+
|
| 13 |
+
SEEDS = [
|
| 14 |
+
"https://en.wikipedia.org/wiki/Main_Page", "https://en.wikipedia.org/wiki/Special:Random", "https://en.wikipedia.org/wiki/Python_(programming_language)",
|
| 15 |
+
"https://news.ycombinator.com/", "https://news.ycombinator.com/newest", "https://news.ycombinator.com/login",
|
| 16 |
+
"https://github.com/", "https://github.com/tile-ai/tilelang", "https://github.com/browser-use/browser-use/issues", "https://github.com/login",
|
| 17 |
+
"https://www.python.org/", "https://docs.python.org/3/", "https://pypi.org/", "https://pypi.org/project/laya/",
|
| 18 |
+
"https://archlinux.org/", "https://wiki.archlinux.org/", "https://developer.mozilla.org/en-US/", "https://duckduckgo.com/",
|
| 19 |
+
"https://www.bing.com/", "https://stackoverflow.com/questions", "https://www.reddit.com/", "https://arxiv.org/",
|
| 20 |
+
"https://arxiv.org/list/cs.LG/recent", "https://huggingface.co/models", "https://huggingface.co/convaiinnovations/laya",
|
| 21 |
+
"https://www.saucedemo.com/", "https://the-internet.herokuapp.com/", "https://the-internet.herokuapp.com/login",
|
| 22 |
+
"https://demo.opencart.com/", "https://www.demoblaze.com/", "https://books.toscrape.com/", "https://quotes.toscrape.com/login",
|
| 23 |
+
"https://www.google.com/travel/flights?hl=en", "https://www.booking.com/", "https://www.airbnb.com/", "https://www.amazon.com/",
|
| 24 |
+
"https://www.ebay.com/", "https://www.imdb.com/", "https://www.nytimes.com/", "https://www.bbc.com/",
|
| 25 |
+
"https://www.openstreetmap.org/", "https://weather.com/", "https://www.wolframalpha.com/", "https://translate.google.com/",
|
| 26 |
+
"https://www.gnu.org/", "https://kernel.org/", "https://www.rust-lang.org/", "https://go.dev/", "https://nodejs.org/en",
|
| 27 |
+
"https://www.npmjs.com/", "https://crates.io/", "https://docs.rs/", "https://www.kaggle.com/", "https://paperswithcode.com/",
|
| 28 |
+
# round 2: more sites, more form-heavy pages
|
| 29 |
+
"https://en.wikipedia.org/wiki/Special:Search", "https://en.wikipedia.org/wiki/Portal:Current_events", "https://de.wikipedia.org/", "https://zh.wikipedia.org/",
|
| 30 |
+
"https://news.ycombinator.com/ask", "https://news.ycombinator.com/show", "https://news.ycombinator.com/jobs", "https://news.ycombinator.com/submit",
|
| 31 |
+
"https://github.com/explore", "https://github.com/trending", "https://github.com/pytorch/pytorch", "https://github.com/pytorch/pytorch/pulls",
|
| 32 |
+
"https://github.com/pytorch/pytorch/issues", "https://gitlab.com/explore", "https://gitee.com/explore", "https://about.gitlab.com/",
|
| 33 |
+
"https://www.python.org/downloads/", "https://docs.python.org/3/tutorial/", "https://docs.python.org/3/library/", "https://peps.python.org/",
|
| 34 |
+
"https://pypi.org/search/?q=torch", "https://pypi.org/project/torch/", "https://pypi.org/account/login/", "https://pypi.org/help/",
|
| 35 |
+
"https://the-internet.herokuapp.com/dropdown", "https://the-internet.herokuapp.com/checkboxes", "https://the-internet.herokuapp.com/forgot_password",
|
| 36 |
+
"https://the-internet.herokuapp.com/inputs", "https://the-internet.herokuapp.com/tables", "https://the-internet.herokuapp.com/javascript_alerts",
|
| 37 |
+
"https://demo.opencart.com/index.php?route=product/category&path=20", "https://demo.opencart.com/index.php?route=account/login",
|
| 38 |
+
"https://demo.opencart.com/index.php?route=account/register", "https://demo.opencart.com/index.php?route=product/search&search=mac",
|
| 39 |
+
"https://www.demoblaze.com/cart.html", "https://www.demoblaze.com/prod.html?idp_=1", "https://books.toscrape.com/catalogue/page-2.html",
|
| 40 |
+
"https://books.toscrape.com/catalogue/category/books/mystery_3/index.html", "https://quotes.toscrape.com/tag/love/", "https://quotes.toscrape.com/page/2/",
|
| 41 |
+
"https://www.saucedemo.com/inventory.html", "https://automationexercise.com/", "https://automationexercise.com/login", "https://automationexercise.com/products",
|
| 42 |
+
"https://practicetestautomation.com/practice-test-login/", "https://demoqa.com/", "https://demoqa.com/text-box", "https://demoqa.com/select-menu",
|
| 43 |
+
"https://demoqa.com/webtables", "https://www.selenium.dev/selenium/web/web-form.html", "https://formy-project.herokuapp.com/", "https://formy-project.herokuapp.com/form",
|
| 44 |
+
"https://parabank.parasoft.com/parabank/index.htm", "https://parabank.parasoft.com/parabank/register.htm", "https://www.globalsqa.com/angularJs-protractor/BankingProject/",
|
| 45 |
+
"https://opensource-demo.orangehrmlive.com/", "https://magento.softwaretestingboard.com/", "https://magento.softwaretestingboard.com/women.html",
|
| 46 |
+
"https://www.airbnb.com/s/London/homes", "https://www.booking.com/searchresults.html?ss=Paris", "https://www.google.com/travel/hotels?hl=en",
|
| 47 |
+
"https://www.google.com/maps?hl=en", "https://www.google.com/search?q=tilelang&hl=en", "https://duckduckgo.com/?q=modernbert", "https://www.bing.com/search?q=laya",
|
| 48 |
+
"https://arxiv.org/list/cs.CL/new", "https://arxiv.org/abs/2412.13663", "https://arxiv.org/search/?query=flash+attention&searchtype=all",
|
| 49 |
+
"https://huggingface.co/datasets", "https://huggingface.co/spaces", "https://huggingface.co/docs", "https://huggingface.co/login", "https://huggingface.co/answerdotai/ModernBERT-base",
|
| 50 |
+
"https://www.modelscope.cn/models", "https://www.modelscope.cn/datasets", "https://www.kaggle.com/datasets", "https://www.kaggle.com/competitions",
|
| 51 |
+
"https://stackoverflow.com/", "https://stackoverflow.com/questions/tagged/python", "https://superuser.com/", "https://askubuntu.com/",
|
| 52 |
+
"https://www.reddit.com/r/MachineLearning/", "https://old.reddit.com/", "https://old.reddit.com/r/python/", "https://lobste.rs/",
|
| 53 |
+
"https://www.bbc.com/news", "https://www.bbc.com/sport", "https://www.theguardian.com/international", "https://www.reuters.com/", "https://apnews.com/",
|
| 54 |
+
"https://www.imdb.com/chart/top/", "https://www.imdb.com/find/?q=inception", "https://www.rottentomatoes.com/", "https://www.goodreads.com/",
|
| 55 |
+
"https://www.openstreetmap.org/search?query=Berlin", "https://www.wikidata.org/", "https://commons.wikimedia.org/", "https://www.wiktionary.org/",
|
| 56 |
+
"https://developer.mozilla.org/en-US/docs/Web/JavaScript", "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select", "https://web.dev/",
|
| 57 |
+
"https://www.rust-lang.org/learn", "https://doc.rust-lang.org/book/", "https://go.dev/doc/", "https://pkg.go.dev/", "https://nodejs.org/en/download",
|
| 58 |
+
"https://www.npmjs.com/package/react", "https://react.dev/", "https://vuejs.org/", "https://tailwindcss.com/docs", "https://getbootstrap.com/docs/",
|
| 59 |
+
"https://www.wolframalpha.com/input?i=2%2B2", "https://translate.google.com/?sl=en&tl=zh-CN&text=hello", "https://www.deepl.com/translator",
|
| 60 |
+
"https://weather.com/weather/today/l/USNY0996", "https://www.timeanddate.com/", "https://www.xe.com/currencyconverter/", "https://www.calculator.net/",
|
| 61 |
+
"https://archlinux.org/packages/", "https://aur.archlinux.org/", "https://wiki.archlinux.org/title/Installation_guide", "https://www.kernel.org/doc/",
|
| 62 |
+
"https://www.debian.org/", "https://ubuntu.com/download", "https://www.gnu.org/software/", "https://www.fsf.org/",
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
def observe(url, timeout=25):
|
| 66 |
+
b = Browser(url)
|
| 67 |
+
try:
|
| 68 |
+
page = b.observe(screenshot=False)
|
| 69 |
+
links = b.evaluate("""(() => { const out=[]; for (const a of document.querySelectorAll('a[href]')) {
|
| 70 |
+
const h=a.href; if (h.startsWith(location.origin) && !h.includes('#') && h!==location.href) out.push(h); } return out.slice(0,400); })()""") or []
|
| 71 |
+
return page, links
|
| 72 |
+
finally:
|
| 73 |
+
b.close()
|
| 74 |
+
|
| 75 |
+
def main():
|
| 76 |
+
out, max_pages = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 80
|
| 77 |
+
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
|
| 78 |
+
seen = set()
|
| 79 |
+
if os.path.exists(out):
|
| 80 |
+
for line in open(out):
|
| 81 |
+
seen.add(json.loads(line)["url"])
|
| 82 |
+
rng = random.Random(0)
|
| 83 |
+
queue = list(SEEDS); rng.shuffle(queue)
|
| 84 |
+
n = len(seen)
|
| 85 |
+
with open(out, "a") as f:
|
| 86 |
+
while queue and n < max_pages:
|
| 87 |
+
url = queue.pop(0)
|
| 88 |
+
if url in seen:
|
| 89 |
+
continue
|
| 90 |
+
t = time.time()
|
| 91 |
+
try:
|
| 92 |
+
page, links = observe(url)
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"skip {url}: {type(e).__name__}: {str(e)[:80]}", flush=True); continue
|
| 95 |
+
elements, targets, controls = action_space(page["actions"])
|
| 96 |
+
if len(elements) < 5 or len(elements) > 160:
|
| 97 |
+
print(f"skip {url}: {len(elements)} elements", flush=True); continue
|
| 98 |
+
seen.add(page["url"]); n += 1
|
| 99 |
+
f.write(json.dumps({"url": page["url"], "title": page["title"], "text": page["text"], "actions": page["actions"],
|
| 100 |
+
"scroll": page.get("scroll")}, ensure_ascii=False) + "\n"); f.flush()
|
| 101 |
+
print(f"[{n}] {len(elements):3d} elements {time.time()-t:4.1f}s {page['title'][:60]}", flush=True)
|
| 102 |
+
rng.shuffle(links)
|
| 103 |
+
queue.extend(l for l in links[:3] if l not in seen)
|
| 104 |
+
print("done", n, "pages")
|
| 105 |
+
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
main()
|
code/finetune/common_ft.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared: build the exact jev-ultrafast systemone request for a page + goal, with the server-side compaction."""
|
| 2 |
+
import importlib.util, json, sys, types
|
| 3 |
+
|
| 4 |
+
JEV = "/home/ckl/projects/S/jev-ultrafast/jev_ultrafast"
|
| 5 |
+
# load model.py / questions.py without running the package __init__ (which pulls in browser-harness)
|
| 6 |
+
if "jev_ultrafast" not in sys.modules:
|
| 7 |
+
_pkg = types.ModuleType("jev_ultrafast"); _pkg.__path__ = [JEV]; sys.modules["jev_ultrafast"] = _pkg
|
| 8 |
+
for _name in ("questions", "model"):
|
| 9 |
+
_spec = importlib.util.spec_from_file_location(f"jev_ultrafast.{_name}", f"{JEV}/{_name}.py")
|
| 10 |
+
_m = importlib.util.module_from_spec(_spec); sys.modules[_spec.name] = _m; _spec.loader.exec_module(_m)
|
| 11 |
+
from jev_ultrafast.model import action_space # noqa: E402
|
| 12 |
+
from jev_ultrafast.questions import NEXT_ACTION, TARGET # noqa: E402
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
FMT = os.environ.get("LAYA_FMT", "v1")
|
| 16 |
+
# v1: jev's state verbatim (page text up to 6000 chars + the whole element table as JSON) -- the 1024-token budget truncates
|
| 17 |
+
# most of it, so the model often never sees the candidates' context. 3000 chars was tried (v7): -0.04 top-1.
|
| 18 |
+
# v2: elements live only in the option list (full label + role + value); state keeps title/url/history and 1500 chars of text.
|
| 19 |
+
# v3: v2 + option labels capped at 50 chars and 1200 chars of text (~30% fewer tokens; for the 322M base to hit ~20 ms/step)
|
| 20 |
+
PAGE_TEXT_CHARS = {"v2": 1500, "v3": 1200}.get(FMT, 6000)
|
| 21 |
+
LABEL_CHARS = 50 if FMT == "v3" else 10000
|
| 22 |
+
|
| 23 |
+
LABELS = {
|
| 24 |
+
"CLICK": "Click an element, button, menu option, autocomplete suggestion, or calendar day.",
|
| 25 |
+
"TYPE_TEXT": "Enter or replace text in an editable field. A small LLM will supply the value from the goal.",
|
| 26 |
+
"SELECT": "Select an observed dropdown value.",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def compact(v):
|
| 31 |
+
if isinstance(v, dict) and "element" in v:
|
| 32 |
+
s = str(v["element"])[:LABEL_CHARS]
|
| 33 |
+
if v.get("role"):
|
| 34 |
+
s += f" ({v['role']})"
|
| 35 |
+
if v.get("current_value"):
|
| 36 |
+
s += f" = {str(v['current_value'])[:30]!r}"
|
| 37 |
+
for k in ("checked", "selected", "expanded"):
|
| 38 |
+
if k in v:
|
| 39 |
+
s += f" {k}={v[k]}"
|
| 40 |
+
return s
|
| 41 |
+
return v
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def build_request(page, goal, history=()):
|
| 45 |
+
"""Mirror of jev_ultrafast.model.choose() up to the HTTP call. Returns (state, questions, targets, controls)."""
|
| 46 |
+
elements, targets, controls = action_space(page["actions"])
|
| 47 |
+
operations = {key: LABELS[key] for key in targets}
|
| 48 |
+
operations.update({key: value["label"] for key, value in controls.items()})
|
| 49 |
+
operations.update(DONE="Every requirement is visibly satisfied.", BLOCKED="No supported operation can progress.")
|
| 50 |
+
questions = {"operation": {"type": "choice", "criteria": operations, "instructions": {"goal": goal, "rules": NEXT_ACTION}}}
|
| 51 |
+
for operation, candidates in targets.items():
|
| 52 |
+
questions[operation.lower() + "_target"] = {
|
| 53 |
+
"type": "choice",
|
| 54 |
+
"criteria": {index: {"element": f"[{index}] {a['label']}", "current_value": a.get("current_value", a.get("value", "")),
|
| 55 |
+
**{k: a[k] for k in ("role", "checked", "selected", "expanded") if k in a}} for index, a in candidates.items()},
|
| 56 |
+
"instructions": {"goal": goal, "operation": operation, "rules": [NEXT_ACTION, TARGET]},
|
| 57 |
+
}
|
| 58 |
+
state = {"page": {"url": page["url"], "title": page["title"], "text": page["text"][:PAGE_TEXT_CHARS]},
|
| 59 |
+
"recent_actions": [{k: h.get(k) for k in ("action", "kind", "text", "page_changed")} for h in list(history)[-10:]]}
|
| 60 |
+
if FMT not in ("v2", "v3"):
|
| 61 |
+
state["elements"] = elements
|
| 62 |
+
for q in questions.values():
|
| 63 |
+
q["criteria"] = {k: compact(v) for k, v in q["criteria"].items()}
|
| 64 |
+
return state, questions, targets, controls
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def gold_for(case, targets, controls):
|
| 68 |
+
"""(gold operation key, gold target index or None) for a case in the operation/target question vocab."""
|
| 69 |
+
op = case["gold_op"]
|
| 70 |
+
if op == "DONE":
|
| 71 |
+
return "DONE", None
|
| 72 |
+
if op in ("SCROLL_DOWN", "SCROLL_UP", "WAIT"): # page-level controls: operation question only
|
| 73 |
+
return (op, None) if op in {k.upper() for k in controls} else (None, None)
|
| 74 |
+
for index, a in targets.get(op, {}).items():
|
| 75 |
+
if a["id"] == case["gold_id"]:
|
| 76 |
+
return op, index
|
| 77 |
+
return None, None
|
code/finetune/convert_mind2web.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mind2Web (osunlp/Mind2Web from ModelScope) -> cases in this repo's format (page_obj with jev-style actions).
|
| 2 |
+
|
| 3 |
+
python finetune/convert_mind2web.py finetune/data/mind2web/data/train/*.json finetune/out/m2w_cases.jsonl
|
| 4 |
+
|
| 5 |
+
Each Mind2Web action becomes one case: goal = confirmed_task, history = previous action_reprs, gold = the positive
|
| 6 |
+
candidate; the element table = positive + up to MAX_NEG sampled negative candidates rendered like jev's snapshot.js
|
| 7 |
+
(label from text / aria-label / placeholder / alt / title / value, role from tag).
|
| 8 |
+
"""
|
| 9 |
+
import json, random, re, sys
|
| 10 |
+
from bs4 import BeautifulSoup
|
| 11 |
+
|
| 12 |
+
MAX_NEG = 44
|
| 13 |
+
ROLE = {"a": "link", "button": "button", "input": "textbox", "textarea": "textbox", "select": "combobox", "option": "option",
|
| 14 |
+
"img": "img", "li": "listitem", "label": "label", "span": "generic", "div": "generic", "svg": "img", "h1": "heading",
|
| 15 |
+
"h2": "heading", "h3": "heading", "p": "text", "td": "cell", "th": "columnheader", "tr": "row", "ul": "list"}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def label_of(el):
|
| 19 |
+
attrs = el.attrs
|
| 20 |
+
for k in ("aria-label", "placeholder", "alt", "title"):
|
| 21 |
+
v = attrs.get(k)
|
| 22 |
+
if isinstance(v, list): v = " ".join(v)
|
| 23 |
+
if v and v.strip(): return v.strip()
|
| 24 |
+
txt = " ".join(el.get_text(" ", strip=True).split())
|
| 25 |
+
if txt: return txt[:80]
|
| 26 |
+
v = attrs.get("value")
|
| 27 |
+
if v: return str(v).strip()[:80]
|
| 28 |
+
return (attrs.get("name") or attrs.get("id") or el.name or "")[:60]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def kind_of(el, op):
|
| 32 |
+
t = el.name
|
| 33 |
+
typ = (el.attrs.get("type") or "").lower()
|
| 34 |
+
if t == "select": return "select"
|
| 35 |
+
if t == "textarea" or (t == "input" and typ in ("", "text", "search", "email", "password", "number", "tel", "url")) or el.attrs.get("contenteditable"):
|
| 36 |
+
return "fill"
|
| 37 |
+
return "click"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def convert_action(task, ai, action, rng):
|
| 41 |
+
soup = BeautifulSoup(action["cleaned_html"], "lxml")
|
| 42 |
+
by_id = {}
|
| 43 |
+
for el in soup.find_all(attrs={"backend_node_id": True}):
|
| 44 |
+
by_id[el.attrs["backend_node_id"]] = el
|
| 45 |
+
pos = action["pos_candidates"]
|
| 46 |
+
if not pos: return None
|
| 47 |
+
gold_el = by_id.get(pos[0]["backend_node_id"])
|
| 48 |
+
if gold_el is None: return None
|
| 49 |
+
negs = [c for c in action["neg_candidates"] if c["backend_node_id"] in by_id]
|
| 50 |
+
rng.shuffle(negs)
|
| 51 |
+
cands = [pos[0]] + negs[:MAX_NEG]
|
| 52 |
+
rng.shuffle(cands)
|
| 53 |
+
op = action["operation"]["op"]
|
| 54 |
+
# values typed/selected in earlier steps: a field that already holds its value must show it (jev's rules key on that)
|
| 55 |
+
filled = {}
|
| 56 |
+
for r in task["action_reprs"][:ai]:
|
| 57 |
+
m = re.match(r"\[(\w+)\]\s+(.*?)\s+->\s+(TYPE|SELECT):\s*(.*)$", r)
|
| 58 |
+
if m: filled[" ".join(m.group(2).split()).lower()] = m.group(4).strip()
|
| 59 |
+
actions, gold_id, node = [], None, 0
|
| 60 |
+
for c in cands:
|
| 61 |
+
el = by_id[c["backend_node_id"]]
|
| 62 |
+
lab = label_of(el)
|
| 63 |
+
if not lab: continue
|
| 64 |
+
node += 1
|
| 65 |
+
is_gold = c["backend_node_id"] == pos[0]["backend_node_id"]
|
| 66 |
+
kind = {"CLICK": "click", "TYPE": "fill", "SELECT": "select"}[op] if is_gold else kind_of(el, op)
|
| 67 |
+
role = ROLE.get(el.name, "generic")
|
| 68 |
+
base = {"node": node, "label": lab, "role": role}
|
| 69 |
+
if kind == "select":
|
| 70 |
+
opts = [o.get_text(" ", strip=True) for o in el.find_all("option")][:8] or [action["operation"]["value"] or "option"]
|
| 71 |
+
if is_gold and action["operation"]["value"] and action["operation"]["value"] not in opts:
|
| 72 |
+
opts = [action["operation"]["value"]] + opts[:7]
|
| 73 |
+
for oi, o in enumerate(opts):
|
| 74 |
+
a = {**base, "id": f"select:{node}:{oi}", "kind": "select", "value": o, "label": f"{lab} → {o}", "current_value": ""}
|
| 75 |
+
actions.append(a)
|
| 76 |
+
if is_gold and (o == action["operation"]["value"] or (oi == 0 and not action["operation"]["value"])): gold_id = a["id"]
|
| 77 |
+
continue
|
| 78 |
+
a = {**base, "id": f"{kind}:{node}", "kind": kind}
|
| 79 |
+
if kind == "fill":
|
| 80 |
+
a["value"] = filled.get(lab.lower(), "")
|
| 81 |
+
a["current_value"] = a["value"]
|
| 82 |
+
actions.append(a)
|
| 83 |
+
if is_gold: gold_id = a["id"]
|
| 84 |
+
if gold_id is None: return None
|
| 85 |
+
for k, lab in (("wait", "Wait for the page to update"), ("scroll_down", "Scroll down"), ("scroll_up", "Scroll up")):
|
| 86 |
+
actions.append({"id": k, "kind": "wait" if k == "wait" else "scroll", "label": lab, "node": None, "delta": 600 if k == "scroll_down" else -600})
|
| 87 |
+
text = " ".join(soup.get_text(" ", strip=True).split())[:6000]
|
| 88 |
+
hist = []
|
| 89 |
+
for r in task["action_reprs"][:ai]:
|
| 90 |
+
lab_, _, opv = r.rpartition(" -> ")
|
| 91 |
+
kind_, _, val = opv.partition(": ")
|
| 92 |
+
hist.append({"action": lab_.strip(), "kind": {"CLICK": "click", "TYPE": "fill", "SELECT": "select"}.get(kind_, kind_.lower()),
|
| 93 |
+
"text": val or None, "page_changed": kind_ == "CLICK"})
|
| 94 |
+
gold_op = {"CLICK": "CLICK", "TYPE": "TYPE_TEXT", "SELECT": "SELECT"}[op]
|
| 95 |
+
return {"page": -1, "url": f"https://{task['website']}.com/", "title": task["website"], "goal": task["confirmed_task"], "gold_op": gold_op,
|
| 96 |
+
"gold_id": gold_id, "kind": {"CLICK": "click", "TYPE": "fill", "SELECT": "select"}[op], "label": label_of(gold_el), "history": hist,
|
| 97 |
+
"source": "mind2web", "task_id": task["annotation_id"], "website": task["website"],
|
| 98 |
+
"page_obj": {"url": f"https://{task['website']}.com/", "title": task["website"], "text": text, "actions": actions}}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def main():
|
| 102 |
+
files, out = sys.argv[1:-1], sys.argv[-1]
|
| 103 |
+
rng = random.Random(0); n = 0; skipped = 0
|
| 104 |
+
with open(out, "w") as f:
|
| 105 |
+
for fn in files:
|
| 106 |
+
tasks = json.load(open(fn))
|
| 107 |
+
for t in tasks:
|
| 108 |
+
for ai, a in enumerate(t["actions"]):
|
| 109 |
+
c = convert_action(t, ai, a, rng)
|
| 110 |
+
if c is None: skipped += 1; continue
|
| 111 |
+
f.write(json.dumps(c, ensure_ascii=False) + "\n"); n += 1
|
| 112 |
+
print(f"{fn}: total {n} cases, skipped {skipped}", flush=True)
|
| 113 |
+
print("wrote", n)
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
main()
|
code/finetune/dagger.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DAgger-style harvesting: run real tasks with the current laya agent, and at every step ask the local Qwen teacher which
|
| 2 |
+
action is right given the page's element table. Disagreements (and agreements) become training cases with the *agent's*
|
| 3 |
+
on-policy states, so the next model learns exactly where this one goes wrong.
|
| 4 |
+
|
| 5 |
+
python finetune/dagger.py out/dagger_cases.jsonl [tasks.jsonl] (services: chromium 9222, laya 8791, sglang 30000)
|
| 6 |
+
|
| 7 |
+
tasks.jsonl lines: {"url": ..., "goal": ...}; default = apps/browser_suite.TASKS plus extra goals below.
|
| 8 |
+
"""
|
| 9 |
+
import json, os, sys, time
|
| 10 |
+
import httpx
|
| 11 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast"); sys.path.insert(0, "/home/ckl/projects/S/laya/apps")
|
| 12 |
+
os.environ.update(BU_CDP_URL="http://127.0.0.1:9222", TYPESAFE_BASE_URL="http://127.0.0.1:8791", TYPESAFE_API_KEY="local",
|
| 13 |
+
TEXT_MODEL_API_KEY="local", TEXT_MODEL_BASE_URL="http://127.0.0.1:30000/v1", TEXT_MODEL="Qwen/Qwen3-8B-AWQ",
|
| 14 |
+
TEXT_MODEL_EXTRA_JSON='{"chat_template_kwargs": {"enable_thinking": false}}')
|
| 15 |
+
from jev_ultrafast import Agent
|
| 16 |
+
from jev_ultrafast.model import action_space
|
| 17 |
+
from browser_suite import TASKS
|
| 18 |
+
|
| 19 |
+
EXTRA = [
|
| 20 |
+
("https://en.wikipedia.org/wiki/Main_Page", "Open the article about Albert Einstein."),
|
| 21 |
+
("https://news.ycombinator.com/", "Open the 'past' page."),
|
| 22 |
+
("https://news.ycombinator.com/", "Open the comments of the first story on the front page."),
|
| 23 |
+
("https://github.com/tile-ai/tilelang", "Open the Pull requests tab."),
|
| 24 |
+
("https://github.com/tile-ai/tilelang", "Open the README's 'examples' folder."),
|
| 25 |
+
("https://www.python.org/", "Open the documentation page."),
|
| 26 |
+
("https://docs.python.org/3/", "Open the tutorial."),
|
| 27 |
+
("https://books.toscrape.com/", "Open the 'Mystery' category and then open the first book in it."),
|
| 28 |
+
("https://books.toscrape.com/", "Go to page 2 of the catalogue."),
|
| 29 |
+
("https://the-internet.herokuapp.com/", "Open the 'Dropdown' example and select 'Option 2'."),
|
| 30 |
+
("https://the-internet.herokuapp.com/", "Open the 'Checkboxes' example and tick the first checkbox."),
|
| 31 |
+
("https://quotes.toscrape.com/", "Open the quotes tagged 'love'."),
|
| 32 |
+
("https://quotes.toscrape.com/", "Go to the next page of quotes."),
|
| 33 |
+
("https://arxiv.org/", "Open the listing of new submissions in cs.CL."),
|
| 34 |
+
("https://pypi.org/", "Search PyPI for 'tilelang' and open the project page."),
|
| 35 |
+
("https://duckduckgo.com/", "Search for 'ModernBERT paper'."),
|
| 36 |
+
("https://www.saucedemo.com/", "Log in with username 'standard_user' and password 'secret_sauce', then add the 'Sauce Labs Backpack' to the cart."),
|
| 37 |
+
("https://demo.opencart.com/", "Open the 'Desktops' category from the top menu."),
|
| 38 |
+
("https://www.demoblaze.com/", "Open the 'Laptops' category."),
|
| 39 |
+
("https://huggingface.co/models", "Search models for 'laya'."),
|
| 40 |
+
]
|
| 41 |
+
TEACHER = os.environ["TEXT_MODEL_BASE_URL"] + "/chat/completions"
|
| 42 |
+
client = httpx.Client(timeout=180)
|
| 43 |
+
SYS = """You are the teacher for a browser agent. You see the user's goal, the actions taken so far, the current page (title, url, text excerpt) and a
|
| 44 |
+
numbered table of the controls on it. Decide the single best NEXT step:
|
| 45 |
+
- {"operation": "CLICK", "index": n} click control n
|
| 46 |
+
- {"operation": "TYPE_TEXT", "index": n} type into text control n (a separate helper supplies the value)
|
| 47 |
+
- {"operation": "SELECT", "index": n, "value": "..."} choose that option of select control n
|
| 48 |
+
- {"operation": "DONE"} every requirement of the goal is already visibly satisfied on this page
|
| 49 |
+
- {"operation": "WAIT"} / {"operation": "SCROLL_DOWN"} / {"operation": "SCROLL_UP"}
|
| 50 |
+
Do not re-do satisfied steps; a field that already shows the requested value is done. Return JSON only."""
|
| 51 |
+
|
| 52 |
+
def teach(goal, history, page, elements):
|
| 53 |
+
table = [{"index": e["index"], "label": e["label"][:70], "role": e.get("role"), "ops": e["operations"], **({"value": e["value"]} if e.get("value") else {})} for e in elements[:120]]
|
| 54 |
+
user = {"goal": goal, "actions_so_far": [{k: h.get(k) for k in ("action", "kind", "text")} for h in history[-8:]],
|
| 55 |
+
"page": {"title": page["title"], "url": page["url"], "text": page["text"][:2500]}, "controls": table}
|
| 56 |
+
body = {"model": os.environ["TEXT_MODEL"], "max_tokens": 120, "temperature": 0.0, "response_format": {"type": "json_object"},
|
| 57 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 58 |
+
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}]}
|
| 59 |
+
r = client.post(TEACHER, json=body).json()
|
| 60 |
+
return json.loads(r["choices"][0]["message"]["content"])
|
| 61 |
+
|
| 62 |
+
def to_case(goal, history, page, verdict):
|
| 63 |
+
elements, targets, controls = action_space(page["actions"])
|
| 64 |
+
op = str(verdict.get("operation", "")).upper()
|
| 65 |
+
if op in ("DONE",):
|
| 66 |
+
return {"page": -1, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": "DONE", "gold_id": "DONE", "kind": "done", "label": "",
|
| 67 |
+
"history": history, "source": "dagger", "page_obj": {k: page[k] for k in ("url", "title", "text", "actions")}}
|
| 68 |
+
if op in ("CLICK", "TYPE_TEXT", "SELECT"):
|
| 69 |
+
idx = str(verdict.get("index"))
|
| 70 |
+
cands = targets.get(op, {})
|
| 71 |
+
if op == "SELECT":
|
| 72 |
+
hit = [k for k, a in cands.items() if k.split(":")[0] == idx and (a["value"] == verdict.get("value") or a["label"].endswith(str(verdict.get("value"))))]
|
| 73 |
+
key = hit[0] if hit else None
|
| 74 |
+
else:
|
| 75 |
+
key = idx if idx in cands else None
|
| 76 |
+
if key is None: return None
|
| 77 |
+
a = cands[key]
|
| 78 |
+
return {"page": -1, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": op, "gold_id": a["id"], "kind": a["kind"], "label": a["label"],
|
| 79 |
+
"history": history, "source": "dagger", "page_obj": {k: page[k] for k in ("url", "title", "text", "actions")}}
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def main():
|
| 83 |
+
out = sys.argv[1]
|
| 84 |
+
tasks = [(u, g) for _, u, g, _ in TASKS] + EXTRA
|
| 85 |
+
if len(sys.argv) > 2:
|
| 86 |
+
tasks += [(json.loads(l)["url"], json.loads(l)["goal"]) for l in open(sys.argv[2])]
|
| 87 |
+
n_cases = n_dis = 0
|
| 88 |
+
with open(out, "a") as f:
|
| 89 |
+
for url, goal in tasks:
|
| 90 |
+
t0 = time.time()
|
| 91 |
+
try:
|
| 92 |
+
with Agent(url, goal) as agent:
|
| 93 |
+
steps = 0
|
| 94 |
+
while agent.state["status"] not in ("done", "blocked") and steps < 12:
|
| 95 |
+
page = agent.state["page"]; history = list(agent.state["history"])
|
| 96 |
+
elements = action_space(page["actions"])[0]
|
| 97 |
+
try:
|
| 98 |
+
verdict = teach(goal, history, page, elements)
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(" teacher fail", str(e)[:60]); break
|
| 101 |
+
case = to_case(goal, [{k: h.get(k) for k in ("action", "kind", "text", "page_changed")} for h in history], page, verdict)
|
| 102 |
+
if case:
|
| 103 |
+
f.write(json.dumps(case, ensure_ascii=False) + "\n"); f.flush(); n_cases += 1
|
| 104 |
+
# step the agent with its own policy
|
| 105 |
+
try:
|
| 106 |
+
st = agent.command("tick")
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(" tick fail", type(e).__name__, str(e)[:50]); break
|
| 109 |
+
d = st["decisions"][-1] if st["decisions"] else None
|
| 110 |
+
agent_choice = (d["operation"], d.get("target")) if d else None
|
| 111 |
+
teacher_choice = (str(verdict.get("operation", "")).upper(), str(verdict.get("index")) if verdict.get("index") is not None else None)
|
| 112 |
+
if agent_choice and agent_choice[0] != teacher_choice[0] or (agent_choice and agent_choice[1] != teacher_choice[1] and teacher_choice[0] in ("CLICK", "TYPE_TEXT")):
|
| 113 |
+
n_dis += 1
|
| 114 |
+
steps += 1
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f" task fail {type(e).__name__}: {str(e)[:60]}")
|
| 117 |
+
print(f"{goal[:60]:60s} cases={n_cases} disagreements={n_dis} {time.time()-t0:.0f}s", flush=True)
|
| 118 |
+
print("wrote", n_cases, "cases ->", out)
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
code/finetune/eval.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Held-out eval: operation accuracy and target top-1 (given the gold operation) on unseen pages.
|
| 2 |
+
|
| 3 |
+
python finetune/eval.py out/pages.jsonl out/eval_cases.jsonl <checkpoint dir> [subfolder]
|
| 4 |
+
"""
|
| 5 |
+
import json, os, sys, time
|
| 6 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
sys.path.insert(0, "/home/ckl/projects/S/laya-upstream") # laya with Agent.accelerate()
|
| 8 |
+
from common_ft import build_request
|
| 9 |
+
import laya
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
pages = [json.loads(l) for l in open(sys.argv[1])]; cases = [json.loads(l) for l in open(sys.argv[2])]
|
| 13 |
+
agent = laya.load(sys.argv[3], subfolder=sys.argv[4] if len(sys.argv) > 4 else None)
|
| 14 |
+
agent.cfg["max_len"], agent.cfg["head_max_len"] = 1024, int(os.environ.get("LAYA_HEAD", agent.cfg.get("head_max_len_train", 512)))
|
| 15 |
+
agent.accelerate()
|
| 16 |
+
op_ok = tgt_ok = tgt_n = 0; ranks = []; t = time.time(); by_kind = {}
|
| 17 |
+
for c in cases:
|
| 18 |
+
state, questions, targets, controls = build_request(c.get("page_obj") or pages[c["page"]], c["goal"], c.get("history", []))
|
| 19 |
+
r = agent.predict(state, questions)["answers"]
|
| 20 |
+
op_hit = r["operation"]["choice"] == c["gold_op"]; op_ok += op_hit
|
| 21 |
+
k = by_kind.setdefault(f"{c.get('source', 'live'):9s} {c['gold_op']}", [0, 0, 0]); k[0] += 1; k[1] += op_hit
|
| 22 |
+
if c.get("gold_index") is not None:
|
| 23 |
+
a = r[c["gold_op"].lower() + "_target"]; probs = a["probabilities"]
|
| 24 |
+
order = sorted(probs, key=probs.get, reverse=True); rank = order.index(c["gold_index"]) + 1
|
| 25 |
+
tgt_n += 1; tgt_ok += rank == 1; ranks.append(rank / len(probs)); k[2] += rank == 1
|
| 26 |
+
dt = (time.time() - t) / len(cases) * 1000
|
| 27 |
+
print(f"{sys.argv[3]}/{sys.argv[4] if len(sys.argv) > 4 else ''}: cases {len(cases)} operation acc {op_ok/len(cases):.3f} "
|
| 28 |
+
f"target top-1 {tgt_ok/max(1,tgt_n):.3f} (n={tgt_n}, mean normalized rank {sum(ranks)/max(1,len(ranks)):.3f}) {dt:.0f} ms/case")
|
| 29 |
+
for kind, (n, o, tg) in sorted(by_kind.items()):
|
| 30 |
+
print(f" {kind:19s} n={n:4d} op acc {o/n:.2f}" + (f" target top-1 {tg/n:.2f}" if not kind.endswith("DONE") else ""))
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
main()
|
code/finetune/gen_goals.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reverse-generate browser goals with a local LLM: pick an element as gold, ask Qwen to write the user goal for it.
|
| 2 |
+
|
| 3 |
+
python finetune/gen_goals.py out/pages.jsonl out/cases.jsonl [per_page=12]
|
| 4 |
+
|
| 5 |
+
Each case: {url, title, goal, gold_op, gold_id, gold_node, kind, label}. Also one DONE case per page.
|
| 6 |
+
"""
|
| 7 |
+
import json, os, random, sys, threading
|
| 8 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 9 |
+
import httpx
|
| 10 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 11 |
+
from jev_ultrafast.model import action_space
|
| 12 |
+
|
| 13 |
+
LLM = os.environ.get("TEXT_MODEL_BASE_URL", "http://127.0.0.1:30000/v1") + "/chat/completions"
|
| 14 |
+
MODEL = os.environ.get("TEXT_MODEL", "Qwen/Qwen3-8B-AWQ")
|
| 15 |
+
client = httpx.Client(timeout=120)
|
| 16 |
+
|
| 17 |
+
SYS = """You write realistic browser-automation goals. Given a web page and ONE target control on it, write the goal a user
|
| 18 |
+
would give to an assistant such that the assistant's NEXT step is to use exactly that control. Rules:
|
| 19 |
+
- One or two sentences, natural language, from the user's perspective, mention what they want (not the UI mechanics).
|
| 20 |
+
- The goal must single out the target among the other listed controls; do not mention element numbers.
|
| 21 |
+
- For a text field, the goal must imply typing a concrete value into it (include the value).
|
| 22 |
+
- For a dropdown option, the goal must imply choosing that option.
|
| 23 |
+
- Vary phrasing: sometimes terse ("open the login page"), sometimes contextual ("I want to read about X, take me there").
|
| 24 |
+
Return JSON: {"goal": "..."}"""
|
| 25 |
+
|
| 26 |
+
def ask(page, target, others):
|
| 27 |
+
user = {"page": {"title": page["title"], "url": page["url"], "text_excerpt": page["text"][:700]},
|
| 28 |
+
"target": {"kind": target["kind"], "label": target["label"], "role": target.get("role"),
|
| 29 |
+
"value": target.get("value", target.get("current_value", ""))},
|
| 30 |
+
"other_controls_on_page": [o["label"][:60] for o in others]}
|
| 31 |
+
body = {"model": MODEL, "max_tokens": 200, "temperature": 0.9, "response_format": {"type": "json_object"},
|
| 32 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 33 |
+
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}]}
|
| 34 |
+
r = client.post(LLM, json=body).json()
|
| 35 |
+
goal = json.loads(r["choices"][0]["message"]["content"])["goal"]
|
| 36 |
+
return goal.strip()
|
| 37 |
+
|
| 38 |
+
def main():
|
| 39 |
+
src, out, per_page = sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 12
|
| 40 |
+
pages = [json.loads(l) for l in open(src)]
|
| 41 |
+
rng = random.Random(1)
|
| 42 |
+
jobs = []
|
| 43 |
+
for pi, page in enumerate(pages):
|
| 44 |
+
elements, targets, controls = action_space(page["actions"])
|
| 45 |
+
cands = [a for a in page["actions"] if a["kind"] in ("click", "fill", "select")]
|
| 46 |
+
# de-duplicate by label, prefer informative labels
|
| 47 |
+
seen, uniq = set(), []
|
| 48 |
+
for a in cands:
|
| 49 |
+
lab = a["label"].split(" → ")[0].strip()
|
| 50 |
+
if len(lab) < 2 or lab.lower() in seen: continue
|
| 51 |
+
seen.add(lab.lower()); uniq.append(a)
|
| 52 |
+
rng.shuffle(uniq)
|
| 53 |
+
fills = [a for a in uniq if a["kind"] == "fill"][:3]
|
| 54 |
+
picks = fills + [a for a in uniq if a["kind"] != "fill"][: max(0, per_page - len(fills))]
|
| 55 |
+
for a in picks:
|
| 56 |
+
others = rng.sample([o for o in uniq if o is not a], min(10, len(uniq) - 1))
|
| 57 |
+
jobs.append((pi, page, a, others))
|
| 58 |
+
print(f"{len(pages)} pages -> {len(jobs)} goal jobs", flush=True)
|
| 59 |
+
lock = threading.Lock(); done = [0]
|
| 60 |
+
def work(job):
|
| 61 |
+
pi, page, a, others = job
|
| 62 |
+
try:
|
| 63 |
+
goal = ask(page, a, others)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print("fail", type(e).__name__, str(e)[:60], flush=True); return None
|
| 66 |
+
with lock:
|
| 67 |
+
done[0] += 1
|
| 68 |
+
if done[0] % 50 == 0: print(f" {done[0]}/{len(jobs)}", flush=True)
|
| 69 |
+
op = {"click": "CLICK", "fill": "TYPE_TEXT", "select": "SELECT"}[a["kind"]]
|
| 70 |
+
return {"page": pi, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": op, "gold_id": a["id"],
|
| 71 |
+
"gold_node": a.get("node"), "kind": a["kind"], "label": a["label"]}
|
| 72 |
+
with ThreadPoolExecutor(16) as ex:
|
| 73 |
+
cases = [c for c in ex.map(work, jobs) if c]
|
| 74 |
+
for pi, page in enumerate(pages): # DONE cases: the goal is already satisfied by the current page
|
| 75 |
+
cases.append({"page": pi, "url": page["url"], "title": page["title"], "gold_op": "DONE", "gold_id": "DONE", "kind": "done",
|
| 76 |
+
"label": "", "goal": rng.choice([f"Open the page titled '{page['title'][:70]}'. Stop once it is open.",
|
| 77 |
+
f"Go to {page['url']} and stop when it has loaded.",
|
| 78 |
+
f"Navigate to the '{page['title'][:50]}' page."])})
|
| 79 |
+
with open(out, "w") as f:
|
| 80 |
+
for c in cases: f.write(json.dumps(c, ensure_ascii=False) + "\n")
|
| 81 |
+
print("wrote", len(cases), "cases ->", out)
|
| 82 |
+
for c in rng.sample(cases, 8): print(f" [{c['gold_op']:9s}] {c['label'][:35]:35s} <- {c['goal'][:90]}")
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
main()
|
code/finetune/gen_step2.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Step-2 negatives: on landing pages from done_cases (history = one executed click), reverse-generate NEW goals whose
|
| 2 |
+
next step is another element on that page. Breaks the 'any history => DONE' shortcut.
|
| 3 |
+
|
| 4 |
+
python finetune/gen_step2.py out/done_cases.jsonl out/step2_cases.jsonl [per_page=3]
|
| 5 |
+
"""
|
| 6 |
+
import json, random, sys, threading
|
| 7 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 8 |
+
sys.path.insert(0, "/home/ckl/projects/S/laya/finetune")
|
| 9 |
+
from gen_goals import ask
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
src, out, per = sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 3
|
| 13 |
+
dones = [json.loads(l) for l in open(src)]
|
| 14 |
+
rng = random.Random(7); jobs = []
|
| 15 |
+
for d in dones:
|
| 16 |
+
page = d["page_obj"]
|
| 17 |
+
cands = [a for a in page["actions"] if a["kind"] in ("click", "fill", "select") and len(a["label"].split(" → ")[0].strip()) > 1
|
| 18 |
+
and a["label"] != d["label"]]
|
| 19 |
+
seen, uniq = set(), []
|
| 20 |
+
for a in cands:
|
| 21 |
+
k = a["label"].lower()
|
| 22 |
+
if k in seen: continue
|
| 23 |
+
seen.add(k); uniq.append(a)
|
| 24 |
+
rng.shuffle(uniq)
|
| 25 |
+
fills = [a for a in uniq if a["kind"] == "fill"][:1]
|
| 26 |
+
for a in fills + [a for a in uniq if a["kind"] != "fill"][: per - len(fills)]:
|
| 27 |
+
jobs.append((d, a, rng.sample([o for o in uniq if o is not a], min(10, len(uniq) - 1))))
|
| 28 |
+
print(f"{len(dones)} landing pages -> {len(jobs)} jobs", flush=True)
|
| 29 |
+
lock = threading.Lock(); n = [0]
|
| 30 |
+
def work(job):
|
| 31 |
+
d, a, others = job
|
| 32 |
+
try:
|
| 33 |
+
goal = ask(d["page_obj"], a, others)
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print("fail", str(e)[:60], flush=True); return None
|
| 36 |
+
with lock:
|
| 37 |
+
n[0] += 1
|
| 38 |
+
if n[0] % 100 == 0: print(f" {n[0]}/{len(jobs)}", flush=True)
|
| 39 |
+
op = {"click": "CLICK", "fill": "TYPE_TEXT", "select": "SELECT"}[a["kind"]]
|
| 40 |
+
# keep the history: the previous click is unrelated to the new goal, which is what happens mid-task all the time
|
| 41 |
+
return {**{k: d[k] for k in ("page", "url", "title", "history", "page_obj")}, "goal": goal, "gold_op": op, "gold_id": a["id"],
|
| 42 |
+
"gold_node": a.get("node"), "kind": a["kind"], "label": a["label"], "source": "live"}
|
| 43 |
+
with ThreadPoolExecutor(16) as ex:
|
| 44 |
+
cases = [c for c in ex.map(work, jobs) if c]
|
| 45 |
+
with open(out, "w") as f:
|
| 46 |
+
for c in cases: f.write(json.dumps(c, ensure_ascii=False) + "\n")
|
| 47 |
+
print("wrote", len(cases))
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
code/finetune/make_done_cases.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Turn click cases into realistic DONE cases by actually executing the click in the browser.
|
| 2 |
+
|
| 3 |
+
python finetune/make_done_cases.py out/pages.jsonl out/cases.jsonl out/done_cases.jsonl [max=250]
|
| 4 |
+
|
| 5 |
+
For a click case (page A, goal, gold element): open A, click the gold element, observe the landing page B.
|
| 6 |
+
If the page changed, emit {goal, gold_op: DONE, page_obj: B, history: [that click]} with the same goal phrasing.
|
| 7 |
+
"""
|
| 8 |
+
import json, os, random, sys, time
|
| 9 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 10 |
+
os.environ.setdefault("BU_CDP_URL", "http://127.0.0.1:9222")
|
| 11 |
+
from jev_ultrafast.browser import Browser
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
pages_f, cases_f, out = sys.argv[1:4]; mx = int(sys.argv[4]) if len(sys.argv) > 4 else 250
|
| 15 |
+
pages = [json.loads(l) for l in open(pages_f)]
|
| 16 |
+
cases = [c for c in (json.loads(l) for l in open(cases_f)) if c["kind"] == "click"]
|
| 17 |
+
rng = random.Random(3); rng.shuffle(cases)
|
| 18 |
+
n_ok = n_try = 0
|
| 19 |
+
with open(out, "w") as f:
|
| 20 |
+
for c in cases:
|
| 21 |
+
if n_ok >= mx: break
|
| 22 |
+
src = pages[c["page"]]; n_try += 1
|
| 23 |
+
try:
|
| 24 |
+
b = Browser(src["url"])
|
| 25 |
+
try:
|
| 26 |
+
page = b.observe(screenshot=False)
|
| 27 |
+
act = next((a for a in page["actions"] if a["kind"] == "click" and a["label"] == c["label"]), None)
|
| 28 |
+
if act is None:
|
| 29 |
+
continue
|
| 30 |
+
b.act(act, page); time.sleep(0.3)
|
| 31 |
+
dest = b.observe(screenshot=False)
|
| 32 |
+
finally:
|
| 33 |
+
b.close()
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print("fail", type(e).__name__, str(e)[:60], flush=True); continue
|
| 36 |
+
if dest["url"] == page["url"] and dest["title"] == page["title"]:
|
| 37 |
+
continue
|
| 38 |
+
hist = [{"action": act["label"], "kind": "click", "text": None, "page_changed": True}]
|
| 39 |
+
f.write(json.dumps({"page": c["page"], "url": dest["url"], "title": dest["title"], "goal": c["goal"], "gold_op": "DONE",
|
| 40 |
+
"gold_id": "DONE", "kind": "done", "label": act["label"], "history": hist,
|
| 41 |
+
"page_obj": {k: dest[k] for k in ("url", "title", "text", "actions")}}, ensure_ascii=False) + "\n"); f.flush()
|
| 42 |
+
n_ok += 1
|
| 43 |
+
if n_ok % 25 == 0: print(f" {n_ok} done cases from {n_try} tries", flush=True)
|
| 44 |
+
print("wrote", n_ok, "DONE cases")
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
code/finetune/rollouts.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Scripted multi-step trajectories executed in the real browser, for the three skills the model lacks:
|
| 2 |
+
scroll : goal targets an element that is only visible after scrolling -> [SCROLL_DOWN, CLICK, DONE]
|
| 3 |
+
search : goal asks to search for a phrase in a text field -> [TYPE_TEXT, CLICK submit/suggestion, DONE]
|
| 4 |
+
select : goal asks to choose an option of a <select> -> [SELECT, DONE]
|
| 5 |
+
|
| 6 |
+
python finetune/rollouts.py out/pages.jsonl out/rollout_cases.jsonl [max_pages=300]
|
| 7 |
+
|
| 8 |
+
Goals are templated (varied phrasing); no LLM needed. Pages are the crawled ones; the suite's exact URLs are skipped.
|
| 9 |
+
"""
|
| 10 |
+
import json, os, random, re, sys, time
|
| 11 |
+
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
|
| 12 |
+
os.environ.setdefault("BU_CDP_URL", "http://127.0.0.1:9222")
|
| 13 |
+
from jev_ultrafast.browser import Browser, StalePage
|
| 14 |
+
|
| 15 |
+
SUITE_URLS = {"https://en.wikipedia.org/wiki/Main_Page", "https://news.ycombinator.com/", "https://github.com/tile-ai/tilelang", "https://www.python.org/",
|
| 16 |
+
"https://books.toscrape.com/", "https://the-internet.herokuapp.com/dropdown", "https://the-internet.herokuapp.com/checkboxes",
|
| 17 |
+
"https://quotes.toscrape.com/", "https://duckduckgo.com/", "https://arxiv.org/", "https://www.google.com/travel/flights?hl=en"}
|
| 18 |
+
SCROLL_T = ["Open '{x}'.", "I want to see '{x}', take me there.", "Go to {x}.", "Find and click '{x}' on this page.", "Open the '{x}' link further down the page.", "Navigate to '{x}'."]
|
| 19 |
+
SEARCH_T = ["Search for '{q}'.", "Look up '{q}' using the search box and show the results.", "Find results for '{q}'.", "Use the site search to find '{q}'.", "Search this site for {q}."]
|
| 20 |
+
SELECT_T = ["Select '{o}' in the '{f}' dropdown.", "Choose {o} for {f}.", "Set '{f}' to '{o}'.", "Pick the option '{o}'."]
|
| 21 |
+
QUERIES = ["python tutorial", "flash attention", "climate change", "rust async", "linear algebra", "tilelang", "modernbert", "black holes", "sourdough bread", "gpu kernels"]
|
| 22 |
+
|
| 23 |
+
def lab(a): return a["label"].split(" → ")[0].strip()
|
| 24 |
+
def case(page, goal, gold_op, gold_id, kind, label, hist, tag):
|
| 25 |
+
return {"page": -1, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": gold_op, "gold_id": gold_id, "kind": kind, "label": label,
|
| 26 |
+
"history": hist, "source": "rollout", "skill": tag, "page_obj": {k: page[k] for k in ("url", "title", "text", "actions")}}
|
| 27 |
+
def hist_of(a, text=None, changed=True): return {"action": a["label"], "kind": a["kind"], "text": text, "page_changed": changed}
|
| 28 |
+
def find(page, pred):
|
| 29 |
+
return next((a for a in page["actions"] if pred(a)), None)
|
| 30 |
+
|
| 31 |
+
def do_scroll(b, page, rng, out):
|
| 32 |
+
if not page.get("scroll") or page["scroll"]["height"] < 1300: return 0
|
| 33 |
+
before = {a["label"] for a in page["actions"] if a["kind"] == "click"}
|
| 34 |
+
sd = find(page, lambda a: a["id"] == "scroll_down")
|
| 35 |
+
if not sd: return 0
|
| 36 |
+
b.act(sd, page); time.sleep(0.3); p2 = b.observe(screenshot=False)
|
| 37 |
+
new = [a for a in p2["actions"] if a["kind"] == "click" and a["label"] not in before and 3 <= len(lab(a)) <= 60 and a.get("role") in ("link", "button")]
|
| 38 |
+
if not new: return 0
|
| 39 |
+
n = 0
|
| 40 |
+
for tgt in rng.sample(new, min(2, len(new))):
|
| 41 |
+
goal = rng.choice(SCROLL_T).format(x=lab(tgt))
|
| 42 |
+
out.write(json.dumps(case(page, goal, "SCROLL_DOWN", "scroll_down", "scroll", "Scroll down", [], "scroll"), ensure_ascii=False) + "\n")
|
| 43 |
+
out.write(json.dumps(case(p2, goal, "CLICK", tgt["id"], "click", tgt["label"], [hist_of(sd, changed=False)], "scroll"), ensure_ascii=False) + "\n")
|
| 44 |
+
n += 2
|
| 45 |
+
# execute one click for a DONE state
|
| 46 |
+
tgt = new[0]
|
| 47 |
+
try:
|
| 48 |
+
b.act(tgt, p2); time.sleep(0.4); p3 = b.observe(screenshot=False)
|
| 49 |
+
if p3["url"] != p2["url"]:
|
| 50 |
+
goal = rng.choice(SCROLL_T).format(x=lab(tgt))
|
| 51 |
+
out.write(json.dumps(case(p3, goal, "DONE", "DONE", "done", "", [hist_of(sd, changed=False), hist_of(tgt)], "scroll"), ensure_ascii=False) + "\n"); n += 1
|
| 52 |
+
except Exception: pass
|
| 53 |
+
return n
|
| 54 |
+
|
| 55 |
+
def do_search(b, page, rng, out):
|
| 56 |
+
field = find(page, lambda a: a["kind"] == "fill" and re.search(r"search|query|find|keyword", a["label"], re.I))
|
| 57 |
+
if not field: return 0
|
| 58 |
+
q = rng.choice(QUERIES); goal = rng.choice(SEARCH_T).format(q=q)
|
| 59 |
+
out.write(json.dumps(case(page, goal, "TYPE_TEXT", field["id"], "fill", field["label"], [], "search"), ensure_ascii=False) + "\n")
|
| 60 |
+
b.act(field, page, text=q); time.sleep(0.4); p2 = b.observe(screenshot=False)
|
| 61 |
+
# submit: a search/go button, or a suggestion containing the query
|
| 62 |
+
sub = find(p2, lambda a: a["kind"] == "click" and (re.search(r"^(search|go|submit|find)\b", lab(a), re.I) or (a.get("role") in ("option", "listitem", "link") and q.split()[0].lower() in a["label"].lower())))
|
| 63 |
+
if not sub: return 1
|
| 64 |
+
out.write(json.dumps(case(p2, goal, "CLICK", sub["id"], "click", sub["label"], [hist_of(field, q, False)], "search"), ensure_ascii=False) + "\n")
|
| 65 |
+
try:
|
| 66 |
+
b.act(sub, p2); time.sleep(0.6); p3 = b.observe(screenshot=False)
|
| 67 |
+
if p3["url"] != p2["url"] or p3["title"] != p2["title"]:
|
| 68 |
+
out.write(json.dumps(case(p3, goal, "DONE", "DONE", "done", "", [hist_of(field, q, False), hist_of(sub)], "search"), ensure_ascii=False) + "\n"); return 3
|
| 69 |
+
except Exception: pass
|
| 70 |
+
return 2
|
| 71 |
+
|
| 72 |
+
def do_select(b, page, rng, out):
|
| 73 |
+
sels = [a for a in page["actions"] if a["kind"] == "select" and a.get("value") and a["value"] != a.get("current_value")]
|
| 74 |
+
if not sels: return 0
|
| 75 |
+
by_field = {}
|
| 76 |
+
for a in sels: by_field.setdefault(a["node"], []).append(a)
|
| 77 |
+
node, opts = rng.choice(list(by_field.items()))
|
| 78 |
+
opt = rng.choice(opts); f = lab(opt); o = opt["label"].split(" → ")[-1].strip()
|
| 79 |
+
goal = rng.choice(SELECT_T).format(o=o, f=f)
|
| 80 |
+
out.write(json.dumps(case(page, goal, "SELECT", opt["id"], "select", opt["label"], [], "select"), ensure_ascii=False) + "\n")
|
| 81 |
+
try:
|
| 82 |
+
b.act(opt, page); time.sleep(0.3); p2 = b.observe(screenshot=False)
|
| 83 |
+
out.write(json.dumps(case(p2, goal, "DONE", "DONE", "done", "", [hist_of(opt, changed=False)], "select"), ensure_ascii=False) + "\n"); return 2
|
| 84 |
+
except Exception: return 1
|
| 85 |
+
|
| 86 |
+
def main():
|
| 87 |
+
pages = [json.loads(l) for l in open(sys.argv[1])]; out_f = sys.argv[2]; mx = int(sys.argv[3]) if len(sys.argv) > 3 else 300
|
| 88 |
+
rng = random.Random(11); rng.shuffle(pages)
|
| 89 |
+
counts = {"scroll": 0, "search": 0, "select": 0}; done = 0
|
| 90 |
+
with open(out_f, "w") as out:
|
| 91 |
+
for pg in pages:
|
| 92 |
+
if done >= mx: break
|
| 93 |
+
if pg["url"] in SUITE_URLS: continue
|
| 94 |
+
for skill, fn in (("scroll", do_scroll), ("search", do_search), ("select", do_select)):
|
| 95 |
+
try:
|
| 96 |
+
b = Browser(pg["url"])
|
| 97 |
+
try:
|
| 98 |
+
page = b.observe(screenshot=False); n = fn(b, page, rng, out)
|
| 99 |
+
finally:
|
| 100 |
+
b.close()
|
| 101 |
+
counts[skill] += n
|
| 102 |
+
except Exception as e:
|
| 103 |
+
pass
|
| 104 |
+
done += 1
|
| 105 |
+
if done % 25 == 0: print(f" {done} pages {counts}", flush=True)
|
| 106 |
+
print("wrote", counts, "->", out_f)
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
main()
|
code/finetune/run_after_v6.sh
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# after v6: (1) real-task suite with v6, (2) training-speed benchmark of the three cheap wins, (3) v7 = v6 data with
|
| 3 |
+
# truncated page text + no checkpointing + torch.compile, 3 epochs, calibrate, eval, suite.
|
| 4 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 5 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 6 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 7 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 8 |
+
P=.venv/bin/python
|
| 9 |
+
until grep -qE "V6_DONE|Traceback" finetune/out/v6.log; do sleep 30; done
|
| 10 |
+
echo "== suite v6"
|
| 11 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 12 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v6 999 >/dev/null
|
| 13 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/finetune/out/suite_v6.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 14 |
+
bash $S/stop_all.sh >/dev/null 2>&1; sleep 3
|
| 15 |
+
echo "== speed benchmark (256 items, 1 epoch)"
|
| 16 |
+
echo "-- old items (1016 tok), CKPT=1 COMPILE=0"; CKPT=1 COMPILE=0 LIMIT=256 $P finetune/train.py finetune/out/train_items.pt /tmp/bench_ck 1 2>&1 | grep "=== epoch"
|
| 17 |
+
echo "-- old items, CKPT=0 COMPILE=0"; CKPT=0 COMPILE=0 LIMIT=256 $P finetune/train.py finetune/out/train_items.pt /tmp/bench_ck 1 2>&1 | grep -E "=== epoch|OutOfMemory"
|
| 18 |
+
echo "-- old items, CKPT=0 COMPILE=1"; CKPT=0 COMPILE=1 LIMIT=256 $P finetune/train.py finetune/out/train_items.pt /tmp/bench_ck 1 2>&1 | grep -E "=== epoch|OutOfMemory|Error"
|
| 19 |
+
echo "== rebuild items with 3000-char page text"
|
| 20 |
+
$P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/step2_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 21 |
+
echo "-- new items, CKPT=0 COMPILE=1"; CKPT=0 COMPILE=1 LIMIT=256 $P finetune/train.py finetune/out/train_items.pt /tmp/bench_ck 1 2>&1 | grep -E "=== epoch|OutOfMemory|Error"
|
| 22 |
+
echo "== train v7 (3 epochs, fast settings)"; CKPT=0 COMPILE=1 $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v7 3 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 23 |
+
echo "== calibrate v7"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v7 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 24 |
+
echo "== eval v7"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v7 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 25 |
+
echo "== suite v7"
|
| 26 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 27 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v7 999 >/dev/null
|
| 28 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/finetune/out/suite_v7.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 29 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 30 |
+
echo AFTER_V6_DONE
|
code/finetune/run_all.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# build items -> zero-shot baselines -> fine-tune -> eval. Logs to finetune/out/run.log
|
| 3 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 4 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server"); do kill $p; done; sleep 3
|
| 7 |
+
uv pip install --python $P "httpx[http2]" 2>&1 | tail -1
|
| 8 |
+
set -e
|
| 9 |
+
echo "== build items"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/
|
| 10 |
+
echo "== zero-shot baselines"
|
| 11 |
+
$P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl "$LAYA_BASE" 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 12 |
+
$P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl convaiinnovations/laya multilingual 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
echo "== train"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser 4 2>&1 | grep -vE "Warn|warn"
|
| 14 |
+
echo "== eval fine-tuned"
|
| 15 |
+
$P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl finetune/out/laya-browser 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 16 |
+
echo RUN_ALL_DONE
|
code/finetune/run_final.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Final comparison after v10s: v10 vs v10s, gating off vs tau=0.7, 3 runs per task; plus per-step latency profiles.
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
O=finetune/out; P=.venv/bin/python
|
| 6 |
+
until grep -q "V10S_DONE" $O/v10s.log; do sleep 60; done
|
| 7 |
+
echo "== per-step latency"
|
| 8 |
+
for ck in laya-browser-v10 laya-browser-v10s; do echo "-- $ck"; $P apps/profile_step.py $PWD/$O/$ck 2>&1 | grep -vE "TileLang|Warn|warn|Fetch|loaded"; done
|
| 9 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 10 |
+
for ck in laya-browser-v10 laya-browser-v10s; do
|
| 11 |
+
for tau in 0 0.7; do
|
| 12 |
+
echo "== suite $ck tau=$tau x3"
|
| 13 |
+
export ESCALATE_TAU=$tau ESCALATE_LOG=$PWD/$O/escalations_final_${ck}_$tau.jsonl; rm -f $ESCALATE_LOG
|
| 14 |
+
bash $S/restart_s1.sh $PWD/$O/$ck 999 >/dev/null
|
| 15 |
+
(cd ../jev-ultrafast && REPEATS=3 SUITE_OUT=$PWD/../laya/$O/suite_final_${ck}_$tau.json timeout 3000 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -E "^==|per task")
|
| 16 |
+
curl -s http://127.0.0.1:8791/ | python3 -c "import json,sys; d=json.load(sys.stdin); print(f\" server calls={d['calls']} escalated={d['escalated']} ({100*d['escalated']/max(1,d['calls']):.0f}%)\")"
|
| 17 |
+
done
|
| 18 |
+
done
|
| 19 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 20 |
+
echo FINAL_DONE
|
code/finetune/run_gated.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# after v10: real-task suite with System-1/System-2 gating at several confidence thresholds; escalations logged as DAgger cases
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
O=finetune/out
|
| 6 |
+
until grep -q "V10_DONE" $O/v10.log; do sleep 60; done
|
| 7 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 8 |
+
for tau in 0.5 0.7 0.9; do
|
| 9 |
+
echo "== suite v10 + gating tau=$tau"
|
| 10 |
+
export ESCALATE_TAU=$tau ESCALATE_LOG=$PWD/$O/escalations_tau$tau.jsonl
|
| 11 |
+
rm -f $ESCALATE_LOG
|
| 12 |
+
bash $S/restart_s1.sh $PWD/$O/laya-browser-v10 999 >/dev/null
|
| 13 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/$O/suite_v10_tau$tau.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 14 |
+
curl -s http://127.0.0.1:8791/ | python3 -c "import json,sys; d=json.load(sys.stdin); print(f\" server calls={d['calls']} escalated={d['escalated']} ({100*d['escalated']/max(1,d['calls']):.0f}%)\")"
|
| 15 |
+
done
|
| 16 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 17 |
+
echo GATED_DONE
|
code/finetune/run_train.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
echo "== train"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser 4 2>&1 | grep -vE "Warn|warn"
|
| 7 |
+
echo "== eval fine-tuned"
|
| 8 |
+
$P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 9 |
+
echo TRAIN_DONE
|
code/finetune/run_v10.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# v10 = v9 recipe (format v2, head 768, 4 epochs) + the live data that failed silently in v9: goals on 421 pages,
|
| 3 |
+
# real DONE, step-2, DAgger with policy v9. Full logs kept per step under finetune/out/v10_*.log
|
| 4 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 5 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 6 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 7 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CKPT=0 COMPILE=0
|
| 8 |
+
P=.venv/bin/python; O=finetune/out
|
| 9 |
+
bash $S/start_infra.sh
|
| 10 |
+
for i in 1 2 3 4 5 6; do
|
| 11 |
+
r=$(curl -s -m 60 http://127.0.0.1:30000/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"Qwen/Qwen3-8B-AWQ","max_tokens":20,"chat_template_kwargs":{"enable_thinking":false},"messages":[{"role":"user","content":"say ok"}]}' | head -c 300)
|
| 12 |
+
echo "sglang probe: $r" | cut -c1-120; echo "$r" | grep -q '"content"' && break; sleep 20
|
| 13 |
+
done
|
| 14 |
+
echo "== goals (421 pages)"; (cd ../jev-ultrafast && timeout 5400 .venv/bin/python ../laya/finetune/gen_goals.py ../laya/finetune/out/pages.jsonl ../laya/finetune/out/cases.jsonl 12 > ../laya/$O/v10_goals.log 2>&1); tail -3 $O/v10_goals.log; echo "cases: $(wc -l < $O/cases.jsonl)"
|
| 15 |
+
echo "== real DONE"; (cd ../jev-ultrafast && timeout 5400 .venv/bin/python ../laya/finetune/make_done_cases.py ../laya/finetune/out/pages.jsonl ../laya/finetune/out/cases.jsonl ../laya/finetune/out/done_cases.jsonl 700 > ../laya/$O/v10_done.log 2>&1); tail -1 $O/v10_done.log
|
| 16 |
+
echo "== step-2"; (cd ../jev-ultrafast && timeout 3600 .venv/bin/python ../laya/finetune/gen_step2.py ../laya/finetune/out/done_cases.jsonl ../laya/finetune/out/step2_cases.jsonl 2 > ../laya/$O/v10_step2.log 2>&1); tail -1 $O/v10_step2.log
|
| 17 |
+
echo "== dagger (policy v9)"
|
| 18 |
+
bash $S/restart_s1.sh $PWD/$O/laya-browser-v9 999 >/dev/null
|
| 19 |
+
(cd ../jev-ultrafast && timeout 3000 .venv/bin/python ../laya/finetune/dagger.py ../laya/finetune/out/dagger_cases.jsonl > ../laya/$O/v10_dagger.log 2>&1); tail -1 $O/v10_dagger.log
|
| 20 |
+
bash $S/stop_all.sh >/dev/null 2>&1; sleep 3
|
| 21 |
+
export LAYA_FMT=v2 LAYA_HEAD=768
|
| 22 |
+
echo "== build v10"; $P finetune/build_items.py $O/pages.jsonl $O/cases.jsonl $O/ $O/done_cases.jsonl $O/step2_cases.jsonl $O/m2w_cases.jsonl $O/dagger_cases.jsonl
|
| 23 |
+
echo "== train v10 (4 epochs)"; $P finetune/train.py $O/train_items.pt $O/laya-browser-v10 4 2>&1 | grep --line-buffered -E "=== epoch|saved|Error|Traceback"
|
| 24 |
+
echo "== calibrate"; $P finetune/calibrate.py $O/pages.jsonl $O/eval_cases.jsonl $PWD/$O/laya-browser-v10 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 25 |
+
echo "== eval v10"; $P finetune/eval.py $O/pages.jsonl $O/eval_cases.jsonl $PWD/$O/laya-browser-v10 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 26 |
+
echo "== eval v9 (same eval)"; $P finetune/eval.py $O/pages.jsonl $O/eval_cases.jsonl $PWD/$O/laya-browser-v9 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 27 |
+
echo "== suite v10"
|
| 28 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 29 |
+
bash $S/restart_s1.sh $PWD/$O/laya-browser-v10 999 >/dev/null
|
| 30 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/$O/suite_v10.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 31 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 32 |
+
echo V10_DONE
|
code/finetune/run_v10s.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# v10s: same data as v10, 322M mmBERT-base (multilingual) backbone, format v3 -> target ~20 ms per browser step.
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
until grep -q "GATED_DONE" finetune/out/gated.log; do sleep 60; done
|
| 6 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/multilingual | head -1)
|
| 7 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CKPT=0 COMPILE=0 LAYA_FMT=v3 LAYA_HEAD=768
|
| 8 |
+
P=.venv/bin/python; O=finetune/out
|
| 9 |
+
echo "== build v10s (format v3, mmBERT-base tokenizer)"
|
| 10 |
+
$P finetune/build_items.py $O/pages.jsonl $O/cases.jsonl $O/ $O/done_cases.jsonl $O/step2_cases.jsonl $O/m2w_cases.jsonl $O/dagger_cases.jsonl
|
| 11 |
+
echo "== train v10s (4 epochs)"; $P finetune/train.py $O/train_items.pt $O/laya-browser-v10s 4 2>&1 | grep --line-buffered -E "=== epoch|saved|Error|Traceback"
|
| 12 |
+
echo "== calibrate"; $P finetune/calibrate.py $O/pages.jsonl $O/eval_cases.jsonl $PWD/$O/laya-browser-v10s 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
echo "== eval v10s"; $P finetune/eval.py $O/pages.jsonl $O/eval_cases.jsonl $PWD/$O/laya-browser-v10s 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 14 |
+
echo "== profile step"; $P apps/profile_step.py $PWD/$O/laya-browser-v10s 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 15 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 16 |
+
for tau in 0 0.7; do
|
| 17 |
+
echo "== suite v10s tau=$tau"
|
| 18 |
+
export ESCALATE_TAU=$tau ESCALATE_LOG=$PWD/$O/escalations_v10s_tau$tau.jsonl; rm -f $ESCALATE_LOG
|
| 19 |
+
bash $S/restart_s1.sh $PWD/$O/laya-browser-v10s 999 >/dev/null
|
| 20 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/$O/suite_v10s_tau$tau.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 21 |
+
curl -s http://127.0.0.1:8791/ | python3 -c "import json,sys; d=json.load(sys.stdin); print(f\" server calls={d['calls']} escalated={d['escalated']} ({100*d['escalated']/max(1,d['calls']):.0f}%)\")"
|
| 22 |
+
done
|
| 23 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 24 |
+
echo V10S_DONE
|
code/finetune/run_v2.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server|apps/systemone_server"); do kill $p; done; sleep 3
|
| 7 |
+
echo "== build"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl
|
| 8 |
+
echo "== zero-shot baseline (typed) on v2 eval"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl "$LAYA_BASE" 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 9 |
+
echo "== train v2"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v2 4 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 10 |
+
echo "== eval v2"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v2 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 11 |
+
echo V2_DONE
|
code/finetune/run_v3.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server|apps/systemone_server"); do kill $p; done; sleep 3
|
| 7 |
+
echo "== build"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 8 |
+
echo "== zero-shot baseline (typed) on v2 eval"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl "$LAYA_BASE" 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 9 |
+
echo "== train v3"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v3 4 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 10 |
+
echo "== eval v3"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v3 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 11 |
+
echo V3_DONE
|
code/finetune/run_v4.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server|apps/systemone_server"); do kill $p; done; sleep 3
|
| 7 |
+
echo "== wait for shards"; until [ "$(ls finetune/data/mind2web/data/train/train_*.json 2>/dev/null | wc -l)" -ge 11 ] && ! ls finetune/data/mind2web/data/train/*.incomplete >/dev/null 2>&1; do sleep 10; done
|
| 8 |
+
echo "== convert all shards"; $P finetune/convert_mind2web.py finetune/data/mind2web/data/train/train_*.json finetune/out/m2w_cases.jsonl 2>&1 | tail -1
|
| 9 |
+
echo "== build"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 10 |
+
echo "== train v4 (3 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v4 3 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 11 |
+
echo "== eval v4"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v4 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 12 |
+
echo "== eval v3 on same eval set"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v3 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
echo V4_DONE
|
code/finetune/run_v5.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server|apps/systemone_server"); do kill $p; done; sleep 3
|
| 7 |
+
|
| 8 |
+
echo "== convert all shards"; $P finetune/convert_mind2web.py finetune/data/mind2web/data/train/train_*.json finetune/out/m2w_cases.jsonl 2>&1 | tail -1
|
| 9 |
+
echo "== build"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 10 |
+
echo "== train v5 (3 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v5 3 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 11 |
+
echo "== calibrate"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v5 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 12 |
+
echo "== eval v5"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v5 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
|
| 14 |
+
echo V5_DONE
|
code/finetune/run_v6.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 3 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 4 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 5 |
+
P=.venv/bin/python
|
| 6 |
+
for p in $(pgrep -f "sglang.launch_server|apps/systemone_server"); do kill $p; done; sleep 3
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
echo "== build"; $P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/step2_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 10 |
+
echo "== train v6 (3 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v6 3 2>&1 | grep -E "=== epoch|saved|Error|Traceback"
|
| 11 |
+
echo "== calibrate"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v6 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 12 |
+
echo "== eval v6"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v6 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
|
| 14 |
+
echo V6_DONE
|
code/finetune/run_v7.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# v7: best training config (no checkpointing, no compile, 3000-char page text) + DONE oversampling x4.
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 6 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CKPT=0 COMPILE=0
|
| 7 |
+
P=.venv/bin/python
|
| 8 |
+
echo "== build (3000-char text, DONE x4)"
|
| 9 |
+
$P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/step2_cases.jsonl finetune/out/m2w_cases.jsonl
|
| 10 |
+
echo "== speed: new items, CKPT=0 COMPILE=0 (256 items)"; LIMIT=256 $P finetune/train.py finetune/out/train_items.pt /tmp/bench_ck 1 2>&1 | grep -E "=== epoch|Error"
|
| 11 |
+
echo "== train v7 (3 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v7 3 2>&1 | grep --line-buffered -E "=== epoch|saved|Error|Traceback"
|
| 12 |
+
echo "== calibrate v7"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v7 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
echo "== eval v7"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v7 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 14 |
+
echo "== eval v6 on same (truncated) eval"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v6 2>&1 | grep -vE "TileLang|Warn|warn|Fetch" | head -1
|
| 15 |
+
echo "== suite v7"
|
| 16 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 17 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v7 999 >/dev/null
|
| 18 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/finetune/out/suite_v7.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 19 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 20 |
+
echo V7_DONE
|
code/finetune/run_v8.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# v8: after v7 -> DAgger harvest on real tasks with v7 as the policy and Qwen as teacher -> retrain -> eval -> suite.
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 6 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CKPT=0 COMPILE=0
|
| 7 |
+
P=.venv/bin/python
|
| 8 |
+
echo "== build v8"
|
| 9 |
+
$P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/step2_cases.jsonl finetune/out/m2w_cases.jsonl finetune/out/dagger_cases.jsonl
|
| 10 |
+
echo "== train v8 (3 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v8 3 2>&1 | grep --line-buffered -E "=== epoch|saved|Error|Traceback"
|
| 11 |
+
echo "== calibrate v8"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v8 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 12 |
+
echo "== eval v8"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v8 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 13 |
+
echo "== suite v8"
|
| 14 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 15 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v8 999 >/dev/null
|
| 16 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/finetune/out/suite_v8.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 17 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 18 |
+
echo V8_DONE
|
code/finetune/run_v9.sh
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# v9 "one shot": 420 pages -> goals -> real DONE -> step-2 -> DAgger (policy v8) -> format v2 (head 768) -> 4 epochs -> eval -> suite
|
| 3 |
+
S=/tmp/claude-1000/-home-ckl-projects-S/8a7ce50a-5640-4606-837a-93df5ff48c83/scratchpad
|
| 4 |
+
cd /home/ckl/projects/S/laya && source env.sh
|
| 5 |
+
export LAYA_BASE=$(ls -d ~/.cache/huggingface/hub/models--convaiinnovations--laya/snapshots/*/typed-decisions | head -1)
|
| 6 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CKPT=0 COMPILE=0
|
| 7 |
+
P=.venv/bin/python; J=../jev-ultrafast/.venv/bin/python
|
| 8 |
+
until grep -q "V8_DONE" finetune/out/v8.log && grep -q "^done" finetune/out/collect2.log; do sleep 30; done
|
| 9 |
+
echo "== pages: $(wc -l < finetune/out/pages.jsonl)"
|
| 10 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 11 |
+
echo "== goals for all pages"; (cd ../jev-ultrafast && timeout 3600 .venv/bin/python ../laya/finetune/gen_goals.py ../laya/finetune/out/pages.jsonl ../laya/finetune/out/cases.jsonl 12 2>&1 | tail -2)
|
| 12 |
+
echo "== real DONE cases"; (cd ../jev-ultrafast && timeout 3600 .venv/bin/python ../laya/finetune/make_done_cases.py ../laya/finetune/out/pages.jsonl ../laya/finetune/out/cases.jsonl ../laya/finetune/out/done_cases.jsonl 700 2>&1 | tail -1)
|
| 13 |
+
echo "== step-2 negatives"; (cd ../jev-ultrafast && timeout 1800 .venv/bin/python ../laya/finetune/gen_step2.py ../laya/finetune/out/done_cases.jsonl ../laya/finetune/out/step2_cases.jsonl 2 2>&1 | tail -1)
|
| 14 |
+
echo "== dagger round 2 (policy v8)"
|
| 15 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v8 999 >/dev/null
|
| 16 |
+
(cd ../jev-ultrafast && timeout 3000 .venv/bin/python ../laya/finetune/dagger.py ../laya/finetune/out/dagger_cases.jsonl 2>&1 | tail -1)
|
| 17 |
+
bash $S/stop_all.sh >/dev/null 2>&1; sleep 3
|
| 18 |
+
export LAYA_FMT=v2 LAYA_HEAD=768
|
| 19 |
+
echo "== build v9 (format v2, head 768)"
|
| 20 |
+
$P finetune/build_items.py finetune/out/pages.jsonl finetune/out/cases.jsonl finetune/out/ finetune/out/done_cases.jsonl finetune/out/step2_cases.jsonl finetune/out/m2w_cases.jsonl finetune/out/dagger_cases.jsonl
|
| 21 |
+
echo "== train v9 (4 epochs)"; $P finetune/train.py finetune/out/train_items.pt finetune/out/laya-browser-v9 4 2>&1 | grep --line-buffered -E "=== epoch|saved|Error|Traceback"
|
| 22 |
+
echo "== calibrate v9"; $P finetune/calibrate.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v9 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 23 |
+
echo "== eval v9"; $P finetune/eval.py finetune/out/pages.jsonl finetune/out/eval_cases.jsonl $PWD/finetune/out/laya-browser-v9 2>&1 | grep -vE "TileLang|Warn|warn|Fetch"
|
| 24 |
+
echo "== suite v9"
|
| 25 |
+
bash $S/start_infra.sh >/dev/null 2>&1
|
| 26 |
+
bash $S/restart_s1.sh $PWD/finetune/out/laya-browser-v9 999 >/dev/null
|
| 27 |
+
(cd ../jev-ultrafast && SUITE_OUT=$PWD/../laya/finetune/out/suite_v9.json timeout 1500 .venv/bin/python ../laya/apps/browser_suite.py 2>&1 | grep -vE "Warn|TileLang")
|
| 28 |
+
bash $S/stop_all.sh >/dev/null 2>&1
|
| 29 |
+
echo V9_DONE
|
code/finetune/train.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single-GPU RLCD fine-tune (laya's recipe: GRPO-style noisy-logit policy gradient + soft CE), from a laya checkpoint.
|
| 2 |
+
|
| 3 |
+
LAYA_BASE=<checkpoint dir> python finetune/train.py out/train_items.pt out/laya-browser [epochs=4]
|
| 4 |
+
"""
|
| 5 |
+
import json, os, random, sys, time
|
| 6 |
+
import torch
|
| 7 |
+
sys.path.insert(0, "/home/ckl/projects/S/laya-upstream")
|
| 8 |
+
from safetensors.torch import load_file, save_file
|
| 9 |
+
from transformers import AutoTokenizer
|
| 10 |
+
from laya.common import build_model, proper_reward
|
| 11 |
+
|
| 12 |
+
def collate(items, pad_id):
|
| 13 |
+
n, L = len(items), max(len(it["ids"]) for it in items); kmax = max(len(it["markers"]) for it in items)
|
| 14 |
+
ids = torch.full((n, L), pad_id, dtype=torch.long); att = torch.zeros((n, L), dtype=torch.long)
|
| 15 |
+
mpos = torch.zeros((n, kmax), dtype=torch.long); mmask = torch.zeros((n, kmax), dtype=torch.bool); target = torch.zeros((n, kmax))
|
| 16 |
+
for i, it in enumerate(items):
|
| 17 |
+
ids[i, :len(it["ids"])] = torch.tensor(it["ids"]); att[i, :len(it["ids"])] = 1; k = len(it["markers"])
|
| 18 |
+
mpos[i, :k] = torch.tensor(it["markers"]); mmask[i, :k] = True; target[i, :len(it["target"])] = torch.tensor(it["target"])
|
| 19 |
+
return dict(input_ids=ids, attention_mask=att, marker_pos=mpos, marker_mask=mmask, target=target, qtype=torch.tensor([it["qtype"] for it in items]))
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
items_f, out, epochs = sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
| 23 |
+
limit = int(os.environ.get("LIMIT", "0")) # LIMIT=N: time N items only (benchmarking)
|
| 24 |
+
base = os.environ["LAYA_BASE"]; dev = torch.device("cuda")
|
| 25 |
+
cfg = json.load(open(os.path.join(base, "rl_agent_config.json")))
|
| 26 |
+
cfg.update(max_len=1024, head_max_len=int(os.environ.get('LAYA_HEAD', '512')))
|
| 27 |
+
tok = AutoTokenizer.from_pretrained(os.path.join(base, "tokenizer"))
|
| 28 |
+
model = build_model(cfg, encoder_dir=os.path.join(base, "encoder"))
|
| 29 |
+
model.load_state_dict(load_file(os.path.join(base, "model.safetensors")), strict=True)
|
| 30 |
+
model.encoder.config.reference_compile = False
|
| 31 |
+
if os.environ.get("CKPT", "0") == "1": # gradient checkpointing costs ~30%; 16 GB fits micro=4 x 1024 without it
|
| 32 |
+
model.encoder.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
| 33 |
+
model.to(dev).train()
|
| 34 |
+
if os.environ.get("COMPILE", "1") == "1":
|
| 35 |
+
model.encoder = torch.compile(model.encoder, dynamic=True)
|
| 36 |
+
items = torch.load(items_f, weights_only=False)
|
| 37 |
+
if limit: items = items[:limit]
|
| 38 |
+
MICRO, ACCUM, G, LR_ENC, LR_HEAD, S0, S1 = 4, 8, 4, 2.5e-5, 1e-4, 0.4, 0.1
|
| 39 |
+
enc = [p for n, p in model.named_parameters() if "encoder." in n]; head = [p for n, p in model.named_parameters() if "encoder." not in n]
|
| 40 |
+
opt = torch.optim.AdamW([{"params": enc, "lr": LR_ENC}, {"params": head, "lr": LR_HEAD}], weight_decay=0.01)
|
| 41 |
+
total = max(1, (len(items) // (MICRO * ACCUM)) * epochs)
|
| 42 |
+
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=total, eta_min=1e-6)
|
| 43 |
+
t0 = time.time(); step = 0
|
| 44 |
+
for ep in range(epochs):
|
| 45 |
+
random.seed(42 + ep); random.shuffle(items)
|
| 46 |
+
sigma = S0 + (S1 - S0) * ep / max(1, epochs - 1); tot_loss, nb = 0.0, 0; opt.zero_grad(set_to_none=True)
|
| 47 |
+
for b in range(0, len(items), MICRO):
|
| 48 |
+
batch = collate(items[b:b + MICRO], tok.pad_token_id); batch = {k: v.to(dev) for k, v in batch.items()}
|
| 49 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 50 |
+
logits, act = model(batch["input_ids"], batch["attention_mask"], batch["marker_pos"], batch["marker_mask"], batch["qtype"])
|
| 51 |
+
logits = logits.float(); mask = batch["marker_mask"]; k = mask.sum(-1, keepdim=True).float(); target = batch["target"]
|
| 52 |
+
eps = torch.randn((G,) + logits.shape, device=dev) * sigma * mask; eps = (eps - eps.sum(-1, keepdim=True) / k) * mask
|
| 53 |
+
z = logits.detach().unsqueeze(0) + eps; q = torch.softmax(z.masked_fill(~mask, -1e4), -1)
|
| 54 |
+
with torch.no_grad():
|
| 55 |
+
r = proper_reward(q, target.unsqueeze(0), batch["qtype"], mask, w_sph=0.75, w_rps=1.0); adv = r - r.mean(0, keepdim=True); adv = adv / (adv.std() + 1e-6)
|
| 56 |
+
logp = -(((z - logits.unsqueeze(0)) ** 2) * mask).sum(-1) / (2 * sigma ** 2)
|
| 57 |
+
loss_rl = -(adv * logp).mean(); loss_ce = -(target * torch.log_softmax(logits.masked_fill(~mask, -1e4), -1)).sum(-1).mean()
|
| 58 |
+
loss = (loss_rl + loss_ce) / ACCUM + 0.0 * act.sum(); loss.backward(); nb += 1; tot_loss += loss.item() * ACCUM
|
| 59 |
+
if nb % ACCUM == 0 or b + MICRO >= len(items):
|
| 60 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step(); sched.step(); opt.zero_grad(set_to_none=True); step += 1
|
| 61 |
+
if step % 20 == 0: print(f" ep {ep+1} step {step}/{total} loss {loss.item()*ACCUM:.3f} ce {loss_ce.item():.3f} reward {r.mean().item():.3f} {time.time()-t0:.0f}s", flush=True)
|
| 62 |
+
print(f"=== epoch {ep+1}/{epochs} avg loss {tot_loss/max(1,nb):.4f} {time.time()-t0:.0f}s", flush=True)
|
| 63 |
+
os.makedirs(out, exist_ok=True); model.eval()
|
| 64 |
+
if hasattr(model.encoder, "_orig_mod"):
|
| 65 |
+
model.encoder = model.encoder._orig_mod
|
| 66 |
+
save_file({k: v.half().contiguous().cpu() for k, v in model.state_dict().items()}, os.path.join(out, "model.safetensors"))
|
| 67 |
+
model.encoder.config.save_pretrained(os.path.join(out, "encoder")); tok.save_pretrained(os.path.join(out, "tokenizer"))
|
| 68 |
+
cfg.update(fine_tuned=True, model_name="laya-browser", temperature=[1.0, 1.0, 1.0], temperature_by_options={},
|
| 69 |
+
laya_fmt=os.environ.get("LAYA_FMT", "v1"), head_max_len_train=cfg["head_max_len"])
|
| 70 |
+
json.dump(cfg, open(os.path.join(out, "rl_agent_config.json"), "w"), indent=2); print("saved", out)
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
main()
|
code/jev-ultrafast.patch
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diff --git i/jev_ultrafast/model.py w/jev_ultrafast/model.py
|
| 2 |
+
index d311747..9e4a7cd 100644
|
| 3 |
+
--- i/jev_ultrafast/model.py
|
| 4 |
+
+++ w/jev_ultrafast/model.py
|
| 5 |
+
@@ -116,7 +116,8 @@ def choose(state, goal, history):
|
| 6 |
+
"questions": questions,
|
| 7 |
+
}
|
| 8 |
+
started = time.perf_counter()
|
| 9 |
+
- result = post_json("https://api.typesafe.ai/v1/systemone", os.environ["TYPESAFE_API_KEY"], body)
|
| 10 |
+
+ result = post_json(os.environ.get("TYPESAFE_BASE_URL", "https://api.typesafe.ai").rstrip("/") + "/v1/systemone",
|
| 11 |
+
+ os.environ.get("TYPESAFE_API_KEY", ""), body)
|
| 12 |
+
operation_answer = validate_choice(result["answers"].get("operation", {}), operations)
|
| 13 |
+
operation = operation_answer["choice"]
|
| 14 |
+
target = None
|
| 15 |
+
@@ -166,6 +167,8 @@ def field_text(context):
|
| 16 |
+
reasoning = {"thinking": {"type": "disabled"}} if "api.deepseek.com/" in base else {"reasoning": {"effort": "low"}}
|
| 17 |
+
if os.environ.get("TEXT_MODEL_REASONING") == "none":
|
| 18 |
+
reasoning = {"reasoning": {"enabled": False}}
|
| 19 |
+
+ if os.environ.get("TEXT_MODEL_EXTRA_JSON"): # e.g. {"chat_template_kwargs": {"enable_thinking": false}} for local Qwen
|
| 20 |
+
+ reasoning = json.loads(os.environ["TEXT_MODEL_EXTRA_JSON"])
|
| 21 |
+
started = time.perf_counter()
|
| 22 |
+
result = post_json(
|
| 23 |
+
base + "/chat/completions",
|
| 24 |
+
diff --git i/uv.lock w/uv.lock
|
| 25 |
+
index 3b2f12b..d6e65fc 100644
|
| 26 |
+
--- i/uv.lock
|
| 27 |
+
+++ w/uv.lock
|
| 28 |
+
@@ -1,133 +1,133 @@
|
| 29 |
+
version = 1
|
| 30 |
+
-revision = 2
|
| 31 |
+
+revision = 3
|
| 32 |
+
requires-python = ">=3.12"
|
| 33 |
+
|
| 34 |
+
[[package]]
|
| 35 |
+
name = "anyio"
|
| 36 |
+
version = "4.15.1"
|
| 37 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 38 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 39 |
+
dependencies = [
|
| 40 |
+
{ name = "idna" },
|
| 41 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
|
| 42 |
+
]
|
| 43 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
|
| 44 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
|
| 45 |
+
wheels = [
|
| 46 |
+
- { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
|
| 47 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
[[package]]
|
| 51 |
+
name = "browser-harness"
|
| 52 |
+
version = "0.1.13"
|
| 53 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 54 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 55 |
+
dependencies = [
|
| 56 |
+
{ name = "cdp-use" },
|
| 57 |
+
{ name = "fetch-use" },
|
| 58 |
+
{ name = "pillow" },
|
| 59 |
+
{ name = "websockets" },
|
| 60 |
+
]
|
| 61 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/7f/fe/59ab493e7cf76731a7f5aabae3821eea6ea82a628c14c30e7aed48adc362/browser_harness-0.1.13.tar.gz", hash = "sha256:284dc547a042c309feafd9a9f4a74b2a8651b7963ea3ac6cb2f2d64889f6a8f3", size = 114905, upload-time = "2026-09-04T02:53:23.133Z" }
|
| 62 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/fe/59ab493e7cf76731a7f5aabae3821eea6ea82a628c14c30e7aed48adc362/browser_harness-0.1.13.tar.gz", hash = "sha256:284dc547a042c309feafd9a9f4a74b2a8651b7963ea3ac6cb2f2d64889f6a8f3", size = 114905, upload-time = "2026-09-04T02:53:23.133Z" }
|
| 63 |
+
wheels = [
|
| 64 |
+
- { url = "https://files.pythonhosted.org/packages/4a/bd/f166cf9a465eff436048d25851ab28b0d798af02b56998127a4165227fc4/browser_harness-0.1.13-py3-none-any.whl", hash = "sha256:2491459e4bfc0ee8aea22dc6c4680fc0f791b7ba553446323c50d2883449d769", size = 121123, upload-time = "2026-09-04T02:53:21.841Z" },
|
| 65 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/bd/f166cf9a465eff436048d25851ab28b0d798af02b56998127a4165227fc4/browser_harness-0.1.13-py3-none-any.whl", hash = "sha256:2491459e4bfc0ee8aea22dc6c4680fc0f791b7ba553446323c50d2883449d769", size = 121123, upload-time = "2026-09-04T02:53:21.841Z" },
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
[[package]]
|
| 69 |
+
name = "cdp-use"
|
| 70 |
+
version = "1.4.5"
|
| 71 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 72 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 73 |
+
dependencies = [
|
| 74 |
+
{ name = "httpx" },
|
| 75 |
+
{ name = "typing-extensions" },
|
| 76 |
+
{ name = "websockets" },
|
| 77 |
+
]
|
| 78 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/f7/7a/c549417e8c5e4dface6d5d828cd7dc72502dcea33a99f5324abf5a853ce9/cdp_use-1.4.5.tar.gz", hash = "sha256:0da3a32df46336a03ff5a22bc6bc442cd7d2f2d50a118fd4856f29d37f6d26a0", size = 193961, upload-time = "2026-02-22T04:32:50.574Z" }
|
| 79 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/7a/c549417e8c5e4dface6d5d828cd7dc72502dcea33a99f5324abf5a853ce9/cdp_use-1.4.5.tar.gz", hash = "sha256:0da3a32df46336a03ff5a22bc6bc442cd7d2f2d50a118fd4856f29d37f6d26a0", size = 193961, upload-time = "2026-02-22T04:32:50.574Z" }
|
| 80 |
+
wheels = [
|
| 81 |
+
- { url = "https://files.pythonhosted.org/packages/56/12/386d8c6bf0448c43674e24d6194c3b57d62e5361e90bca3d58108819ad32/cdp_use-1.4.5-py3-none-any.whl", hash = "sha256:8f8e2435e3a20e4009d2974144192cf3c132f6c2971338e156198814d9b91ecb", size = 350504, upload-time = "2026-02-22T04:32:49.22Z" },
|
| 82 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/12/386d8c6bf0448c43674e24d6194c3b57d62e5361e90bca3d58108819ad32/cdp_use-1.4.5-py3-none-any.whl", hash = "sha256:8f8e2435e3a20e4009d2974144192cf3c132f6c2971338e156198814d9b91ecb", size = 350504, upload-time = "2026-02-22T04:32:49.22Z" },
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
[[package]]
|
| 86 |
+
name = "certifi"
|
| 87 |
+
version = "2026.7.22"
|
| 88 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 89 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
| 90 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 91 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
| 92 |
+
wheels = [
|
| 93 |
+
- { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
| 94 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
[[package]]
|
| 98 |
+
name = "colorama"
|
| 99 |
+
version = "0.4.6"
|
| 100 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 101 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
| 102 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 103 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
| 104 |
+
wheels = [
|
| 105 |
+
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 106 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
[[package]]
|
| 110 |
+
name = "fetch-use"
|
| 111 |
+
version = "0.4.0"
|
| 112 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 113 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/5d/2d/66784fa8b66a04f170ad8f6598688b30b3a194dad4185b36d53da4ae1505/fetch_use-0.4.0.tar.gz", hash = "sha256:9511987d4907ec6dac501e21d66946d10098f66b5d21bc2aba4189cd81ba189a", size = 8974, upload-time = "2026-04-09T04:02:44.384Z" }
|
| 114 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 115 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/2d/66784fa8b66a04f170ad8f6598688b30b3a194dad4185b36d53da4ae1505/fetch_use-0.4.0.tar.gz", hash = "sha256:9511987d4907ec6dac501e21d66946d10098f66b5d21bc2aba4189cd81ba189a", size = 8974, upload-time = "2026-04-09T04:02:44.384Z" }
|
| 116 |
+
wheels = [
|
| 117 |
+
- { url = "https://files.pythonhosted.org/packages/57/97/d4104692aa5c99a30fea22b5adffd2ce35b1ad86ae5236766cfc1ae468f1/fetch_use-0.4.0-py3-none-any.whl", hash = "sha256:b7885f2907e7920373fa75dcdb00afd6e603a25a9ee2151aa2881f5465e1a2c0", size = 9468, upload-time = "2026-04-09T04:02:43.458Z" },
|
| 118 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/97/d4104692aa5c99a30fea22b5adffd2ce35b1ad86ae5236766cfc1ae468f1/fetch_use-0.4.0-py3-none-any.whl", hash = "sha256:b7885f2907e7920373fa75dcdb00afd6e603a25a9ee2151aa2881f5465e1a2c0", size = 9468, upload-time = "2026-04-09T04:02:43.458Z" },
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
[[package]]
|
| 122 |
+
name = "h11"
|
| 123 |
+
version = "0.16.0"
|
| 124 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 125 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
| 126 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 127 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
| 128 |
+
wheels = [
|
| 129 |
+
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
| 130 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
[[package]]
|
| 134 |
+
name = "h2"
|
| 135 |
+
version = "4.4.1"
|
| 136 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 137 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 138 |
+
dependencies = [
|
| 139 |
+
{ name = "hpack" },
|
| 140 |
+
{ name = "hyperframe" },
|
| 141 |
+
]
|
| 142 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" }
|
| 143 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" }
|
| 144 |
+
wheels = [
|
| 145 |
+
- { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" },
|
| 146 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" },
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
[[package]]
|
| 150 |
+
name = "hpack"
|
| 151 |
+
version = "4.2.0"
|
| 152 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 153 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" }
|
| 154 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 155 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" }
|
| 156 |
+
wheels = [
|
| 157 |
+
- { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" },
|
| 158 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" },
|
| 159 |
+
]
|
| 160 |
+
|
| 161 |
+
[[package]]
|
| 162 |
+
name = "httpcore"
|
| 163 |
+
version = "1.0.9"
|
| 164 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 165 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 166 |
+
dependencies = [
|
| 167 |
+
{ name = "certifi" },
|
| 168 |
+
{ name = "h11" },
|
| 169 |
+
]
|
| 170 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
| 171 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
| 172 |
+
wheels = [
|
| 173 |
+
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
| 174 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
[[package]]
|
| 178 |
+
name = "httpx"
|
| 179 |
+
version = "0.28.1"
|
| 180 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 181 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 182 |
+
dependencies = [
|
| 183 |
+
{ name = "anyio" },
|
| 184 |
+
{ name = "certifi" },
|
| 185 |
+
{ name = "httpcore" },
|
| 186 |
+
{ name = "idna" },
|
| 187 |
+
]
|
| 188 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
| 189 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
| 190 |
+
wheels = [
|
| 191 |
+
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
| 192 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
[package.optional-dependencies]
|
| 196 |
+
@@ -138,28 +138,28 @@ http2 = [
|
| 197 |
+
[[package]]
|
| 198 |
+
name = "hyperframe"
|
| 199 |
+
version = "6.1.0"
|
| 200 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 201 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
|
| 202 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 203 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
|
| 204 |
+
wheels = [
|
| 205 |
+
- { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
|
| 206 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
[[package]]
|
| 210 |
+
name = "idna"
|
| 211 |
+
version = "3.19"
|
| 212 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 213 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
| 214 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 215 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
| 216 |
+
wheels = [
|
| 217 |
+
- { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
| 218 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
| 219 |
+
]
|
| 220 |
+
|
| 221 |
+
[[package]]
|
| 222 |
+
name = "iniconfig"
|
| 223 |
+
version = "2.3.0"
|
| 224 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 225 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
| 226 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 227 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
| 228 |
+
wheels = [
|
| 229 |
+
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
| 230 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
| 231 |
+
]
|
| 232 |
+
|
| 233 |
+
[[package]]
|
| 234 |
+
@@ -194,105 +194,105 @@ dev = [
|
| 235 |
+
[[package]]
|
| 236 |
+
name = "packaging"
|
| 237 |
+
version = "26.3"
|
| 238 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 239 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
| 240 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 241 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
| 242 |
+
wheels = [
|
| 243 |
+
- { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
| 244 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
[[package]]
|
| 248 |
+
name = "pillow"
|
| 249 |
+
version = "12.3.0"
|
| 250 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 251 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
| 252 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 253 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
| 254 |
+
wheels = [
|
| 255 |
+
- { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
| 256 |
+
- { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
| 257 |
+
- { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
| 258 |
+
- { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
| 259 |
+
- { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
| 260 |
+
- { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
| 261 |
+
- { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
| 262 |
+
- { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
| 263 |
+
- { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
| 264 |
+
- { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
|
| 265 |
+
- { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
|
| 266 |
+
- { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
|
| 267 |
+
- { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
|
| 268 |
+
- { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
|
| 269 |
+
- { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
|
| 270 |
+
- { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
|
| 271 |
+
- { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
|
| 272 |
+
- { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
|
| 273 |
+
- { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
|
| 274 |
+
- { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
|
| 275 |
+
- { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
|
| 276 |
+
- { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
| 277 |
+
- { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
| 278 |
+
- { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
| 279 |
+
- { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
| 280 |
+
- { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
| 281 |
+
- { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
| 282 |
+
- { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
| 283 |
+
- { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
| 284 |
+
- { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
| 285 |
+
- { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
| 286 |
+
- { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
| 287 |
+
- { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
| 288 |
+
- { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
| 289 |
+
- { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
| 290 |
+
- { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
| 291 |
+
- { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
| 292 |
+
- { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
| 293 |
+
- { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
| 294 |
+
- { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
| 295 |
+
- { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
| 296 |
+
- { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
| 297 |
+
- { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
|
| 298 |
+
- { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
|
| 299 |
+
- { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
|
| 300 |
+
- { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
|
| 301 |
+
- { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
|
| 302 |
+
- { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
|
| 303 |
+
- { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
|
| 304 |
+
- { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
|
| 305 |
+
- { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
|
| 306 |
+
- { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
|
| 307 |
+
- { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
|
| 308 |
+
- { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
|
| 309 |
+
- { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
|
| 310 |
+
- { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
|
| 311 |
+
- { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
|
| 312 |
+
- { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
|
| 313 |
+
- { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
|
| 314 |
+
- { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
|
| 315 |
+
- { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
|
| 316 |
+
- { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
|
| 317 |
+
- { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
|
| 318 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
| 319 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
| 320 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
| 321 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
| 322 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
| 323 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
| 324 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
| 325 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
| 326 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
| 327 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
|
| 328 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
|
| 329 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
|
| 330 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
|
| 331 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
|
| 332 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
|
| 333 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
|
| 334 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
|
| 335 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
|
| 336 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
|
| 337 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
|
| 338 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
|
| 339 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
| 340 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
| 341 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
| 342 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
| 343 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
| 344 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
| 345 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
| 346 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
| 347 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
| 348 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
| 349 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
| 350 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
| 351 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
| 352 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
| 353 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
| 354 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
| 355 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
| 356 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
| 357 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
| 358 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
| 359 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
| 360 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
|
| 361 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
|
| 362 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
|
| 363 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
|
| 364 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
|
| 365 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
|
| 366 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
|
| 367 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
|
| 368 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
|
| 369 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
|
| 370 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
|
| 371 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
|
| 372 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
|
| 373 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
|
| 374 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
|
| 375 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
|
| 376 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
|
| 377 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
|
| 378 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
|
| 379 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
|
| 380 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
|
| 381 |
+
]
|
| 382 |
+
|
| 383 |
+
[[package]]
|
| 384 |
+
name = "pluggy"
|
| 385 |
+
version = "1.6.0"
|
| 386 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 387 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
| 388 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 389 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
| 390 |
+
wheels = [
|
| 391 |
+
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
| 392 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
| 393 |
+
]
|
| 394 |
+
|
| 395 |
+
[[package]]
|
| 396 |
+
name = "pygments"
|
| 397 |
+
version = "2.21.0"
|
| 398 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 399 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
| 400 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 401 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
| 402 |
+
wheels = [
|
| 403 |
+
- { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
| 404 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
| 405 |
+
]
|
| 406 |
+
|
| 407 |
+
[[package]]
|
| 408 |
+
name = "pytest"
|
| 409 |
+
version = "8.4.2"
|
| 410 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 411 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 412 |
+
dependencies = [
|
| 413 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 414 |
+
{ name = "iniconfig" },
|
| 415 |
+
@@ -300,72 +300,72 @@ dependencies = [
|
| 416 |
+
{ name = "pluggy" },
|
| 417 |
+
{ name = "pygments" },
|
| 418 |
+
]
|
| 419 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
| 420 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
| 421 |
+
wheels = [
|
| 422 |
+
- { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
| 423 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
[[package]]
|
| 427 |
+
name = "ruff"
|
| 428 |
+
version = "0.16.8"
|
| 429 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 430 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
|
| 431 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 432 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
|
| 433 |
+
wheels = [
|
| 434 |
+
- { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
|
| 435 |
+
- { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
|
| 436 |
+
- { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
|
| 437 |
+
- { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
|
| 438 |
+
- { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
|
| 439 |
+
- { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
|
| 440 |
+
- { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
|
| 441 |
+
- { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
|
| 442 |
+
- { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
|
| 443 |
+
- { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
|
| 444 |
+
- { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
|
| 445 |
+
- { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
|
| 446 |
+
- { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
|
| 447 |
+
- { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
|
| 448 |
+
- { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
|
| 449 |
+
- { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
|
| 450 |
+
- { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
|
| 451 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
|
| 452 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
|
| 453 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
|
| 454 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
|
| 455 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
|
| 456 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
|
| 457 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
|
| 458 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
|
| 459 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
|
| 460 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
|
| 461 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
|
| 462 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
|
| 463 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
|
| 464 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
|
| 465 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
|
| 466 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
|
| 467 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
|
| 468 |
+
]
|
| 469 |
+
|
| 470 |
+
[[package]]
|
| 471 |
+
name = "typing-extensions"
|
| 472 |
+
version = "4.16.0"
|
| 473 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 474 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
| 475 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 476 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
| 477 |
+
wheels = [
|
| 478 |
+
- { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
| 479 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
| 480 |
+
]
|
| 481 |
+
|
| 482 |
+
[[package]]
|
| 483 |
+
name = "websockets"
|
| 484 |
+
version = "15.0.1"
|
| 485 |
+
-source = { registry = "https://pypi.org/simple" }
|
| 486 |
+
-sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
| 487 |
+
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
| 488 |
+
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
| 489 |
+
wheels = [
|
| 490 |
+
- { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
| 491 |
+
- { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
| 492 |
+
- { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
| 493 |
+
- { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
| 494 |
+
- { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
| 495 |
+
- { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
| 496 |
+
- { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
| 497 |
+
- { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
| 498 |
+
- { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
| 499 |
+
- { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
| 500 |
+
- { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
| 501 |
+
- { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
| 502 |
+
- { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
| 503 |
+
- { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
| 504 |
+
- { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
| 505 |
+
- { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
| 506 |
+
- { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
| 507 |
+
- { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
| 508 |
+
- { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
| 509 |
+
- { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
| 510 |
+
- { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
| 511 |
+
- { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
| 512 |
+
- { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
| 513 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
| 514 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
| 515 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
| 516 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
| 517 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
| 518 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
| 519 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
| 520 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
| 521 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
| 522 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
| 523 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
| 524 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
| 525 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
| 526 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
| 527 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
| 528 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
| 529 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
| 530 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
| 531 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
| 532 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
| 533 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
| 534 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
| 535 |
+
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
| 536 |
+
]
|
code/kernels/bench_fast.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stock forward vs TileLang fast path: numerics, latency, and accuracy on real datasets.
|
| 2 |
+
|
| 3 |
+
python benchmarks/bench_fast.py # english checkpoint
|
| 4 |
+
python benchmarks/bench_fast.py --subfolder multilingual
|
| 5 |
+
python benchmarks/bench_fast.py --eval 1000 # + AG News / dair-ai emotion accuracy & ECE
|
| 6 |
+
|
| 7 |
+
Set HF_ENDPOINT to a mirror if huggingface.co is slow for you.
|
| 8 |
+
"""
|
| 9 |
+
import argparse, json, os, sys, time
|
| 10 |
+
os.environ.setdefault("USE_TF", "0"); os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
import numpy as np, torch
|
| 13 |
+
import laya
|
| 14 |
+
from laya.common import QTYPES, build_sequence, collate_items, ece_score
|
| 15 |
+
|
| 16 |
+
ap = argparse.ArgumentParser()
|
| 17 |
+
ap.add_argument("--model", default="convaiinnovations/laya"); ap.add_argument("--subfolder", default=None)
|
| 18 |
+
ap.add_argument("--eval", type=int, default=0, help="samples per dataset for the accuracy comparison (0 = skip)")
|
| 19 |
+
ap.add_argument("--iters", type=int, default=30); ap.add_argument("--json", default=None)
|
| 20 |
+
args = ap.parse_args()
|
| 21 |
+
|
| 22 |
+
agent = laya.load(args.model, subfolder=args.subfolder)
|
| 23 |
+
if agent.device.type != "cuda":
|
| 24 |
+
sys.exit("needs a CUDA device")
|
| 25 |
+
report = {"model": args.model, "subfolder": args.subfolder, "gpu": torch.cuda.get_device_name(0), "torch": torch.__version__}
|
| 26 |
+
|
| 27 |
+
Q = {"department": {"type": "choice", "instructions": "Which team should handle this?",
|
| 28 |
+
"criteria": {"billing": "invoices, refunds", "technical": "bugs, outages", "sales": "pricing", "shipping": "delivery"}},
|
| 29 |
+
"urgency": {"type": "score", "instructions": "How urgent is this?", "criteria": ["not urgent", "soon", "blocking"]},
|
| 30 |
+
"churn": {"type": "noul", "instructions": "Does the user threaten to cancel?"}}
|
| 31 |
+
def qs(n): return {f"{k}{i}": v for i in range(n) for k, v in Q.items()}
|
| 32 |
+
short = {"subject": "Duplicate charge on invoice 4411", "body": "We were billed twice for March. Please refund the duplicate or we're moving to a competitor."}
|
| 33 |
+
long_ = {"subject": "Outage report", "body": "Since yesterday our whole team cannot log in, the dashboard returns 502 errors and our release is blocked. " * 40}
|
| 34 |
+
|
| 35 |
+
def batch(state, q):
|
| 36 |
+
items = []
|
| 37 |
+
for qid in q:
|
| 38 |
+
qq = agent._to_internal(q[qid]); seq, m = build_sequence(agent.tok, state, qq, agent.cfg["max_len"], agent.cfg["head_max_len"])
|
| 39 |
+
items.append({"ids": seq, "markers": m, "qtype": QTYPES[qq["t"]]})
|
| 40 |
+
return {k: v.cuda() for k, v in collate_items([items], agent.tok.pad_token_id).items() if torch.is_tensor(v)}
|
| 41 |
+
|
| 42 |
+
def fwd(b, amp=True):
|
| 43 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=agent.dtype, enabled=amp):
|
| 44 |
+
return agent.model(b["input_ids"], b["attention_mask"], b["marker_pos"], b["marker_mask"], b["qtype"])
|
| 45 |
+
|
| 46 |
+
def timeit(fn, iters=args.iters):
|
| 47 |
+
for _ in range(3): fn()
|
| 48 |
+
torch.cuda.synchronize(); t = time.perf_counter()
|
| 49 |
+
for _ in range(iters): fn()
|
| 50 |
+
torch.cuda.synchronize(); return (time.perf_counter() - t) / iters * 1000
|
| 51 |
+
|
| 52 |
+
cases = [("short x1", short, {"department": Q["department"]}), ("short x3", short, Q), ("short x30", short, qs(10)),
|
| 53 |
+
("long x3", long_, Q), ("long x30", long_, qs(10))]
|
| 54 |
+
P = lambda l: torch.softmax(l.float(), -1)
|
| 55 |
+
print(f"\n== {report['gpu']} {args.model}/{args.subfolder or ''} dtype={agent.dtype}")
|
| 56 |
+
print("== numerics: max |p - p_fp32| over all options")
|
| 57 |
+
rows = []
|
| 58 |
+
for name, st, q in cases:
|
| 59 |
+
b = batch(st, q)
|
| 60 |
+
agent.deaccelerate()
|
| 61 |
+
l32, _ = fwd(b, amp=False); lo, _ = fwd(b); t_stock = timeit(lambda: fwd(b))
|
| 62 |
+
assert agent.accelerate(strict=True)
|
| 63 |
+
lf, _ = fwd(b); t_fast = timeit(lambda: fwd(b))
|
| 64 |
+
d_o, d_f, d_of = [(P(a) - P(c)).abs().max().item() for a, c in ((lo, l32), (lf, l32), (lf, lo))]
|
| 65 |
+
agree = (lf.argmax(-1) == l32.argmax(-1)).float().mean().item()
|
| 66 |
+
L = b["input_ids"].shape[1]
|
| 67 |
+
print(f"{name:10s} L={L:4d} stock-bf16={d_o:.4f} fast={d_f:.4f} fast-vs-stock={d_of:.4f} argmax agree={agree:.2f}")
|
| 68 |
+
rows.append(dict(case=name, L=L, stock_ms=t_stock, fast_ms=t_fast, dp_stock=d_o, dp_fast=d_f, agree=agree))
|
| 69 |
+
print("\n== model forward latency (ms)")
|
| 70 |
+
print(f"{'case':10s} {'L':>5s} {'stock':>9s} {'fast':>9s} {'speedup':>8s}")
|
| 71 |
+
for r in rows:
|
| 72 |
+
print(f"{r['case']:10s} {r['L']:5d} {r['stock_ms']:9.2f} {r['fast_ms']:9.2f} {r['stock_ms']/r['fast_ms']:7.1f}x")
|
| 73 |
+
print("\n== end-to-end agent.predict() incl. tokenization (ms)")
|
| 74 |
+
for name, st, q in cases:
|
| 75 |
+
agent.deaccelerate(); ts = timeit(lambda: agent.predict(st, q)); agent.accelerate(strict=True); tf = timeit(lambda: agent.predict(st, q))
|
| 76 |
+
print(f"{name:10s} stock={ts:8.2f} fast={tf:8.2f} {ts/tf:5.1f}x")
|
| 77 |
+
[r for r in rows if r["case"] == name][0].update(e2e_stock_ms=ts, e2e_fast_ms=tf)
|
| 78 |
+
report["latency"] = rows
|
| 79 |
+
|
| 80 |
+
if args.eval:
|
| 81 |
+
from datasets import load_dataset
|
| 82 |
+
evals = {
|
| 83 |
+
"ag_news": ("fancyzhx/ag_news", "test", "text", "label",
|
| 84 |
+
{"world": "international news, politics, conflicts", "sports": "sports, games, athletes",
|
| 85 |
+
"business": "companies, markets, economy", "sci/tech": "science, technology, software, space"}),
|
| 86 |
+
"emotion": ("dair-ai/emotion", "test", "text", "label",
|
| 87 |
+
{"sadness": None, "joy": None, "love": None, "anger": None, "fear": None, "surprise": None}),
|
| 88 |
+
}
|
| 89 |
+
report["eval"] = {}
|
| 90 |
+
print(f"\n== accuracy on real datasets ({args.eval} samples each), stock vs fast")
|
| 91 |
+
print(f"{'dataset':9s} {'acc stock':>10s} {'acc fast':>9s} {'ECE stock':>10s} {'ECE fast':>9s} {'agree':>6s} {'stock ms/it':>12s} {'fast ms/it':>11s}")
|
| 92 |
+
for name, (repo, split, tcol, lcol, crit) in evals.items():
|
| 93 |
+
ds = load_dataset(repo, split=split).shuffle(seed=0).select(range(args.eval))
|
| 94 |
+
labels = list(crit.keys())
|
| 95 |
+
q = {"label": {"type": "choice", "instructions": f"Which category does this {name.replace('_', ' ')} text belong to?", "criteria": crit}}
|
| 96 |
+
out = {}
|
| 97 |
+
for mode in ("stock", "fast"):
|
| 98 |
+
agent.deaccelerate() if mode == "stock" else agent.accelerate(strict=True)
|
| 99 |
+
preds, confs, correct = [], [], []
|
| 100 |
+
torch.cuda.synchronize(); t = time.perf_counter()
|
| 101 |
+
for ex in ds:
|
| 102 |
+
a = agent.predict({"text": ex[tcol]}, q)["answers"]["label"]
|
| 103 |
+
preds.append(a["choice"]); confs.append(max(a["probabilities"].values())); correct.append(labels.index(a["choice"]) == ex[lcol])
|
| 104 |
+
torch.cuda.synchronize(); dt = (time.perf_counter() - t) / len(ds) * 1000
|
| 105 |
+
out[mode] = dict(acc=float(np.mean(correct)), ece=ece_score(np.array(confs), np.array(correct, dtype=float)), ms=dt, preds=preds)
|
| 106 |
+
agree = float(np.mean([a == b for a, b in zip(out["stock"]["preds"], out["fast"]["preds"])]))
|
| 107 |
+
print(f"{name:9s} {out['stock']['acc']:10.3f} {out['fast']['acc']:9.3f} {out['stock']['ece']:10.3f} {out['fast']['ece']:9.3f} {agree:6.3f} {out['stock']['ms']:12.1f} {out['fast']['ms']:11.1f}")
|
| 108 |
+
report["eval"][name] = dict(n=args.eval, stock={k: v for k, v in out["stock"].items() if k != "preds"}, fast={k: v for k, v in out["fast"].items() if k != "preds"}, agreement=agree)
|
| 109 |
+
if args.json:
|
| 110 |
+
json.dump(report, open(args.json, "w"), indent=1); print("saved", args.json)
|
code/kernels/fast.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GPU fast path for laya: TileLang fused kernels + bf16 resident weights + CUDA graphs.
|
| 2 |
+
|
| 3 |
+
agent = laya.load("convaiinnovations/laya", fast=True) # or agent.accelerate()
|
| 4 |
+
|
| 5 |
+
Requires CUDA and `pip install laya[fast]` (tilelang). Falls back to the stock forward otherwise.
|
| 6 |
+
"""
|
| 7 |
+
import sys
|
| 8 |
+
import torch
|
| 9 |
+
import tl_kernels as K
|
| 10 |
+
|
| 11 |
+
BF = torch.bfloat16
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _bucket_n(n):
|
| 15 |
+
return 1 << max(0, (n - 1).bit_length())
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class FastLaya:
|
| 19 |
+
def __init__(self, model, max_len=1024, use_graphs=True, verbose=False):
|
| 20 |
+
self.m = model
|
| 21 |
+
enc = model.encoder
|
| 22 |
+
cfg = enc.config
|
| 23 |
+
dev = next(model.parameters()).device
|
| 24 |
+
self.dev = dev
|
| 25 |
+
self.use_graphs = use_graphs
|
| 26 |
+
self.verbose = verbose
|
| 27 |
+
self.H, self.Dh, self.D = cfg.num_attention_heads, cfg.hidden_size // cfg.num_attention_heads, cfg.hidden_size
|
| 28 |
+
self.F = cfg.intermediate_size
|
| 29 |
+
self.eps = cfg.norm_eps
|
| 30 |
+
self.max_len = max_len
|
| 31 |
+
f32 = lambda t: t.detach().float().contiguous()
|
| 32 |
+
b16 = lambda t: t.detach().to(BF).contiguous()
|
| 33 |
+
zeros = torch.zeros(self.D, device=dev)
|
| 34 |
+
self.zeros = {self.D: zeros, 3 * self.D: torch.zeros(3 * self.D, device=dev), 4 * self.D: torch.zeros(4 * self.D, device=dev),
|
| 35 |
+
2 * self.F: torch.zeros(2 * self.F, device=dev)}
|
| 36 |
+
# --- encoder weights
|
| 37 |
+
self.emb_w = b16(enc.embeddings.tok_embeddings.weight)
|
| 38 |
+
self.emb_ln = f32(enc.embeddings.norm.weight)
|
| 39 |
+
self.layers = []
|
| 40 |
+
for i, lyr in enumerate(enc.layers):
|
| 41 |
+
self.layers.append(dict(
|
| 42 |
+
attn_ln=None if i == 0 else f32(lyr.attn_norm.weight),
|
| 43 |
+
wqkv=b16(lyr.attn.Wqkv.weight), wo=b16(lyr.attn.Wo.weight),
|
| 44 |
+
mlp_ln=f32(lyr.mlp_norm.weight), wi=b16(lyr.mlp.Wi.weight), wo2=b16(lyr.mlp.Wo.weight),
|
| 45 |
+
window=lyr.attn.sliding_window or 0, ltype=lyr.attention_type))
|
| 46 |
+
self.final_ln = f32(enc.final_norm.weight)
|
| 47 |
+
# --- rotary tables (rounded through bf16 exactly like HF does before applying)
|
| 48 |
+
rot = enc.rotary_emb
|
| 49 |
+
pos = torch.arange(max_len, device=dev).float()
|
| 50 |
+
self.rope = {}
|
| 51 |
+
for lt in set(cfg.layer_types):
|
| 52 |
+
inv = getattr(rot, f"{lt}_inv_freq").float()
|
| 53 |
+
scl = getattr(rot, f"{lt}_attention_scaling")
|
| 54 |
+
fr = torch.outer(pos, inv)
|
| 55 |
+
self.rope[lt] = ((fr.cos() * scl).to(BF).float().contiguous(), (fr.sin() * scl).to(BF).float().contiguous())
|
| 56 |
+
# --- decision head (nn.TransformerEncoderLayer, norm_first, relu)
|
| 57 |
+
self.type_emb = b16(model.type_emb.weight)
|
| 58 |
+
self.head = []
|
| 59 |
+
for lyr in model.head.layers:
|
| 60 |
+
sa = lyr.self_attn
|
| 61 |
+
self.head.append(dict(
|
| 62 |
+
n1w=f32(lyr.norm1.weight), n1b=f32(lyr.norm1.bias), n2w=f32(lyr.norm2.weight), n2b=f32(lyr.norm2.bias),
|
| 63 |
+
in_w=b16(sa.in_proj_weight), in_b=f32(sa.in_proj_bias), out_w=b16(sa.out_proj.weight), out_b=f32(sa.out_proj.bias),
|
| 64 |
+
l1w=b16(lyr.linear1.weight), l1b=f32(lyr.linear1.bias), l2w=b16(lyr.linear2.weight), l2b=f32(lyr.linear2.bias)))
|
| 65 |
+
# --- kernels (M is dynamic, so these compile once)
|
| 66 |
+
D, F = self.D, self.F
|
| 67 |
+
self.k_qkv = K.gemm_kernel(3 * D, D)
|
| 68 |
+
self.k_o = K.gemm_kernel(D, D)
|
| 69 |
+
self.k_geglu = K.gemm_geglu_kernel(F, D)
|
| 70 |
+
self.k_o2 = K.gemm_kernel(D, F)
|
| 71 |
+
self.k_ln = K.add_ln_kernel(D, residual=False, bias=False, eps=self.eps)
|
| 72 |
+
self.k_addln = K.add_ln_kernel(D, residual=True, bias=False, eps=self.eps)
|
| 73 |
+
self.k_addln_b = K.add_ln_kernel(D, residual=True, bias=True, eps=1e-5)
|
| 74 |
+
self.k_ln_b = K.add_ln_kernel(D, residual=False, bias=True, eps=1e-5)
|
| 75 |
+
self.k_in = K.gemm_kernel(3 * D, D, bias=True)
|
| 76 |
+
self.k_out = K.gemm_kernel(D, D, bias=True)
|
| 77 |
+
self.k_ffn1 = K.gemm_kernel(4 * D, D, bias=True, act="relu")
|
| 78 |
+
self.k_ffn2 = K.gemm_kernel(D, 4 * D, bias=True)
|
| 79 |
+
self._rope_k, self._rope_tab, self._attn_k = None, {}, {}
|
| 80 |
+
self.graphs = {}
|
| 81 |
+
|
| 82 |
+
# ------------------------------------------------------------------ kernels per shape
|
| 83 |
+
DYNAMIC_MAX_L = 256 # up to here one dynamic-shape attention kernel is as fast as a static one
|
| 84 |
+
LONG_BUCKET = 64 # beyond it, static kernels per (B, L) bucket of this size
|
| 85 |
+
|
| 86 |
+
def rope_k(self):
|
| 87 |
+
if self._rope_k is None:
|
| 88 |
+
self._rope_k = K.rope_kernel(self.H, self.Dh)
|
| 89 |
+
return self._rope_k
|
| 90 |
+
|
| 91 |
+
def rope_tab(self, ltype, L):
|
| 92 |
+
key = (ltype, L)
|
| 93 |
+
if key not in self._rope_tab:
|
| 94 |
+
cos, sin = self.rope[ltype]
|
| 95 |
+
self._rope_tab[key] = (cos[:L].contiguous(), sin[:L].contiguous())
|
| 96 |
+
return self._rope_tab[key]
|
| 97 |
+
|
| 98 |
+
def attn_k(self, B, L, window):
|
| 99 |
+
key = (None, None, window) if L <= self.DYNAMIC_MAX_L else (B, L, window)
|
| 100 |
+
if key not in self._attn_k:
|
| 101 |
+
self._attn_k[key] = K.attn_kernel(key[0], key[1], self.H, self.Dh, window=window)
|
| 102 |
+
return self._attn_k[key]
|
| 103 |
+
|
| 104 |
+
# ------------------------------------------------------------------ encoder + head on padded [B, L]
|
| 105 |
+
def _encode(self, ids, lens, qtype):
|
| 106 |
+
"""ids [B,L] long (padded), lens [B] int32, qtype [B] long -> hidden [B, L, D] bf16"""
|
| 107 |
+
B, L = ids.shape
|
| 108 |
+
M, D = B * L, self.D
|
| 109 |
+
dev = self.dev
|
| 110 |
+
X = torch.nn.functional.embedding(ids, self.emb_w).view(M, D) # residual stream (bf16)
|
| 111 |
+
Y = torch.empty_like(X)
|
| 112 |
+
qkv = torch.empty(M, 3 * D, device=dev, dtype=BF)
|
| 113 |
+
O = torch.empty(M, D, device=dev, dtype=BF)
|
| 114 |
+
G = torch.empty(M, self.F, device=dev, dtype=BF)
|
| 115 |
+
z = self.zeros
|
| 116 |
+
self.k_ln(X, X, self.emb_ln, z[D], Y) # embeddings.norm
|
| 117 |
+
X, Y = Y, X # X is now the residual stream
|
| 118 |
+
nl = len(self.layers)
|
| 119 |
+
for i, ly in enumerate(self.layers):
|
| 120 |
+
src = X if i == 0 else Y # layer 0 has attn_norm = Identity
|
| 121 |
+
self.k_qkv(src, ly["wqkv"], z[3 * D], qkv)
|
| 122 |
+
cos, sin = self.rope_tab(ly["ltype"], L)
|
| 123 |
+
self.rope_k()(qkv, cos, sin)
|
| 124 |
+
self.attn_k(B, L, ly["window"])(qkv.view(B, L, 3, self.H, self.Dh), lens, O.view(B, L, D))
|
| 125 |
+
self.k_o(O, ly["wo"], z[D], Y) # Y = attn out
|
| 126 |
+
self.k_addln(X, Y, ly["mlp_ln"], z[D], Y) # X += Y ; Y = mlp_norm(X)
|
| 127 |
+
self.k_geglu(Y, ly["wi"], G)
|
| 128 |
+
self.k_o2(G, ly["wo2"], z[D], Y) # Y = mlp out
|
| 129 |
+
nxt = self.layers[i + 1]["attn_ln"] if i + 1 < nl else self.final_ln
|
| 130 |
+
self.k_addln(X, Y, nxt, z[D], Y) # X += Y ; Y = next norm(X)
|
| 131 |
+
# decision head: h = final_norm(x) + type_emb ; 2 x pre-norm transformer layers (relu ffn)
|
| 132 |
+
X = (Y.view(B, L, D) + self.type_emb[qtype][:, None, :]).view(M, D).contiguous()
|
| 133 |
+
F1 = torch.empty(M, 4 * D, device=dev, dtype=BF)
|
| 134 |
+
for j, h in enumerate(self.head):
|
| 135 |
+
self.k_ln_b(X, X, h["n1w"], h["n1b"], Y)
|
| 136 |
+
self.k_in(Y, h["in_w"], h["in_b"], qkv)
|
| 137 |
+
self.attn_k(B, L, 0)(qkv.view(B, L, 3, self.H, self.Dh), lens, O.view(B, L, D))
|
| 138 |
+
self.k_out(O, h["out_w"], h["out_b"], Y)
|
| 139 |
+
self.k_addln_b(X, Y, h["n2w"], h["n2b"], Y) # X += attn ; Y = norm2(X)
|
| 140 |
+
self.k_ffn1(Y, h["l1w"], h["l1b"], F1)
|
| 141 |
+
self.k_ffn2(F1, h["l2w"], h["l2b"], Y)
|
| 142 |
+
X = X + Y # residual (torch, last op)
|
| 143 |
+
return X.view(B, L, D)
|
| 144 |
+
|
| 145 |
+
def _encode_graphed(self, ids, lens, qtype):
|
| 146 |
+
key = tuple(ids.shape)
|
| 147 |
+
g = self.graphs.get(key)
|
| 148 |
+
if g is None:
|
| 149 |
+
s_ids, s_lens, s_q = ids.clone(), lens.clone(), qtype.clone()
|
| 150 |
+
st = torch.cuda.Stream()
|
| 151 |
+
st.wait_stream(torch.cuda.current_stream())
|
| 152 |
+
with torch.cuda.stream(st):
|
| 153 |
+
for _ in range(2):
|
| 154 |
+
self._encode(s_ids, s_lens, s_q) # warm-up (compiles kernels, allocs)
|
| 155 |
+
torch.cuda.current_stream().wait_stream(st)
|
| 156 |
+
graph = torch.cuda.CUDAGraph()
|
| 157 |
+
with torch.cuda.graph(graph):
|
| 158 |
+
s_out = self._encode(s_ids, s_lens, s_q)
|
| 159 |
+
g = self.graphs[key] = (graph, s_ids, s_lens, s_q, s_out)
|
| 160 |
+
if self.verbose:
|
| 161 |
+
print(f"[fast_laya] captured CUDA graph for shape {key}", file=sys.stderr)
|
| 162 |
+
graph, s_ids, s_lens, s_q, s_out = g
|
| 163 |
+
s_ids.copy_(ids); s_lens.copy_(lens); s_q.copy_(qtype)
|
| 164 |
+
graph.replay()
|
| 165 |
+
return s_out
|
| 166 |
+
|
| 167 |
+
# ------------------------------------------------------------------ DecisionModel.forward replacement
|
| 168 |
+
@torch.no_grad()
|
| 169 |
+
def forward(self, input_ids, attention_mask, marker_pos, marker_mask, qtype, detach_encoder=False):
|
| 170 |
+
m = self.m
|
| 171 |
+
N, L0 = input_ids.shape
|
| 172 |
+
g = 16 if L0 <= self.DYNAMIC_MAX_L else self.LONG_BUCKET
|
| 173 |
+
L = min(self.max_len, ((L0 + g - 1) // g) * g)
|
| 174 |
+
B = _bucket_n(N)
|
| 175 |
+
ids = torch.zeros(B, L, dtype=torch.long, device=self.dev)
|
| 176 |
+
ids[:N, :L0] = input_ids
|
| 177 |
+
lens = torch.zeros(B, dtype=torch.int32, device=self.dev)
|
| 178 |
+
lens[:N] = attention_mask.sum(1).to(torch.int32)
|
| 179 |
+
qt = torch.zeros(B, dtype=torch.long, device=self.dev)
|
| 180 |
+
qt[:N] = qtype
|
| 181 |
+
h = (self._encode_graphed if self.use_graphs else self._encode)(ids, lens, qt)
|
| 182 |
+
h = h[:N, :L0].float()
|
| 183 |
+
# ---- scorer / act head (tiny; identical to laya.common.DecisionModel.forward)
|
| 184 |
+
idx = marker_pos.clamp(min=0)[:, :, None].expand(-1, -1, h.size(-1))
|
| 185 |
+
mk = torch.gather(h, 1, idx)
|
| 186 |
+
logits = m.scorer(mk).squeeze(-1).float()
|
| 187 |
+
logits = logits.masked_fill(~marker_mask, -1e4)
|
| 188 |
+
p = torch.softmax(logits, -1)
|
| 189 |
+
k = marker_mask.sum(-1).clamp(min=2).float()
|
| 190 |
+
ent = -(p * torch.log(p.clamp_min(1e-9))).sum(-1) / torch.log(k)
|
| 191 |
+
top2 = p.topk(2, -1).values
|
| 192 |
+
feats = torch.stack([top2[:, 0], top2[:, 0] - top2[:, 1], ent, k / 255.0], -1)
|
| 193 |
+
pooled = h[:, 0].float()
|
| 194 |
+
act_logits = m.act_head(torch.cat([pooled, feats], -1))
|
| 195 |
+
return logits, act_logits
|
code/kernels/test_fast.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TileLang fast path: kernels vs torch reference, and full forward vs the stock model.
|
| 2 |
+
|
| 3 |
+
Skips unless CUDA + tilelang are available. Run: python -m pytest tests/test_fast.py -q
|
| 4 |
+
"""
|
| 5 |
+
import os, sys
|
| 6 |
+
import pytest
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 8 |
+
torch = pytest.importorskip("torch")
|
| 9 |
+
if not torch.cuda.is_available():
|
| 10 |
+
pytest.skip("needs CUDA", allow_module_level=True)
|
| 11 |
+
pytest.importorskip("tilelang")
|
| 12 |
+
from laya import tl_kernels as K # noqa: E402
|
| 13 |
+
|
| 14 |
+
dev = "cuda"
|
| 15 |
+
def err(a, b): return (a.float() - b.float()).abs().max().item()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_gemm_epilogues():
|
| 19 |
+
M, Kd = 200, 768 # M not a multiple of the tile on purpose
|
| 20 |
+
A = torch.randn(M, Kd, device=dev, dtype=torch.bfloat16)
|
| 21 |
+
for N, bias, act in [(2304, False, "none"), (768, True, "gelu"), (3072, True, "relu")]:
|
| 22 |
+
W = torch.randn(N, Kd, device=dev, dtype=torch.bfloat16) * 0.02; b = torch.randn(N, device=dev)
|
| 23 |
+
C = torch.empty(M, N, device=dev, dtype=torch.bfloat16)
|
| 24 |
+
K.gemm_kernel(N, Kd, bias=bias, act=act)(A, W, b, C)
|
| 25 |
+
ref = A.float() @ W.float().T + (b if bias else 0)
|
| 26 |
+
ref = {"none": ref, "gelu": torch.nn.functional.gelu(ref), "relu": torch.relu(ref)}[act]
|
| 27 |
+
assert err(C, ref) < 0.05
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_geglu():
|
| 31 |
+
M, Kd, F = 256, 768, 1152
|
| 32 |
+
A = torch.randn(M, Kd, device=dev, dtype=torch.bfloat16); Wi = torch.randn(2 * F, Kd, device=dev, dtype=torch.bfloat16) * 0.02
|
| 33 |
+
C = torch.empty(M, F, device=dev, dtype=torch.bfloat16); K.gemm_geglu_kernel(F, Kd)(A, Wi, C)
|
| 34 |
+
x = A.float() @ Wi.float().T
|
| 35 |
+
assert err(C, torch.nn.functional.gelu(x[:, :F]) * x[:, F:]) < 0.05
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_add_layernorm():
|
| 39 |
+
M, D = 100, 768
|
| 40 |
+
X = torch.randn(M, D, device=dev, dtype=torch.bfloat16); R = torch.randn_like(X)
|
| 41 |
+
w = torch.rand(D, device=dev) + 0.5; b = torch.randn(D, device=dev)
|
| 42 |
+
X2 = X.clone(); Y = torch.empty_like(X)
|
| 43 |
+
K.add_ln_kernel(D, residual=True, bias=True)(X2, R, w, b, Y)
|
| 44 |
+
xr = (X.float() + R.float()).bfloat16().float()
|
| 45 |
+
assert err(X2, xr) < 1e-6
|
| 46 |
+
assert err(Y, torch.nn.functional.layer_norm(xr, (D,), w, b, 1e-5)) < 0.05
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def test_attention_mask_and_window():
|
| 50 |
+
H, Dh, B = 12, 64, 3
|
| 51 |
+
for L, window, dyn in [(80, 0, True), (200, 65, True), (1024, 65, False), (1024, 0, False)]:
|
| 52 |
+
qkv = torch.randn(B, L, 3, H, Dh, device=dev, dtype=torch.bfloat16)
|
| 53 |
+
lens = torch.tensor([L, L - 7, max(1, L // 3)], device=dev, dtype=torch.int32)
|
| 54 |
+
O = torch.empty(B, L, H * Dh, device=dev, dtype=torch.bfloat16)
|
| 55 |
+
K.attn_kernel(None if dyn else B, None if dyn else L, H, Dh, window=window)(qkv, lens, O)
|
| 56 |
+
q, k, v = [qkv[:, :, i].transpose(1, 2).float() for i in range(3)]
|
| 57 |
+
idx = torch.arange(L, device=dev)
|
| 58 |
+
mask = (idx[None, :] < lens[:, None])[:, None, None, :].expand(B, 1, L, L)
|
| 59 |
+
if window:
|
| 60 |
+
mask = mask & ((idx[:, None] - idx[None, :]).abs() <= window)[None, None]
|
| 61 |
+
ref = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=mask).transpose(1, 2).reshape(B, L, -1)
|
| 62 |
+
valid = idx[None, :] < lens[:, None]
|
| 63 |
+
assert torch.isfinite(O).all()
|
| 64 |
+
assert (O.float() - ref)[valid].abs().max().item() < 0.02
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@pytest.mark.skipif(os.environ.get("LAYA_TEST_MODEL") is None, reason="set LAYA_TEST_MODEL=<repo or path> to run")
|
| 68 |
+
def test_full_forward_matches_stock():
|
| 69 |
+
import laya
|
| 70 |
+
from laya.common import QTYPES, build_sequence, collate_items
|
| 71 |
+
agent = laya.load(os.environ["LAYA_TEST_MODEL"], subfolder=os.environ.get("LAYA_TEST_SUBFOLDER"))
|
| 72 |
+
q = {"dept": {"type": "choice", "instructions": "Which team?", "criteria": {"billing": "refunds", "tech": "bugs", "sales": "pricing"}},
|
| 73 |
+
"urg": {"type": "score", "instructions": "How urgent?", "criteria": ["low", "mid", "high"]},
|
| 74 |
+
"churn": {"type": "noul", "instructions": "Threatens to cancel?"}}
|
| 75 |
+
st = {"body": "We were billed twice, refund now or we cancel. " * 30}
|
| 76 |
+
items = []
|
| 77 |
+
for qid in q:
|
| 78 |
+
qq = agent._to_internal(q[qid]); seq, m = build_sequence(agent.tok, st, qq, agent.cfg["max_len"], agent.cfg["head_max_len"])
|
| 79 |
+
items.append({"ids": seq, "markers": m, "qtype": QTYPES[qq["t"]]})
|
| 80 |
+
b = {k: v.cuda() for k, v in collate_items([items], agent.tok.pad_token_id).items() if torch.is_tensor(v)}
|
| 81 |
+
def run():
|
| 82 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=agent.dtype):
|
| 83 |
+
return agent.model(b["input_ids"], b["attention_mask"], b["marker_pos"], b["marker_mask"], b["qtype"])[0]
|
| 84 |
+
lo = run(); assert agent.accelerate(strict=True); lf = run()
|
| 85 |
+
assert (torch.softmax(lo.float(), -1) - torch.softmax(lf.float(), -1)).abs().max().item() < 0.02
|
code/kernels/tl_kernels.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TileLang kernels for the Laya (ModernBERT + decision head) encoder.
|
| 2 |
+
|
| 3 |
+
All kernels take bf16 activations, accumulate in fp32. Row count M is a runtime
|
| 4 |
+
symbol so one compiled kernel serves every batch/sequence bucket; M must be a
|
| 5 |
+
multiple of 16 (the caller pads); out-of-bounds rows are predicated by TileLang.
|
| 6 |
+
"""
|
| 7 |
+
import tilelang
|
| 8 |
+
import tilelang.language as T
|
| 9 |
+
|
| 10 |
+
DT, ACC = "bfloat16", "float"
|
| 11 |
+
FAST = {tilelang.PassConfigKey.TL_ENABLE_FAST_MATH: True}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _act(x, kind):
|
| 15 |
+
if kind == "gelu": # exact erf-GELU, what HF "gelu" means
|
| 16 |
+
return 0.5 * x * (1.0 + T.erf(x * 0.7071067811865476))
|
| 17 |
+
if kind == "relu":
|
| 18 |
+
return T.max(x, 0.0)
|
| 19 |
+
return x
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ----------------------------------------------------------------------------- GEMM
|
| 23 |
+
@tilelang.jit(pass_configs=FAST)
|
| 24 |
+
def gemm_kernel(N, K, bias=False, act="none", bm=64, bn=128, bk=64, stages=3, threads=128):
|
| 25 |
+
"""C[M,N] = act(A[M,K] @ W[N,K]^T + b)."""
|
| 26 |
+
M = T.dynamic("M")
|
| 27 |
+
|
| 28 |
+
@T.prim_func
|
| 29 |
+
def main(A: T.Tensor((M, K), DT), W: T.Tensor((N, K), DT), Bv: T.Tensor((N,), ACC), C: T.Tensor((M, N), DT)):
|
| 30 |
+
with T.Kernel(T.ceildiv(N, bn), T.ceildiv(M, bm), threads=threads) as (bx, by):
|
| 31 |
+
A_s = T.alloc_shared((bm, bk), DT)
|
| 32 |
+
W_s = T.alloc_shared((bn, bk), DT)
|
| 33 |
+
C_l = T.alloc_fragment((bm, bn), ACC)
|
| 34 |
+
T.clear(C_l)
|
| 35 |
+
for k in T.Pipelined(T.ceildiv(K, bk), num_stages=stages):
|
| 36 |
+
T.copy(A[by * bm, k * bk], A_s)
|
| 37 |
+
T.copy(W[bx * bn, k * bk], W_s)
|
| 38 |
+
T.gemm(A_s, W_s, C_l, transpose_B=True)
|
| 39 |
+
for i, j in T.Parallel(bm, bn):
|
| 40 |
+
v = C_l[i, j]
|
| 41 |
+
if bias:
|
| 42 |
+
v = v + Bv[bx * bn + j]
|
| 43 |
+
C_l[i, j] = _act(v, act)
|
| 44 |
+
T.copy(C_l, C[by * bm, bx * bn])
|
| 45 |
+
return main
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@tilelang.jit(pass_configs=FAST)
|
| 49 |
+
def gemm_geglu_kernel(F, K, bm=64, bn=64, bk=64, stages=3, threads=128):
|
| 50 |
+
"""ModernBERT GLU MLP up-projection, fused: C[M,F] = gelu(A @ Wi[:F]^T) * (A @ Wi[F:]^T)."""
|
| 51 |
+
M = T.dynamic("M")
|
| 52 |
+
|
| 53 |
+
@T.prim_func
|
| 54 |
+
def main(A: T.Tensor((M, K), DT), W: T.Tensor((2 * F, K), DT), C: T.Tensor((M, F), DT)):
|
| 55 |
+
with T.Kernel(T.ceildiv(F, bn), T.ceildiv(M, bm), threads=threads) as (bx, by):
|
| 56 |
+
A_s = T.alloc_shared((bm, bk), DT)
|
| 57 |
+
Wi_s = T.alloc_shared((bn, bk), DT)
|
| 58 |
+
Wg_s = T.alloc_shared((bn, bk), DT)
|
| 59 |
+
Ci = T.alloc_fragment((bm, bn), ACC)
|
| 60 |
+
Cg = T.alloc_fragment((bm, bn), ACC)
|
| 61 |
+
T.clear(Ci); T.clear(Cg)
|
| 62 |
+
for k in T.Pipelined(T.ceildiv(K, bk), num_stages=stages):
|
| 63 |
+
T.copy(A[by * bm, k * bk], A_s)
|
| 64 |
+
T.copy(W[bx * bn, k * bk], Wi_s)
|
| 65 |
+
T.copy(W[F + bx * bn, k * bk], Wg_s)
|
| 66 |
+
T.gemm(A_s, Wi_s, Ci, transpose_B=True)
|
| 67 |
+
T.gemm(A_s, Wg_s, Cg, transpose_B=True)
|
| 68 |
+
for i, j in T.Parallel(bm, bn):
|
| 69 |
+
Ci[i, j] = _act(Ci[i, j], "gelu") * Cg[i, j]
|
| 70 |
+
T.copy(Ci, C[by * bm, bx * bn])
|
| 71 |
+
return main
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ----------------------------------------------------------------------------- LayerNorm (+residual)
|
| 75 |
+
@tilelang.jit(pass_configs=FAST)
|
| 76 |
+
def add_ln_kernel(D, residual=True, bias=False, eps=1e-5, bm=4, threads=32):
|
| 77 |
+
"""X = X + R (in place, if residual); Y = LN(X) * w (+ b). Stats in fp32."""
|
| 78 |
+
M = T.dynamic("M")
|
| 79 |
+
|
| 80 |
+
@T.prim_func
|
| 81 |
+
def main(X: T.Tensor((M, D), DT), R: T.Tensor((M, D), DT), Wv: T.Tensor((D,), ACC), Bv: T.Tensor((D,), ACC),
|
| 82 |
+
Y: T.Tensor((M, D), DT)):
|
| 83 |
+
with T.Kernel(T.ceildiv(M, bm), threads=threads) as bx:
|
| 84 |
+
x = T.alloc_fragment((bm, D), ACC)
|
| 85 |
+
xs = T.alloc_fragment((bm, D), ACC)
|
| 86 |
+
mean = T.alloc_fragment((bm,), ACC)
|
| 87 |
+
var = T.alloc_fragment((bm,), ACC)
|
| 88 |
+
Xb = T.alloc_shared((bm, D), DT)
|
| 89 |
+
T.copy(X[bx * bm, 0], Xb)
|
| 90 |
+
T.copy(Xb, x)
|
| 91 |
+
if residual:
|
| 92 |
+
T.copy(R[bx * bm, 0], Xb)
|
| 93 |
+
T.copy(Xb, xs)
|
| 94 |
+
for i, j in T.Parallel(bm, D):
|
| 95 |
+
x[i, j] = x[i, j] + xs[i, j]
|
| 96 |
+
T.copy(x, Xb)
|
| 97 |
+
T.copy(Xb, X[bx * bm, 0])
|
| 98 |
+
T.reduce_sum(x, mean, dim=1)
|
| 99 |
+
for i in T.Parallel(bm):
|
| 100 |
+
mean[i] = mean[i] / D
|
| 101 |
+
for i, j in T.Parallel(bm, D):
|
| 102 |
+
xs[i, j] = (x[i, j] - mean[i]) * (x[i, j] - mean[i])
|
| 103 |
+
T.reduce_sum(xs, var, dim=1)
|
| 104 |
+
for i in T.Parallel(bm):
|
| 105 |
+
var[i] = T.rsqrt(var[i] / D + eps)
|
| 106 |
+
for i, j in T.Parallel(bm, D):
|
| 107 |
+
v = (x[i, j] - mean[i]) * var[i] * Wv[j]
|
| 108 |
+
if bias:
|
| 109 |
+
v = v + Bv[j]
|
| 110 |
+
xs[i, j] = v
|
| 111 |
+
T.copy(xs, Xb)
|
| 112 |
+
T.copy(Xb, Y[bx * bm, 0])
|
| 113 |
+
return main
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ----------------------------------------------------------------------------- RoPE (in place on packed qkv)
|
| 117 |
+
@tilelang.jit(pass_configs=FAST)
|
| 118 |
+
def rope_kernel(H, Dh, bm=32, threads=128):
|
| 119 |
+
"""QKV[M, 3*H*Dh] packed as (q|k|v)(h)(d). Rotates q and k in place (rotate-half convention, fp32 math).
|
| 120 |
+
cos/sin: [L, Dh/2]. Row r has position r % L. M and L are runtime symbols."""
|
| 121 |
+
M, L = T.dynamic("M"), T.dynamic("L")
|
| 122 |
+
half = Dh // 2
|
| 123 |
+
W = 2 * H * Dh # q and k columns
|
| 124 |
+
|
| 125 |
+
@T.prim_func
|
| 126 |
+
def main(QKV: T.Tensor((M, 3 * H * Dh), DT), Cos: T.Tensor((L, half), ACC), Sin: T.Tensor((L, half), ACC)):
|
| 127 |
+
with T.Kernel(T.ceildiv(M, bm), threads=threads) as bx:
|
| 128 |
+
for i, c in T.Parallel(bm, W // 2):
|
| 129 |
+
r = bx * bm + i
|
| 130 |
+
pos = r % L
|
| 131 |
+
hh = c // half # which (q|k, head)
|
| 132 |
+
d = c % half
|
| 133 |
+
c0 = hh * Dh + d
|
| 134 |
+
c1 = c0 + half
|
| 135 |
+
x0 = T.cast(QKV[r, c0], ACC)
|
| 136 |
+
x1 = T.cast(QKV[r, c1], ACC)
|
| 137 |
+
cs = Cos[pos, d]
|
| 138 |
+
sn = Sin[pos, d]
|
| 139 |
+
QKV[r, c0] = T.cast(x0 * cs - x1 * sn, DT)
|
| 140 |
+
QKV[r, c1] = T.cast(x1 * cs + x0 * sn, DT)
|
| 141 |
+
return main
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ----------------------------------------------------------------------------- flash attention (padding mask + sliding window)
|
| 145 |
+
@tilelang.jit(pass_configs=FAST)
|
| 146 |
+
def attn_kernel(B, L, H, Dh, window=0, bm=64, bn=64, stages=1, threads=128):
|
| 147 |
+
"""QKV: [B, L, 3, H, Dh] bf16 (a view of the packed [M, 3*H*Dh] buffer). Lens: [B] int32 valid length.
|
| 148 |
+
O: [B, L, H*Dh]. window>0 => bidirectional sliding window |i-j| <= window. Masked scores use a large
|
| 149 |
+
finite negative so fully-masked (padding) rows stay finite.
|
| 150 |
+
|
| 151 |
+
B and/or L may be None: they then become runtime symbols (one compile serves every shape, at the
|
| 152 |
+
cost of predicated loads -- ~4x slower for full attention at L=1024, free for short inputs)."""
|
| 153 |
+
scale = (1.0 / Dh) ** 0.5 * 1.44269504 # log2(e)
|
| 154 |
+
if B is None:
|
| 155 |
+
B = T.dynamic("B")
|
| 156 |
+
if L is None:
|
| 157 |
+
L = T.dynamic("L")
|
| 158 |
+
NEG = -1e9
|
| 159 |
+
|
| 160 |
+
@T.prim_func
|
| 161 |
+
def main(QKV: T.Tensor((B, L, 3, H, Dh), DT), Lens: T.Tensor((B,), "int32"), O: T.Tensor((B, L, H * Dh), DT)):
|
| 162 |
+
with T.Kernel(T.ceildiv(L, bm), H, B, threads=threads) as (bx, by, bz):
|
| 163 |
+
Q_s = T.alloc_shared((bm, Dh), DT)
|
| 164 |
+
K_s = T.alloc_shared((bn, Dh), DT)
|
| 165 |
+
V_s = T.alloc_shared((bn, Dh), DT)
|
| 166 |
+
O_s = T.alloc_shared((bm, Dh), DT)
|
| 167 |
+
s = T.alloc_fragment((bm, bn), ACC)
|
| 168 |
+
s_c = T.alloc_fragment((bm, bn), DT)
|
| 169 |
+
o = T.alloc_fragment((bm, Dh), ACC)
|
| 170 |
+
m = T.alloc_fragment((bm,), ACC)
|
| 171 |
+
m_prev = T.alloc_fragment((bm,), ACC)
|
| 172 |
+
sc = T.alloc_fragment((bm,), ACC)
|
| 173 |
+
rs = T.alloc_fragment((bm,), ACC)
|
| 174 |
+
l = T.alloc_fragment((bm,), ACC)
|
| 175 |
+
T.annotate_layout({Q_s: tilelang.layout.make_swizzled_layout(Q_s)})
|
| 176 |
+
T.copy(QKV[bz, bx * bm:(bx + 1) * bm, 0, by, :], Q_s)
|
| 177 |
+
T.fill(o, 0); T.fill(l, 0); T.fill(m, NEG)
|
| 178 |
+
n = Lens[bz]
|
| 179 |
+
if window > 0:
|
| 180 |
+
k_lo = T.max(0, (bx * bm - window) // bn)
|
| 181 |
+
k_hi = T.min(T.ceildiv(L, bn), T.ceildiv(T.min(n, (bx + 1) * bm + window), bn))
|
| 182 |
+
else:
|
| 183 |
+
k_lo = 0
|
| 184 |
+
k_hi = T.ceildiv(n, bn)
|
| 185 |
+
for k in T.Pipelined(k_lo, k_hi, num_stages=stages):
|
| 186 |
+
T.copy(QKV[bz, k * bn:(k + 1) * bn, 1, by, :], K_s)
|
| 187 |
+
for i, j in T.Parallel(bm, bn):
|
| 188 |
+
qi = bx * bm + i
|
| 189 |
+
kj = k * bn + j
|
| 190 |
+
if window > 0:
|
| 191 |
+
ok = (kj < n) & (qi - kj <= window) & (kj - qi <= window)
|
| 192 |
+
else:
|
| 193 |
+
ok = kj < n
|
| 194 |
+
s[i, j] = T.if_then_else(ok, 0.0, NEG)
|
| 195 |
+
T.gemm(Q_s, K_s, s, transpose_B=True, policy=T.GemmWarpPolicy.FullRow)
|
| 196 |
+
T.copy(QKV[bz, k * bn:(k + 1) * bn, 2, by, :], V_s)
|
| 197 |
+
T.copy(m, m_prev)
|
| 198 |
+
T.reduce_max(s, m, dim=1, clear=False)
|
| 199 |
+
for i in T.Parallel(bm):
|
| 200 |
+
sc[i] = T.exp2(m_prev[i] * scale - m[i] * scale)
|
| 201 |
+
for i, j in T.Parallel(bm, bn):
|
| 202 |
+
s[i, j] = T.exp2(s[i, j] * scale - m[i] * scale)
|
| 203 |
+
T.reduce_sum(s, rs, dim=1)
|
| 204 |
+
for i in T.Parallel(bm):
|
| 205 |
+
l[i] = l[i] * sc[i] + rs[i]
|
| 206 |
+
T.copy(s, s_c)
|
| 207 |
+
for i, j in T.Parallel(bm, Dh):
|
| 208 |
+
o[i, j] = o[i, j] * sc[i]
|
| 209 |
+
T.gemm(s_c, V_s, o, policy=T.GemmWarpPolicy.FullRow)
|
| 210 |
+
for i, j in T.Parallel(bm, Dh):
|
| 211 |
+
o[i, j] = o[i, j] / T.max(l[i], 1e-30)
|
| 212 |
+
T.copy(o, O_s)
|
| 213 |
+
T.copy(O_s, O[bz, bx * bm:(bx + 1) * bm, by * Dh:(by + 1) * Dh])
|
| 214 |
+
return main
|
results/fast_english_rtx4070.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "convaiinnovations/laya",
|
| 3 |
+
"subfolder": null,
|
| 4 |
+
"gpu": "NVIDIA GeForce RTX 4070 Ti SUPER",
|
| 5 |
+
"torch": "2.11.0+cu130",
|
| 6 |
+
"latency": [
|
| 7 |
+
{
|
| 8 |
+
"case": "short x1",
|
| 9 |
+
"L": 72,
|
| 10 |
+
"stock_ms": 17.02346319998469,
|
| 11 |
+
"fast_ms": 4.294897766643166,
|
| 12 |
+
"dp_stock": 1.704692840576172e-05,
|
| 13 |
+
"dp_fast": 0.00010764598846435547,
|
| 14 |
+
"agree": 1.0,
|
| 15 |
+
"e2e_stock_ms": 17.660162200021052,
|
| 16 |
+
"e2e_fast_ms": 4.639072833318399
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"case": "short x3",
|
| 20 |
+
"L": 72,
|
| 21 |
+
"stock_ms": 18.0833204666366,
|
| 22 |
+
"fast_ms": 5.4828341333632125,
|
| 23 |
+
"dp_stock": 0.013189524412155151,
|
| 24 |
+
"dp_fast": 0.004427850246429443,
|
| 25 |
+
"agree": 1.0,
|
| 26 |
+
"e2e_stock_ms": 18.868332833335444,
|
| 27 |
+
"e2e_fast_ms": 6.6060417999930605
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"case": "short x30",
|
| 31 |
+
"L": 72,
|
| 32 |
+
"stock_ms": 35.470120733346754,
|
| 33 |
+
"fast_ms": 28.24384649999653,
|
| 34 |
+
"dp_stock": 0.0020374655723571777,
|
| 35 |
+
"dp_fast": 0.00442880392074585,
|
| 36 |
+
"agree": 1.0,
|
| 37 |
+
"e2e_stock_ms": 43.21606490005555,
|
| 38 |
+
"e2e_fast_ms": 35.695823633310894
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"case": "long x3",
|
| 42 |
+
"L": 512,
|
| 43 |
+
"stock_ms": 27.119640500010668,
|
| 44 |
+
"fast_ms": 23.355661833375052,
|
| 45 |
+
"dp_stock": 0.004268214106559753,
|
| 46 |
+
"dp_fast": 0.0036762356758117676,
|
| 47 |
+
"agree": 1.0,
|
| 48 |
+
"e2e_stock_ms": 31.515020933329655,
|
| 49 |
+
"e2e_fast_ms": 27.594624333323736
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"case": "long x30",
|
| 53 |
+
"L": 512,
|
| 54 |
+
"stock_ms": 287.79868293337734,
|
| 55 |
+
"fast_ms": 187.11691290003122,
|
| 56 |
+
"dp_stock": 0.0016844123601913452,
|
| 57 |
+
"dp_fast": 0.003676295280456543,
|
| 58 |
+
"agree": 1.0,
|
| 59 |
+
"e2e_stock_ms": 327.53421153332357,
|
| 60 |
+
"e2e_fast_ms": 232.13719066661724
|
| 61 |
+
}
|
| 62 |
+
],
|
| 63 |
+
"eval": {
|
| 64 |
+
"ag_news": {
|
| 65 |
+
"n": 1000,
|
| 66 |
+
"stock": {
|
| 67 |
+
"acc": 0.922,
|
| 68 |
+
"ece": 0.03231210000000012,
|
| 69 |
+
"ms": 19.09175434700046
|
| 70 |
+
},
|
| 71 |
+
"fast": {
|
| 72 |
+
"acc": 0.925,
|
| 73 |
+
"ece": 0.03202170000000001,
|
| 74 |
+
"ms": 14.999506231999476
|
| 75 |
+
},
|
| 76 |
+
"agreement": 0.995
|
| 77 |
+
},
|
| 78 |
+
"emotion": {
|
| 79 |
+
"n": 1000,
|
| 80 |
+
"stock": {
|
| 81 |
+
"acc": 0.588,
|
| 82 |
+
"ece": 0.3144150999999999,
|
| 83 |
+
"ms": 17.40303504700023
|
| 84 |
+
},
|
| 85 |
+
"fast": {
|
| 86 |
+
"acc": 0.588,
|
| 87 |
+
"ece": 0.3146924999999999,
|
| 88 |
+
"ms": 4.41586683299829
|
| 89 |
+
},
|
| 90 |
+
"agreement": 0.996
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
}
|