项目文件夹

文件
wehub-resource-sync 542cfa195c
CI / Frontend build (push) Failing after 9m6s
CI / Plugin validate (push) Failing after 9m27s
CI / Python lint (push) Failing after 16m1s
CI / Tests (push) Successful in 18m0s
Deploy / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:27 +08:00

198 行
7.4 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env python3
"""Push top-6-retrieval multi-image (3-image) LoRA adapters to HuggingFace.
Uploads adapter files + tokenizer + minimal metadata (no optimizer state,
no RNG, no DeepSpeed raw).
Usage:
HF_TOKEN=$(cat ~/.cache/huggingface/token) python sft/push_multi3_to_hf.py
"""
import os
import shutil
import tempfile
from pathlib import Path
from huggingface_hub import create_repo, upload_folder
TOKEN = (
os.environ.get("HF_TOKEN")
or open(os.path.expanduser("~/.cache/huggingface/token")).read().strip()
)
USER = "Chrisyichuan"
# Each entry: (compression label, adapter dir, LLM-judge score, config summary, epoch/step note)
BEST = [
(
"2x",
"/scratch/users/zwcolin/cxr_embeds/sft_output/qwen3vl_top6_2x_v1",
0.954,
"r=256, 2ep, lr 1e-5, LLM+ViT LoRA, cutoff_len 5120, eff-batch 32 on 8× H100",
"step 6502 (2 epochs)",
),
(
"3x",
"/scratch/users/zwcolin/cxr_embeds/sft_output/qwen3vl_top6_3x_v1",
0.900,
"r=256, 2ep, lr 1e-5, LLM+ViT LoRA, cutoff_len 4096, eff-batch 32 on 8× H100",
"step 6502 (2 epochs)",
),
(
"4x",
"/scratch/users/zwcolin/cxr_embeds/sft_output/qwen3vl_top6_4x_v1",
0.868,
"r=256, 2ep, lr 1e-5, LLM+ViT LoRA, cutoff_len 3072, eff-batch 32 on 8× H100",
"step 6502 (2 epochs)",
),
]
# Multi-image baselines (same 500 test set, GPT-4.1 judge, 3 images per sample, gold always in the 3)
BASE_MULTIIMAGE_0X = 0.892 # base Qwen3-VL-4B on uncompressed top-3
SINGLE_IMAGE_0X_BASELINE = (
0.958 # base Qwen3-VL-4B on uncompressed single image (from RESULTS.md)
)
KEEP_FILES = {
"adapter_config.json",
"adapter_model.safetensors",
"added_tokens.json",
"chat_template.jinja",
"merges.txt",
"preprocessor_config.json",
"special_tokens_map.json",
"tokenizer.json",
"tokenizer_config.json",
"training_args.bin",
"trainer_state.json",
"video_preprocessor_config.json",
"vocab.json",
}
def build_readme(comp, judge, config, step_note):
n_ratio = int(comp.rstrip("x"))
return f"""---
license: apache-2.0
library_name: peft
base_model: Qwen/Qwen3-VL-4B-Instruct
tags:
- peft
- lora
- qwen3-vl
- screenshot-qa
- multi-image
- retrieval-augmented
- compressed-images
- {comp}-compression
pipeline_tag: image-text-to-text
---
# Qwen3-VL-4B Multi-Image Wikipedia Screenshot QA LoRA — {comp} compression (top-3 retrieval)
LoRA adapter for [Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct) fine-tuned as the **reader** in a retrieval-augmented QA pipeline: given a question and **3 candidate Wikipedia screenshots** (one of which contains the answer, two are hard distractors), the model must locate the right image and extract the answer — all under **{comp} pixel compression**.
## Performance (GPT-4.1 LLM-judge on 500 test examples)
| Setup | LLM-judge |
|---|---|
| Uncompressed (0x) single-image, base Qwen3-VL-4B | {SINGLE_IMAGE_0X_BASELINE:.3f} |
| Uncompressed (0x) **multi-image (3)**, base Qwen3-VL-4B | {BASE_MULTIIMAGE_0X:.3f} |
| **This adapter @ {comp} multi-image (3)** | **{judge:.3f}** |
**Key observation:** {"The 2x multi-image SFT model nearly recovers the single-image uncompressed ceiling (0.958), and clearly exceeds the un-SFTed multi-image base (0.892) — fine-tuning compensates for both distractor confusion and 2x compression." if comp == "2x" else "Despite 3x pixel compression, this model slightly exceeds the un-SFTed multi-image uncompressed base (0.892) — fine-tuning compensates for both distractor confusion and 3x compression." if comp == "3x" else "At 4x compression the per-image pixel budget is ~¼ the original. Even with SFT, accuracy falls slightly below the un-SFTed multi-image 0x base (0.892). This checkpoint documents the compression–quality frontier at the extreme end; use 2x/3x for production."}
Gain over multi-image base @ 0x: **{judge - BASE_MULTIIMAGE_0X:+.3f}** ({100 * (judge - BASE_MULTIIMAGE_0X) / BASE_MULTIIMAGE_0X:+.1f}% relative).
## Training setup
- Method: LoRA ({config})
- Base model: `Qwen/Qwen3-VL-4B-Instruct`
- Framework: [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory) (fork)
- Hardware: 8× H100 80GB, DeepSpeed ZeRO-2, bf16
- Checkpoint: {step_note}
## Data — retrieval-augmented multi-image
Training data was built from the [Chrisyichuan screenshot-training-natural-filtered-v2](https://huggingface.co/datasets/Chrisyichuan/screenshot-training-natural-filtered-v2) QA dataset:
1. For each query, retrieve top-6 screenshots from a Qwen3-VL-2B embedding index (dora-ls005 checkpoint) over 28M Wikipedia tiles.
2. Construct a 3-image set: **always include the gold** + up to 2 non-gold retrieved distractors.
3. Randomize gold position among the 3 to avoid positional shortcuts.
4. Apply **{comp} compression** (each dimension scaled by `1/sqrt({n_ratio})` via PIL LANCZOS).
5. Train the reader to answer the query given the 3 compressed images.
~104k training examples, 5.8k validation, 5.8k test. Gold-retrieval rate at top-6 across splits: ~75%.
## Usage
```python
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
from peft import PeftModel
import torch
base = "Qwen/Qwen3-VL-4B-Instruct"
adapter = "{USER}/qwen3vl-4b-wiki-screenshot-multi3-{comp}-lora"
model = Qwen3VLForConditionalGeneration.from_pretrained(base, torch_dtype=torch.bfloat16).cuda()
model = PeftModel.from_pretrained(model, adapter).merge_and_unload()
processor = AutoProcessor.from_pretrained(base)
# Three {comp}-compressed images (one gold + two distractors, any order)
messages = [{{"role": "user", "content": [
{{"type": "image", "image": img1}},
{{"type": "image", "image": img2}},
{{"type": "image", "image": img3}},
{{"type": "text", "text": your_question}},
]}}]
# ... standard Qwen3-VL inference
```
## Notes / limitations
- Training distribution **always includes the gold** in the 3-image set (by construction). If your retriever misses the gold, the model has not seen that distribution — expect degradation on those queries.
- Compression level is fixed at {comp}. Use the adapter that matches your deployment pixel budget.
- Sister adapters at other compression levels: {", ".join(f"`{USER}/qwen3vl-4b-wiki-screenshot-multi3-{x}-lora`" for x in ["2x", "3x", "4x"] if x != comp)}.
"""
def push_one(comp, adapter_dir, judge, config, step_note):
src = Path(adapter_dir)
assert src.exists(), f"missing {src}"
repo_id = f"{USER}/qwen3vl-4b-wiki-screenshot-multi3-{comp}-lora"
print(f"\n=== {repo_id} ===")
print(f" src: {src}")
print(f" judge: {judge}")
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
for name in KEEP_FILES:
p = src / name
if p.exists():
shutil.copy2(p, tmp / name)
print(f" + {name}")
else:
print(f" - {name} (not present, skipped)")
(tmp / "README.md").write_text(build_readme(comp, judge, config, step_note))
print(" + README.md")
create_repo(repo_id, token=TOKEN, exist_ok=True, private=False)
upload_folder(
folder_path=str(tmp),
repo_id=repo_id,
token=TOKEN,
commit_message=f"Upload multi-image {comp} LoRA (LLM-judge {judge})",
)
print(f" uploaded → https://huggingface.co/{repo_id}")
def main():
for args in BEST:
push_one(*args)
print("\nAll done.")
if __name__ == "__main__":
main()