项目文件夹

文件
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

485 行
21 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# CLAUDE.md
> ⚠️ **Dev notes may be out of date.** `docs/training_dev_notes.md` (and some
> references in this file) describe historical internals and may have drifted
> from the current code — e.g. they mention `train_colpali.py` and an old
> `training.*` package/trainer stack (`train.py`, `evaluate.py`, `dataset.py`,
> `model.py`) that are superseded by the self-contained `train_contrastors.py`.
> Treat the docs as background; trust the code and `README.md` as source of truth.
## Pinned Versions
All training and eval **must** use these exact versions — mismatches cause silent numerical divergence:
| Package | Version |
|---------|---------|
| PyTorch | **2.9.1+cu129** |
| cuDNN | **92000** |
| transformers | **4.57.1** |
`uv sync` will install the correct versions from `pyproject.toml` + lockfile.
cuDNN 9.20 is forced via `override-dependencies` in `pyproject.toml` (torch 2.9.1 ships with 9.10, but we need 9.20 for native bf16 Conv3d).
**Always use `uv run` to ensure the locked environment is used.**
## Training Pipeline (Best Config)
```bash
# 0. Install
uv sync
# 1. Mine hard negatives (requires search API at localhost:30888)
uv run python mine_hard_negatives.py \
--input training/data/train.jsonl \
--output training/data/train_hn.jsonl \
--num-negatives 7 --n-docs 50 \
--filter-mode margin --margin 0.95
# 2. Train (best config)
CUDA_VISIBLE_DEVICES=1,2 uv run torchrun --nproc_per_node=2 train_contrastors.py \
--data-split-dir training/data/lite-query-v2-full-filtered-hn-v2-chunks/split \
--max-steps 50 \
--batch-size 16 \
--grad-cache-chunk 4 \
--num-hard-negatives 5 \
--lr 1e-5 \
--warmup-steps 20 \
--test-eval-steps 50 \
--test-max-pairs 0 \
--eval-steps 25 \
--save-steps 50 \
--output-dir training/output_nvme/output_hn_best
# 3. Eval checkpoint on hard-mini-v6 (default) or v7
cd /home/user/Vis-RAG/agent && \
CUDA_VISIBLE_DEVICES=4 uv run python scripts/eval_lora_checkpoint.py \
/home/user/wiki-screenshot-training/training/output_nvme/output_hn_best/checkpoint-50 \
--tiles-dir tiles-hard-mini-v6
# Eval on hard-mini-v7 (400 queries, 7723 tiles — more comprehensive)
# ⚠️ v7 has known issues — prefer v6 for now
cd /home/user/Vis-RAG/agent && \
CUDA_VISIBLE_DEVICES=4 uv run python scripts/eval_lora_checkpoint.py \
/home/user/wiki-screenshot-training/training/output_nvme/output_hn_best/checkpoint-50 \
--tiles-dir tiles-hard-mini-v7 \
--vllm-url http://localhost:8201/v1 \
--vllm-model Qwen/Qwen3-VL-4B-Instruct
# Retrieval-only (skip QA, faster)
cd /home/user/Vis-RAG/agent && \
CUDA_VISIBLE_DEVICES=4 uv run python scripts/eval_lora_checkpoint.py \
/path/to/checkpoint --tiles-dir tiles-hard-mini-v7 --retrieval-only
# Resume from checkpoint
uv run python train_contrastors.py --resume training/output_nvme/output_hn_best/checkpoint-50
```
## Query-Side Tune
`train_contrastors.py` also supports a query-only fine-tuning mode:
- `--mode query-side-tune` trains the query tower only
- the doc/image tower stays frozen at the base model
- datastore embeddings therefore stay valid across checkpoints
- checkpoint eval can query the full external datastore directly via `wiki-screenshot` search API
Recommended command:
```bash
OPENAI_API_KEY=<your-key> \
OPENAI_BASE_URL=https://us.api.openai.com/v1 \
CUDA_VISIBLE_DEVICES=2,3,4,5 uv run torchrun --master_port 29531 --nproc_per_node=4 train_contrastors.py \
--mode query-side-tune \
--query-side-backward direct \
--data-split-dir training/data/lite-query-v2-full-filtered-hn-v2-chunks/split \
--batch-size 64 \
--num-hard-negatives 2 \
--lr 2e-6 \
--warmup-steps 20 \
--eval-steps 25 \
--test-eval-steps 50 \
--save-steps 100 \
--max-steps 100 \
--max-num-visual-tokens 1024 \
--search-api-url http://localhost:30888 \
--simpleqa-max-examples 100 \
--vllm-url http://localhost:8201/v1 \
--vllm-model Qwen/Qwen3-VL-4B-Instruct \
--output-dir training/output_nvme/output_query_side
```
Required services:
- **search API** on `:30888` — for retrieval eval (recall@1/3)
- **vLLM** on `:8201` — Qwen3-VL-4B-Instruct for VQA answering
- **OpenAI API** — GPT-4.1 as SimpleQA grader (needs `OPENAI_API_KEY`)
Important defaults / caveats:
- In `query-side-tune`, default `--query-side-backward` is `direct` (no GradCache replay).
- This is intentional: `direct` was stable in 4-GPU real-data smoke tests.
- `--query-side-backward gradcache` is currently experimental for query-side mode and may hang.
- `standard` mode is unchanged and still uses the original GradCache path.
- Query-side retrieval eval requires a **new enough** `wiki-screenshot` `search_api.py` that accepts pre-computed `embedding` queries.
## Training Scripts
| Script | Description |
|--------|-------------|
| `train_contrastors.py` | **Primary.** Standard mode uses GradCache; query-side mode freezes the doc tower and defaults to direct backward. |
| `train_swift.py` | ms-swift alternative. Simpler (one `sft_main()` call) but no GradCache / learnable temp. |
| `train_colpali.py` | Legacy HF Trainer-based. Simpler but no GradCache. |
Key args for `train_contrastors.py`:
| Arg | Default | Notes |
|-----|---------|-------|
| `--mode` | `standard` | `standard` = shared tower training; `query-side-tune` = train query side only |
| `--query-side-backward` | `direct` | Only used in `query-side-tune`; `direct` is stable default |
| `--batch-size` | 4 | Per-GPU. Effective negatives = batch × num_gpus |
| `--grad-cache-chunk` | 2 | Memory ∝ this, not batch size |
| `--lr` | 2e-5 | Cosine schedule with warmup |
| `--warmup-steps` | 50 | Linear warmup |
| `--temperature` | 0.07 | Initial temp (learnable via LogitScale) |
| `--num-hard-negatives` | 0 | Set 2–7 when using `train_hn.jsonl` |
| `--lora-r` / `--lora-alpha` | 32 / 32 | LoRA rank and alpha |
| `--lora-vit` | off | Also apply LoRA to ViT vision encoder (see below) |
| `--max-num-visual-tokens` | 1024 | Image resolution control |
| `--max-steps` | 500 | Total training steps |
| `--eval-steps` | 100 | Validation loss frequency |
| `--test-eval-steps` | 250 | Full retrieval eval (R@1/5/10, MRR) |
| `--text-warmup-steps` | 0 | Text-only warmup steps before image training |
| `--text-data-dir` | None | Directory with text-qa-pair JSONL files |
| `--text-mix-ratio` | 0 | Fraction of text batches during image phase |
| `--text-curriculum` | off | Gradual text→image transition (50%→33%→20%→0%) |
| `--test-batch-size` | 16 | Eval batch size (lower to avoid OOM on v7/v8) |
| `--hardness-alpha` | 0 | LLaVE hardness weighting (0=off, try 5–9). Upweights harder negatives in softmax |
### LoRA Target Modules
By default, LoRA only targets the **LLM backbone** attention layers (`q_proj`, `k_proj`, `v_proj`, `o_proj`).
The ViT vision encoder is **not** tuned — its attention uses fused `qkv` naming that doesn't match.
Qwen3-VL architecture:
```
ViT (frozen by default):
model.visual.blocks.N.attn.qkv — fused QKV (Linear)
model.visual.blocks.N.attn.proj — output proj (Linear)
model.visual.blocks.N.mlp.linear_fc1 — MLP up (Linear)
model.visual.blocks.N.mlp.linear_fc2 — MLP down (Linear)
Merger (vision→LLM bridge):
model.visual.merger.linear_fc1/fc2 — not matched by --lora-vit
LLM backbone (tuned by default):
model.layers.N.self_attn.q_proj/k_proj/v_proj/o_proj
```
With `--lora-vit`, LoRA is also applied to ViT attn + MLP layers:
- Without: 224 LoRA layers, ~12.8M trainable params
- With `--lora-vit`: 416 LoRA layers, ~25M trainable params
This matches the colpali/BiQwen2 approach which uses `.*model.*` regex to target all layers including ViT.
Query-side-specific eval behavior:
- `test split`: local query embeddings are sent to `--search-api-url`, retrieve top-3, report `recall@1` / `recall@3`
- `SimpleQA`: current query tower retrieves top-3 from the same endpoint, reports article-level `recall@1` / `recall@3`, then grades answers with an OpenAI-compatible judge
## Data
### Source: LLM-Augmented Query-Document Pairs
Training pairs are generated by sending Wikipedia screenshot chunks to LLMs, which produce
natural-language queries that a user might ask to find that specific chunk.
Batch JSONL files live in `agent/scripts/contrastive/batches/`:
| Batch range | Model | Pairs | Notes |
|-------------|-------|-------|-------|
| `batch_000.jsonl``batch_057.jsonl` | Gemini 3.1 Pro | 40,402 | Higher quality, more diverse queries |
| `batch_200.jsonl``batch_257.jsonl` | Flash-Lite | 88,418 | Cheaper, noisier, larger volume |
Total: ~128,820 augmented pairs across 116 batch files.
### Data Format
Basic format (one JSON object per line):
```json
{"query": "What is the population of Tokyo?", "chunk_path": "/opt/dlami/nvme/kiwix_tiles/shard_000/shard_00042/350170.png.tiles/chunk_0000_00.png"}
```
Hard negative format (after mining):
```json
{"query": "...", "chunk_path": "...", "neg_chunk_paths": ["/path/to/neg1.png", "/path/to/neg2.png"]}
```
### Train / Val / Test Split
Split the combined batch data before training:
1. **Concatenate** all batch JSONL files into one pool
2. **Shuffle** with a fixed seed for reproducibility
3. **Split** by ratio:
- **Train**: 90% — used for contrastive fine-tuning
- **Val** (`eval.jsonl`): 5% — monitored every `--eval-steps` for loss
- **Test** (`test.jsonl`): 5% — full retrieval eval (R@1, R@5, R@10, MRR) every `--test-eval-steps`
4. **Mine hard negatives** on the train split only:
```bash
uv run python mine_hard_negatives.py \
--input training/data/train.jsonl \
--output training/data/train_hn.jsonl \
--num-negatives 7 --n-docs 20 --margin 0.95
```
Output files in `training/data/`:
- `train.jsonl` / `train_hn.jsonl` — training (with optional hard negs)
- `eval.jsonl` — validation
- `test.jsonl` — held-out test
### External Training Datasets (MOCA / MADQA)
Additional contrastive training data from MOCA and MADQA benchmarks, with pre-mined hard negatives:
| Dataset | HF Link | Rows | Images | Upload Status |
|---------|---------|------|--------|---------------|
| MOCA ColPali | [Chrisyichuan/moca-colpali-training](https://huggingface.co/datasets/Chrisyichuan/moca-colpali-training) | 118,195 | 118,195 | ✅ done |
| MOCA PixelRAG Ind | [Chrisyichuan/moca-visrag-ind-training](https://huggingface.co/datasets/Chrisyichuan/moca-visrag-ind-training) | 122,752 | 122,752 | ✅ done |
| MOCA PixelRAG Syn | [Chrisyichuan/moca-visrag-syn-training](https://huggingface.co/datasets/Chrisyichuan/moca-visrag-syn-training) | 239,206 | 239,298 | ✅ done |
| MADQA | [Chrisyichuan/madqa-training](https://huggingface.co/datasets/Chrisyichuan/madqa-training) | 1,840 | 3,598 | ✅ done |
Images are stored as **tar shards** under `image_shards/`. After downloading, extract with the included script:
```bash
# Download all four
pip install huggingface_hub
for repo in moca-colpali-training moca-visrag-ind-training moca-visrag-syn-training madqa-training; do
huggingface-cli download Chrisyichuan/$repo --repo-type dataset --local-dir /opt/dlami/nvme/external_data/$repo
done
# Extract images from tar shards
cd /opt/dlami/nvme/external_data/<dataset>
python extract_hf_image_shards.py --dataset-dir .
```
Each dataset contains a JSONL metadata file + `image_shards/` directory. After extraction, `images/` is created. Format matches the main training data:
```json
{"query": "...", "chunk_path": "images/...", "neg_chunk_paths": ["images/...", "..."], "source_dataset": "moca"}
```
Preparation scripts: `prepare_andy_datasets.py` (build HF folders from raw JSONL), `package_andy_shards.py` (tar shard packaging), `upload_andy_datasets.py` (upload to Hub).
### Generating Synthetic Data (for quick testing)
`fake_data.py` generates template-based queries from article titles (not LLM-augmented):
```bash
uv run python fake_data.py \
--tiles-dir /opt/dlami/nvme/kiwix_tiles \
--articles-json /opt/dlami/nvme/kiwix/wikipedia_en_all_maxi_2025-08.zim.articles.json \
--output-dir training/data --num-articles 1000
```
## Evaluation
```bash
# Verify fine-tuned vs base embeddings
CUDA_VISIBLE_DEVICES=0 uv run python verify_embeddings.py \
--adapter training/output_nvme/output_contrastors/checkpoint-500 --max-pairs 100
# Gradient correctness tests
CUDA_VISIBLE_DEVICES=0 uv run python tests/test_grad_equivalence.py
CUDA_VISIBLE_DEVICES=0,1 uv run torchrun --nproc_per_node=2 tests/test_grad_multi_gpu.py
```
## Checkpoint Hygiene
Checkpoints produced during training quickly fill up the disk. **Periodically clean up checkpoints that are not promising:**
- After each new checkpoint is produced, check whether `training/output_nvme/` contains old checkpoints that are clearly unneeded (e.g. intermediate versions where loss did not drop or R@1 did not improve).
- If you are sure they are not promising, delete them directly.
- If you are unsure whether to delete, ask the user before acting.
## Launching Training
> **⚠️ All training jobs must be launched inside a tmux session!**
> Running training in a bare terminal = everything is lost when the SSH connection drops. We have already lost a training run once because of this. **Never run torchrun directly in a bare SSH terminal.**
```bash
# Correct approach: launch inside tmux
tmux new-session -d -s train -c /home/ubuntu/wiki-screenshot-training
tmux send-keys -t train "CUDA_VISIBLE_DEVICES=1,2,3 uv run torchrun --nproc_per_node=3 train_contrastors.py \
[args...] 2>&1 | tee training/output_nvme/<run>/train.log" Enter
# View training output
tmux attach -t train
```
**Never pipe training output through `head`/`tail`/etc.** — SIGPIPE will kill the torchrun workers.
> **⚠️ Before launching training you must confirm that `OPENAI_API_KEY` and `OPENAI_BASE_URL` are set!**
> Without these two environment variables, the QA score of the mini-v6 eval will silently return 0 (the grader swallows all exceptions), and you will think the model is bad when in fact grading never ran.
> **If you find that the current shell has no `OPENAI_API_KEY`, you must immediately remind the user to set it and not continue launching training.**
These two variables should already be configured in `~/.zshrc`. Verify before launching:
```bash
# Mandatory check before launching training
echo "OPENAI_API_KEY=${OPENAI_API_KEY:+SET}" "OPENAI_BASE_URL=${OPENAI_BASE_URL}"
```
**This API key requires `us.api.openai.com`** — using the default `api.openai.com` will 401 silently (the grader catches all exceptions, resulting in QA score = 0 with no visible error).
```bash
export OPENAI_API_KEY="sk-proj-..."
export OPENAI_BASE_URL="https://us.api.openai.com/v1"
```
## Experiment Tracking (CSV)
Every training run must write its experiment results to a CSV file for easy side-by-side comparison.
**File location:** `training/output_nvme/<run_name>/metrics.csv`
**CSV format:**
```csv
step,eval_loss,eval_acc,recall@1,recall@3,qa_score,peak_eval_acc,peak_qa_score
0,,,0.125,0.300,0.42,0.000,0.42
50,1.05,0.62,0.138,0.325,0.45,0.62,0.45
100,0.98,0.65,0.142,0.340,0.48,0.65,0.48
```
**Metrics that must be recorded:**
- `step`: training step count
- `eval_loss`: loss on the eval split
- `eval_acc`: accuracy on the eval split
- `recall@1`, `recall@3`: retrieval recall of the test eval
- `qa_score`: QA score of the test eval (the primary optimization target)
- `peak_eval_acc`: the highest eval accuracy up to the current step
- `peak_qa_score`: the highest QA score up to the current step
**Each run must also record `training/output_nvme/<run_name>/run_config.md`:**
```markdown
# Run: <run_name>
- **Ablation**: describe what this experiment is ablating (which baseline it compares against, what variable was changed)
- **Date**: launch date
- **Machine**: machine name (e.g. colin3)
- **GPUs**: the GPU IDs and count used
- **Key args**: list all non-default parameters
- **Baseline**: name of the baseline run being compared against
- **Hypothesis**: expected effect
```
**How to read:** After training finishes, or during a mid-run check, parse step-level metrics from train.log and write them to the CSV. If the CSV already exists, append new rows.
## Key Findings
- **Hard negatives** are critical for meaningful improvement beyond baseline
- **Primary optimization target is QA score**, not recall@k. Recall can drop while QA score improves (query embeddings become more semantically useful even if exact chunk match rate falls).
## v8r Ablation Results (2026-04-22)
Full stairstep ablation on `training/data/natrual_filtered_v2/split` (350 steps, bs=64, lr=7e-6, lora-vit, visual_tokens=4096), evaluated on both miniv6 (200q, 5291 tiles) and miniv8 (400q, 7426 tiles). vLLM reader: Qwen3-VL-4B-Instruct. Grader: gpt-4.1-2025-04-14.
### Peak metrics across all eval steps
| Run | Config | v6 R@1 | v6 R@3 | **v6 QA** | v8 R@1 | v8 R@3 | **v8 QA** |
|---|---|---|---|---|---|---|---|
| base | no training | 0.650 | 0.800 | 0.665 | 0.688 | 0.833 | 0.730 |
| ab1 | + in-batch only | 0.720 | 0.840 | 0.705 | 0.750 | 0.868 | 0.750 |
| ab2 | + hard negatives | 0.715 | 0.865 | 0.735 | 0.748 | 0.878 | 0.778 |
| ab3 | + text warmup | **0.730** | 0.855 | **0.755** | 0.755 | **0.893** | **0.7825** |
| ab4 | + unfreeze ViT | **0.730** | **0.860** | **0.755** | **0.760** | 0.888 | **0.7825** |
### Final (last checkpoint, step 350) metrics
| Run | Final v6 QA | Final v8 QA | Notes |
|---|---|---|---|
| ab1 | 0.705 | 0.7225 | v8 peaked @step50 (0.750), degraded to 0.7225 — overfit (acc→1.0) |
| ab2 | 0.735 | 0.7425 | v8 peaked @step50 (0.778), degraded to 0.7425 |
| ab3 | 0.745 | 0.780 | close to peak |
| ab4 | 0.755 | 0.7825 | == peak, most stable |
### Key observations
- **Perfect QA stairstep** (peak v6: 0.665→0.705→0.735→0.755=0.755; peak v8: 0.730→0.750→0.778→0.7825=0.7825)
- **ab3 ≈ ab4 in peak** — unfreeze ViT did not add peak QA under this budget (350 steps, single GPU). But ab4 **final** is closer to peak than ab3, so ViT unfreeze adds training stability, not ceiling
- **R@1 is not monotone**: ab2 (0.715) < ab1 (0.720) on v6 — hard neg trades some R@1 for R@3 / QA
- **ab1 / ab2 overfit late**: peak QA reached @step50, then degraded. Lesson: for these configs, shorter training (or early stopping) would have landed better final numbers
### Run dirs
- `training/output_nvme/v8r_base/` (no wandb, eval-only)
- `training/output_nvme/v8r_ab1_inbatch/`
- `training/output_nvme/v8r_ab2_hn2/`
- `training/output_nvme/v8r_ab3_hn2_tw50/`
- `training/output_nvme/v8r_ab4_full/`
Tile caches live next to test images (`test_miniv6/tiles/.tile_cache_*.pt`, `test_miniv8/tiles/.tile_cache_*.pt`). ⚠️ Do not launch 4 runs in parallel from cold — they race to write the cache. Warm the cache first with one eval run, then parallel launches are safe.
## Reader SFT (LlamaFactory)
SFT training of Qwen3-VL-4B to do QA on compressed images (the "reader" model).
**Separate venv**: LlamaFactory has its own dependency environment, **do not use the main project's `.venv`**.
```bash
cd sft/LlamaFactory
source .venv/bin/activate # separate venv, already has LlamaFactory + deepspeed + wandb installed
```
Key dependencies (already installed):
- flash-linear-attention + causal-conv1d (must be installed, otherwise it silently falls back to slow attention)
- torch 2.9.1+cu129 (same as the main project, cuDNN 9.20 to work around the Conv3D fallback issue)
- deepspeed, wandb
**Data preparation** (compress images + convert to ShareGPT format):
```bash
# Run inside the main project venv
uv run python3 sft/prepare_sft_data.py \
--dataset-dir /mnt/data/hf_datasets/screenshot-training-natural-filtered-v2 \
--output-dir /mnt/data/sft_data/compressed_3x \
--compress-ratio 3 --workers 32
```
**Training** (4 GPUs, launch inside tmux):
```bash
cd sft/LlamaFactory && source .venv/bin/activate
CUDA_VISIBLE_DEVICES=1,2,3,4 \
FORCE_TORCHRUN=1 NNODES=1 NPROC_PER_NODE=4 \
llamafactory-cli train /home/ubuntu/wiki-screenshot-training/sft/train_qwen3vl_compressed.yaml
```
Key config (`sft/train_qwen3vl_compressed.yaml`):
- template: `qwen3_vl` + `enable_thinking: false` (do not use `qwen3_vl_nothink`, it is not equivalent)
- LoRA rank 32, lr 1e-5, DeepSpeed ZeRO-2
- W&B project: `llamafactory`
## vLLM Serving
**All vLLM instances must use `serving/vllm/`** — this subproject pins vLLM + transformers + torch via `uv.lock`.
Locked versions:
- vLLM 0.19.0
- transformers 4.57.6
- torch 2.10.0
```bash
cd serving/vllm
uv sync # first time only
uv run vllm serve Qwen/Qwen3-VL-4B-Instruct \
--dtype auto --port 8201 --max-model-len 65536 \
--gpu-memory-utilization 0.8 --api-key dummy
```
Never use standalone venvs or other users' venvs to serve models. The `serving/vllm/uv.lock` is the single source of truth for inference-time dependencies.
**Port discovery:** Before launching training that needs vLLM, check what's already running:
```bash
# List active vLLM endpoints
ss -tlnp | grep -E ':8[0-9]{3}\b'
# Verify model at a port
curl -s http://localhost:<port>/v1/models | python3 -m json.tool
```
Use the actual port in `--vllm-url` (e.g. `http://localhost:8200/v1`). Don't assume the default 8201 — it may be on a different port.