Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .claude/skills/train-backbones/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: train-backbones
description: Launch the BoardOCR backbone sweep. Asks the user for launch mode, image size, epoch count, and optional backbone subset; auto-tunes batch_size/num_workers/prefetch_factor from detected GPU count + VRAM + CPU cores via scripts/autotune.py; then runs scripts/train_backbones.sh with the resolved env vars. Use for the "start / kick off / run the training sweep" ask on this project.
---

# train-backbones

Kicks off training of BoardOCR across one or more backbones. Handles both the
1-GPU dev box (RTX 4070 Ti) and the 8-GPU A100 target uniformly by delegating
to `scripts/train_backbones.sh` (which supports three launch modes) and letting
`scripts/autotune.py` size DataLoader/batch based on the detected hardware.

## What this skill does

1. Ask the user (via `AskUserQuestion`) for the choices they should actually
own — launch mode, image size, epochs, optionally the backbone subset. Do
NOT ask for anything that autotune can infer (workers, batch, VRAM, etc.).
2. Detect hardware and derived hyperparams by running
`uv run python scripts/autotune.py --backbone <bb> --image-size <sz> --mode <mode> [--nprocs <n>]`.
Parse its `KEY=VALUE` lines. The backbone passed to autotune should be the
*heaviest* one in the sweep, so batch_size holds for every backbone in the run.
3. Print a compact plan back to the user (mode, backbones, image_size, epochs,
resolved batch/workers) and then launch the sweep by exporting the env vars
inline with the shell invocation:
`EPOCHS=... BATCH_SIZE=... NUM_WORKERS=... PREFETCH_FACTOR=... IMAGE_SIZE=... ./scripts/train_backbones.sh`
4. Stream stdout so the user sees the sweep script's progress log.

## The three launch modes

Present them via a single `AskUserQuestion` with these labels/descriptions so
the user picks based on their goal, not implementation:

- **Sequential (1 backbone × 1 GPU)** — simplest, dev-box friendly. Runs each
backbone in turn. Env: default.
- **DDP (1 backbone × N GPUs)** — one heavy backbone, split across all GPUs
via torchrun. Env: `NPROC_PER_NODE=<ngpus>`. Useful for training a single
chosen model as fast as possible.
- **Parallel (N backbones × 1 GPU each)** — best for a full comparison sweep
on a multi-GPU node. Each backbone runs on its own GPU concurrently. Env:
`PARALLEL_GPU=1`. Waves of `NGPUS` at a time.

If autotune reports `NGPUS_DETECTED=1`, only Sequential makes sense — skip the
question and note in the plan why.

## Image size options

- **224** (default) — matches ImageNet pretraining, smallest cache (~10 GB),
fastest per epoch. Each 9x9 board cell gets ~25 px.
- **288** — ~32 px/cell, moderate cache (~17 GB), ~1.5x epoch time.
- **384** — ~42 px/cell, big cache (~31 GB), ~2.5x epoch time. Best for
distinguishing fine-detail characters like 成香/成桂.

## Epoch options

Present 30 / 50 / 100 / 200 as anchor choices. Note that first epoch of a
cold-cache run pays the preload build cost (~30-60 s for full dataset).

## Backbones

Default = all 9 (small → large): `mobilenet_v3_small mobilenet_v3_large
convnext_atto convnext_femto efficientnet_b0 convnext_pico efficientnet_b1
convnext_nano convnext_tiny`. Ask only if the user hints at a subset. Pass via
the `BACKBONES` env var, space-separated.

## VRAM cache — not implemented

The user may ask about loading the preload cache into VRAM (an A100 can hold
the whole 10 GB cache easily). This is a known followup, NOT implemented today.
Reasons:

- The current pipeline decodes preload → DRAM → CPU DataLoader workers → GPU.
- Moving the cache to VRAM only pays off if augmentation also runs on GPU,
because otherwise workers still have to copy back to CPU per sample.
- That requires porting the Albumentations chain to Kornia — a real refactor,
not a one-line change.

If asked, explain the tradeoff honestly and defer. The DRAM cache is already
near-instant on cache hit (mmap) and DataLoader isn't currently the bottleneck.

## Autotune calling convention

`scripts/autotune.py` prints eval-able env assignments. Call it once with the
*heaviest* backbone in the sweep so batch_size is safe for every backbone
(smaller ones will fit trivially).

Examples:
```bash
# Parallel mode on the 8-GPU node, all backbones:
uv run python scripts/autotune.py --backbone convnext_tiny --image-size 224 --mode parallel

# DDP mode, 8 GPUs, single backbone:
uv run python scripts/autotune.py --backbone convnext_tiny --image-size 224 --mode ddp --nprocs 8
```

## Full launch invocation

Compose the final shell command. Mode → env var mapping:
- sequential: no extra env
- ddp: `NPROC_PER_NODE=${NGPUS_DETECTED}`
- parallel: `PARALLEL_GPU=1`

Then run (adjust the trailing script name if the user wants to change it):

```bash
IMAGE_SIZE=<sz> EPOCHS=<n> BATCH_SIZE=<b> NUM_WORKERS=<w> PREFETCH_FACTOR=<p> \
[BACKBONES="<subset>"] [PARALLEL_GPU=1 | NPROC_PER_NODE=<n>] \
./scripts/train_backbones.sh
```

Show it in the plan so the user can copy/rerun without going through the
skill later.

## Things not to ask about (skill owns these)

- Learning rate — leave the script default (3e-4). If asked, mention linear
scaling for larger effective batch and that the user can pass `LR=<...>` env.
- Preload — always on (`--preload`). Cache is on-disk mmap; zero cost after
first build.
- Checkpoint dir / W&B project name — script defaults are fine.
- `RESUME_INCOMPLETE` — leave off unless the user asks to resume; not a
hyperparameter question.
6 changes: 5 additions & 1 deletion .devcontainer/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ services:
volumes:
- ../:/home/vscode/app:cached
- venv:/home/vscode/app/.venv
shm_size: 4gb
shm_size: 32gb
tty: true
stdin_open: true

volumes:
venv:
driver: local
hf-cache:
driver: local
uv-cache:
driver: local
28 changes: 25 additions & 3 deletions .devcontainer/cuda/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Keep BuildKit's apt cache mounts populated across builds: docker-clean wipes
# the archives after every install, so drop it and tell apt to keep them.
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \
> /etc/apt/apt.conf.d/keep-cache

# NVIDIA base image is minimal; install the tools required by common-utils and
# subsequent devcontainer features. Everything else (uv, zsh, gh, claude-code)
# is layered on via features in devcontainer.json.
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl git sudo openssh-client tzdata locales \
&& locale-gen en_US.UTF-8 \
&& rm -rf /var/lib/apt/lists/*
&& locale-gen en_US.UTF-8

ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

# Create the non-root vscode user the devcontainer runs as (remoteUser). The
# NVIDIA base is root-only, so unlike the devcontainers base image we make it
# here with passwordless sudo.
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \
&& echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME

USER $USERNAME
9 changes: 5 additions & 4 deletions .devcontainer/cuda/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ services:
# workspace root is two levels up.
- ../../:/home/vscode/app:cached
- venv:/home/vscode/app/.venv
# Persistent HF datasets cache so `HFCaptureDataset` doesn't re-download
# the ~21GB parquet shards on container rebuild.
- hf-cache:/home/vscode/.cache/huggingface
# Persistent per-user cache dir. Covers HF datasets (~21GB parquet
# shards for `HFCaptureDataset`), uv package cache, and anything else
# that would otherwise cost time to rebuild on container recreation.
- cache:/home/vscode/.cache
# DataLoader workers on CUDA can consume more shared memory than the
# default 64MB; 16GB matches typical CUDA training rigs.
shm_size: 16gb
Expand All @@ -29,5 +30,5 @@ services:
volumes:
venv:
driver: local
hf-cache:
cache:
driver: local
39 changes: 39 additions & 0 deletions .devcontainer/cuda/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"features": {
"ghcr.io/devcontainers-extra/features/fzf:1": {
"version": "1.0.15",
"resolved": "ghcr.io/devcontainers-extra/features/fzf@sha256:dbac92d89862c0f266453772ea0e73089d520ff66544f9c146480677fc2dbf7d",
"integrity": "sha256:dbac92d89862c0f266453772ea0e73089d520ff66544f9c146480677fc2dbf7d"
},
"ghcr.io/devcontainers/features/common-utils:2": {
"version": "2.5.9",
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
"integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a"
},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"version": "1.10.0",
"resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e",
"integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e"
},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "1.8.0",
"resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511",
"integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511"
},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {
"version": "1.0.0",
"resolved": "ghcr.io/jsburckhardt/devcontainer-features/uv@sha256:542a0bc2203205b3c696de650ba862f280b20af3543493cc232edd9ac35791f7",
"integrity": "sha256:542a0bc2203205b3c696de650ba862f280b20af3543493cc232edd9ac35791f7"
},
"ghcr.io/stu-bell/devcontainer-features/claude-code:0": {
"version": "0.1.0",
"resolved": "ghcr.io/stu-bell/devcontainer-features/claude-code@sha256:f87b4da3f8648db9111cb25c6bd3817d096018d81f3fb596340f7ffdba14409a",
"integrity": "sha256:f87b4da3f8648db9111cb25c6bd3817d096018d81f3fb596340f7ffdba14409a"
}
}
}
5 changes: 3 additions & 2 deletions .devcontainer/postCreateCommand.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh

sudo chown -R "$(whoami)":"$(whoami)" /home/"$(whoami)"/app/.venv
uv sync
sudo chown -R $(whoami):$(whoami) /home/$(whoami)/app/.venv
sudo chown -R $(whoami):$(whoami) ~/.cache
uv sync --extra experiment
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ data/
# 学習中間物
runs/
wandb/
logs/
checkpoints/
*.pt
*.pth
Expand Down
Loading
Loading