Files
auto-antislop/DGX_SPARK_SETUP.md
Joey Grasty cbd88aa758 Correct FTPO slowness diagnosis: fixed-length padding, not compute-bound
Measured actual FTPO training context lengths (real tokenizer, all 12,000
examples): mean 530 tokens, p99 1080, max 1126 -- against a configured
finetune_max_seq_length of 4000. ftpo_trainer.py's collator pads every batch
to that fixed length rather than to the longest sequence in the batch, so
every forward pass was processing ~4000 tokens of mostly padding (~13%
utilization on average). This also explains why batch_size 1->4 had no
effect: total padded-token compute is invariant to the batch/accum split.

Lowered finetune_max_seq_length to 1280 (covers p99 with headroom, nothing
in the dataset gets truncated) -- should cut per-step compute roughly 3x.
Updated DGX_SPARK_SETUP.md §7 with the measurement and corrected takeaway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 09:57:03 -05:00

245 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Running auto-antislop on a DGX Spark
Notes from getting [auto-antislop](https://github.com/sam-paech/auto-antislop) working end-to-end
(generation → slop analysis → FTPO fine-tuning) on an NVIDIA DGX Spark. The upstream quickstart
assumes a conventional x86_64 box with a discrete-VRAM GPU; the Spark's aarch64 CPU, Blackwell
(GB10) GPU, and **unified** CPU/GPU memory break several of its assumptions. This doc covers what
to change and why.
## Hardware/platform recap
- CPU/GPU: aarch64, NVIDIA GB10 (Blackwell, compute capability sm_121)
- Memory: 121GB **unified** — CPU and GPU draw from the same pool, unlike a normal discrete GPU
- Driver: reports CUDA 13.0 as the max supported runtime; system `nvcc` is also 13.0
The unified memory is the single biggest thing to keep in mind — anywhere upstream defaults assume
"GPU memory" is separate from "the OS's memory," that assumption is wrong here.
## 1. Two conda environments, not one
`vllm` 0.26.0 hard-pins `transformers>=5.5.3`. `unsloth` (needed for FTPO fine-tuning) caps it at
`<=5.5.0`. There is no single transformers version that satisfies both.
The fix: since `main.py` launches vLLM as a **subprocess** (`vllm serve ...` via `PATH` lookup, not
an in-process import — see `utils/vllm_manager.py`), it doesn't actually need to share an
interpreter with unsloth/transformers at all. Give it its own env:
```bash
conda create -n antislop python=3.11 -y
conda activate antislop
pip install "torch==2.11.0" # see §2 re: which index
pip install -r requirements.txt # minus flash-attn, see §3
# transformers ends up needing to be pinned to <=5.5.0 for unsloth — see below
conda create -n antislop-vllm-serve python=3.11 -y
conda activate antislop-vllm-serve
pip install vllm # pulls its own compatible torch/transformers
```
Then expose *only* the `vllm` binary from the second env on `PATH`, without shadowing `python`:
```bash
mkdir -p ~/.local/bin
cat > ~/.local/bin/vllm << 'EOF'
#!/bin/bash
exec /home/<user>/miniconda3/envs/antislop-vllm-serve/bin/vllm "$@"
EOF
chmod +x ~/.local/bin/vllm
```
Make sure `~/.local/bin` is on `PATH`. From inside the `antislop` env, `which vllm` should resolve
to the wrapper while `which python` stays in `antislop`.
In the `antislop` env, after `pip install -r requirements.txt` pulls in `unsloth`/`trl`/etc., pin
transformers back down:
```bash
pip install "transformers<=5.5.0,>=4.51.3"
```
## 2. Match torch's CUDA build to the *system* CUDA toolkit
`pip install torch==2.11.0 --index-url https://download.pytorch.org/whl/cu128` installs a CUDA
12.8 build. The Spark's system `nvcc` (used later to build flash-attn from source) is CUDA 13.0.
Building any CUDA extension against a mismatched torch build fails immediately with:
```
RuntimeError: The detected CUDA version (13.0) mismatches the version that was used to compile
PyTorch (12.8). Please make sure to use the same CUDA versions.
```
Fix: install torch from **plain PyPI** (no `--index-url` override) — for aarch64 it resolves to a
cu130 build that matches the system toolkit:
```bash
pip install "torch==2.11.0" "torchvision==0.26.0" --force-reinstall
```
Also watch for `torchvision` being pulled by a downstream dependency (e.g. installing `vllm` into
the wrong env) with a different CUDA tag than `torch` — same mismatch error, same fix (reinstall
matching versions from the same index).
## 3. Build flash-attn from source, restricted to one arch, low parallelism
There's no aarch64 wheel for `flash-attn` on PyPI — it always builds from source here.
- **Restrict architectures.** flash-attn's default build targets `sm_80;90;100;120` — four separate
kernel variants. GB10 is `sm_121`, which runs fine against `sm_120` binaries (Blackwell
family-compatible), so there's no reason to build the other three:
```bash
FLASH_ATTN_CUDA_ARCHS=120 MAX_JOBS=6 pip install -U --no-build-isolation \
"git+https://github.com/Dao-AILab/flash-attention.git@v2.8.3.post1#egg=flash_attn"
```
- **Lower `MAX_JOBS`.** The default (one job per core — 20 here) OOM-killed the build: too many
parallel `nvcc` processes each compiling flash-attn's heavy templated CUDA kernels. `MAX_JOBS=6`
with the single-arch restriction above completed cleanly in a few minutes.
- Install `wheel ninja packaging cmake` first (`pip install -U wheel ninja packaging cmake`).
## 4. `~/.triton/cache` may be root-owned
If Triton (used by vLLM's compilation backend) was ever invoked as root on the machine before
(e.g. during initial OS/image setup), `~/.triton/cache` and its subdirectories can end up owned by
`root`, which breaks any user-level process trying to JIT-compile through it:
```
PermissionError: [Errno 13] Permission denied: '/home/<user>/.triton/cache/.../tmp.pid_...'
```
Fix (needs an interactive sudo prompt, so run it yourself, not from an agent/script):
```bash
sudo chown -R $USER:$USER ~/.triton
```
## 5. Lower `vllm_gpu_memory_utilization` — this is not a normal GPU
vLLM eagerly reserves `gpu_memory_utilization × total_memory` at startup for its KV cache pool, to
avoid reallocating mid-serve. On a normal discrete GPU this is a free lunch — it only competes with
other GPU processes. On the Spark, "GPU memory" **is** system RAM, so the default `0.85` reserves
~103GB out of 121GB total, starving the OS and desktop. We saw this manifest as active swap
thrashing (`vmstat` showing nonzero `si`/`so` continuously) at 118/121GB used with 1.3GB free.
In `configs/<your-config>.yaml`:
```yaml
vllm_gpu_memory_utilization: 0.5 # ~60GB reserved -- generous for a 4B model, leaves the OS room
```
Adjust upward if you know the box is otherwise idle; 0.5 was comfortably enough for gemma-3-4b-it
serving 50 concurrent generation threads.
## 6. Code patches needed for current library versions
The repo (as of the commit we tested against) predates some of the exact library versions that
`pip install` resolves to today. Three small patches were needed — none are DGX-Spark-specific,
they'd bite on any platform once these versions are current on PyPI:
**`utils/vllm_manager.py`** — vLLM 0.26.0 removed the `--disable-log-requests` flag (replaced by an
opt-in `--enable-log-requests`, off by default already). Delete the line:
```python
"--disable-log-requests", # Cleaner logs during generation
```
**`antislop-vllm/utils/refusal_detector.py`** — passes a `reference_compile=False` kwarg to
`AutoModelForSequenceClassification.from_pretrained(...)` that current transformers doesn't
recognize. It fails *silently* (caught, logged once, falls back to a no-op sentinel for the rest of
the run) — refusal filtering just quietly does nothing unless you check the log for
`[RefusalDetector ERROR]`. Remove the `reference_compile=False,` line.
**`core/ftpo_trainer.py`** — transformers 5.5.0's Gemma3 forward pass now *requires*
`token_type_ids` during training (used to build the causal mask; Gemma3 is natively multimodal and
needs to know which tokens are image vs. text). The FTPO trainer's `compute_loss` calls the model
in three places without it — all three need `token_type_ids=torch.zeros_like(ids)` added (text-only
training, so all-zero/all-text is correct):
- the main forward pass (`outputs = model(...)`)
- the reference-model forward pass, `self.ref_model is None` branch (inside `null_ref_context()`)
- the reference-model forward pass, `self.ref_model is not None` branch
## 7. FTPO fine-tuning was slow because of fixed-length padding, not raw compute
With `finetune_batch_size: 1` / `gradient_accumulation_steps: 16`, we measured ~750 optimizer steps
at ~160-185s/step — a ~34 hour run for the full 12,000-example dataset. Bumping
`finetune_batch_size` to 4 (with `gradient_accumulation_steps` dropped to 4 to keep the same
effective batch size) made **no meaningful difference** — still ~160-175s/step.
Initial hypothesis was that this was inherent — genuinely compute-bound (two full forward passes
per micro-batch, over long sequences), not limited by batch-size/scheduling overhead. Measuring the
actual training data disproved that. `ftpo_trainer.py`'s collator pads every batch to a **fixed**
`finetune_max_seq_length` (4000 tokens) regardless of content:
```python
max_len = self.args.max_length # always 4000, never pad-to-longest-in-batch
prompt_ids = torch.full((batch_sz, max_len), pad_id, dtype=torch.long)
```
We tokenized all 12,000 training contexts with the real tokenizer to see how much of that 4000 was
actually needed:
| | tokens |
|---|---|
| mean | 529.9 |
| median | 509 |
| p90 / p99 | 953 / 1080 |
| **max across all 12,000 examples** | **1126** |
Not one example reaches even a third of the 4000-token padding target; the mean uses 13.2% of it.
Every forward pass — both the main model and the reference-model pass — was processing ~4000 tokens
of mostly padding, roughly 4-7x more than the actual content needs. This also explains why the
batch-size bump did nothing: total padded-token compute is invariant to how the effective batch of
16 gets split into micro-batches, so reshuffling batch/accum never touched the real cost. This is a
collator-design issue, not a hardware ceiling — it would waste the same proportion on any GPU.
**Fix:** lower `finetune_max_seq_length` to comfortably cover the real distribution, e.g. `1280`
(covers p99 with headroom, nothing in the dataset gets truncated) instead of `4000`. That should cut
per-step compute roughly 3x, bringing the ~34h estimate down to somewhere around ~11-12h. We left
`finetune_batch_size` at the default (`1`) since increasing it has no effect either way here.
If you need it faster still, the other lever is `finetune_max_train_examples` (fewer total steps,
less data coverage) — or just accept the runtime and let it run in the background.
## Validated results
Ran the full pipeline against `unsloth/gemma-3-4b-it` (2 iterations, 1200 prompts each):
- Iteration 0 (baseline, no bans): completed in 28m24s, `repetition_per_100k_chars` = 160
- Iteration 1 (with ban lists from iteration 0's analysis): completed in 1h31m43s (slower — active
backtracking around bans), `repetition_per_100k_chars` = 56 — a real, measured reduction in slop
- FTPO training: confirmed working end-to-end (750 steps, 12,000 preference pairs) after the
patches in §6; not run to completion due to the ~34h runtime (§7)
## Quick-reference: full env setup
```bash
git clone --recurse-submodules https://github.com/sam-paech/auto-antislop.git
cd auto-antislop
conda create -n antislop python=3.11 -y
conda activate antislop
pip install "torch==2.11.0" "torchvision==0.26.0" # plain PyPI, matches system CUDA 13.0
grep -v '^flash-attn$' requirements.txt > /tmp/reqs_no_fa.txt
pip install -r /tmp/reqs_no_fa.txt
pip install "transformers<=5.5.0,>=4.51.3" # re-pin down for unsloth
pip install -U wheel ninja packaging cmake
FLASH_ATTN_CUDA_ARCHS=120 MAX_JOBS=6 pip install -U --no-build-isolation \
"git+https://github.com/Dao-AILab/flash-attention.git@v2.8.3.post1#egg=flash_attn"
conda create -n antislop-vllm-serve python=3.11 -y
conda activate antislop-vllm-serve
pip install vllm
mkdir -p ~/.local/bin
printf '#!/bin/bash\nexec %s/miniconda3/envs/antislop-vllm-serve/bin/vllm "$@"\n' "$HOME" \
> ~/.local/bin/vllm
chmod +x ~/.local/bin/vllm
# ensure ~/.local/bin is on PATH
sudo chown -R $USER:$USER ~/.triton # only if it's root-owned
# apply the 3 code patches from §6, then edit vllm_gpu_memory_utilization down to ~0.5
# in whichever configs/*.yaml you're running
conda activate antislop
python main.py -c configs/gemma-3-4b-it.yaml
```