Download README.md from flozi00/zero-gpu-training-space: direct link, hf CLI and curl.
- Browser
- Download file 8.34 kB
-
https://huggingface.co/spaces/flozi00/zero-gpu-training-space/resolve/main/README.md
- Command line
-
hf download hf://spaces/flozi00/zero-gpu-training-space/README.md
-
curl -L -o README.md https://huggingface.co/spaces/flozi00/zero-gpu-training-space/resolve/main/README.md
A newer version of the Gradio SDK is available: 6.31.0
title: Zero-GPU Training Space
emoji: π§
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: 5.9.1
app_file: app.py
pinned: false
short_description: Train SFT/DPO/KTO one step at a time, resumable via bucket
tags:
- gradio
- zero-gpu
- trl
- training
- lora
- sft
- dpo
- kto
hardware:
accelerator: zero-gpu
cpu: 2
memory: 16GB
startup_duration_timeout: 1h
π§ Zero-GPU Training Space
Train SFT / DPO / KTO, checkpointing weights + optimizer/scheduler state to an Hugging Face Storage Bucket. The uploaded training data is never persisted β only the trained LoRA adapter and optimizer state are saved, so you can shut the Space down and resume training later without restarting the optimizer from scratch.
How a request becomes training steps
You upload a trace (a few JSON rows). The number of optimizer steps is derived from the trace, not chosen by hand:
steps = ceil(num_examples / grad_accum) (SFT / DPO, per-device batch = 1)
| Uploaded examples | GRAD_ACCUM |
Optimizer steps |
|---|---|---|
| 1 (a single trace) | 4 | 1 |
| 10 | 4 | 3 (4 + 4 + 2) |
| 5 | 4 | 2 (4 + 1) |
So a single trace trains a single step; 10 traces train 3 steps
(4 + 4 + 2). GRAD_ACCUM is set once per deployment (env). Each call
continues from the last checkpoint (max_steps = last_step + steps), so the
optimizer state is never restarted.
Queue: requests are not lost to a timeout
Requests are queued, not run inline β so a client-side timeout cannot drop them.
- Submit parses the trace and caches it in an in-RAM queue, then returns immediately (no GPU, no training). The request is safe the moment the call returns, even if the client then times out.
- Train drains the queue under
@spaces.GPU, running jobs in method-priority order: all SFT first, then all DPO, then all KTO, FIFO within a method. If 5 SFT and 6 DPO requests arrive, every SFT job runs before any DPO job starts. - Set
QUEUE_PERSIST=trueto also mirror the queue to the bucket, so queued requests survive a Space restart (the RAM queue already survives a client timeout β the Space process keeps running while a client waits).
Design: one bucket + one model per deployment
MODEL_ID and BUCKET_ID are set once via the Space's environment
variables β they are not re-entered per request. The base model is loaded
once and kept resident in RAM/VRAM across calls, so the expensive download
- load happens a single time. Only the LoRA adapter + optimizer state are re-applied per call (from the resumed checkpoint). This is far more efficient than re-loading the model on every request.
Why it works on a Zero-GPU / CPU Space
- GPU when available, CPU otherwise. The device is chosen from
torch.cuda.is_available(). On a ZeroGPU Space@spaces.GPUattaches an A10G for the call (so it trains on GPU); on a dedicated-GPU or CPU Space the decorator is a no-op and it trains on that device. - Resident model cache (persistent devices). On a dedicated-GPU or CPU Space the base model stays cached in memory across calls. On ZeroGPU the GPU is transient (detached between calls), so the model is loaded per-call from the local HF cache instead (no network re-download).
- LoRA. Only a small adapter is trained, so even a 2-core / 16 GB CPU
container is enough for a small base model (default
HuggingFaceTB/SmolLM2-135M). - Incremental. Each call advances the run by a few steps; the HF
Trainercheckpoint (adapter weights +optimizer.pt+scheduler.pt+rng+trainer_state.json) is persisted to the bucket and resumed on the next call. - No data persistence. The trace is parsed into an in-memory
datasetsobject for the call and discarded afterwards. Only weights + state are saved.
Configuration (Space environment variables / secrets)
Set these in the Space settings (or as secrets). Everything below is set once per deployment β the UI never asks for it per request.
| Variable | Meaning | Default | Example |
|---|---|---|---|
HF_TOKEN |
token used to read/write the bucket (set as a secret) | β | hf_β¦ |
MODEL_ID |
base model, one per deployment | HuggingFaceTB/SmolLM2-135M |
Qwen/Qwen2.5-27B |
BUCKET_ID |
HF Storage Bucket, one per deployment | β | flozi00/training-checkpoints |
LORA_R |
LoRA rank | 8 |
16 |
LORA_ALPHA |
LoRA alpha | 16 |
32 |
LORA_DROPOUT |
LoRA dropout | 0.0 |
0.1 |
LORA_TARGET_MODULES |
comma-separated target modules | q_proj,k_proj,v_proj,o_proj |
q_proj,v_proj |
LEARNING_RATE |
learning rate | 1e-4 |
2e-5 |
MAX_LENGTH |
max sequence length | 256 |
512 |
GRAD_ACCUM |
gradient-accumulation size (steps = ceil(n / G)) | 4 |
8 |
QUEUE_PERSIST |
mirror the queue to the bucket (survives a restart) | false |
true |
LoRA hyper-params are fixed at first load (the base model is cached with its LoRA wrapper). To change them mid-deployment, restart the Space (which re-runs the cache).
The three methods
| Method | Trainer | Trace row format |
|---|---|---|
| SFT | trl.SFTTrainer |
{"messages":[{"role":"user","content":"..."},{"role":"assistant","content":"..."}]} or {"text":"..."} |
| DPO | trl.DPOTrainer |
{"prompt":"...","chosen":"...","rejected":"..."} |
| KTO | trl.KTOTrainer |
{"prompt":"...","completion":"...","desirable":true} |
Accept a JSON array or JSONL (one object per line).
How it persists
For one experiment (base model + method) the bucket holds:
<bucket_id>/
state.json # tiny index: {global_step, method, model_id, loss}
checkpoint-<step>/
adapter_config.json
adapter_model.safetensors # LoRA weights <-- persisted
tokenizer.json / ...
trainer_state.json # global_step, LR, log <-- persisted
optimizer.pt # Adam moments <-- persisted (the key!)
scheduler.pt # LR scheduler state <-- persisted
rng.pt # RNG seed state <-- persisted
On the next call the latest checkpoint is downloaded and
trainer.train(resume_from_checkpoint=...) continues exactly where it left
off β the optimizer does not restart from scratch.
Local development
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export MODEL_ID=HuggingFaceTB/SmolLM2-135M # optional; defaults to this
export BUCKET_ID=flozi00/training-checkpoints # optional
python app.py # Gradio on http://localhost:7860
Deploy as a Space (Zero-GPU)
# 1) create the Space (zero-a10g flavor) + a bucket
hf repos create flozi00/zero-gpu-training-space --type space --sdk gradio --flavor zero-a10g
hf repos create flozi00/training-checkpoints --type bucket
# 2) push the code
hf upload flozi00/zero-gpu-training-space --repo-type space . . \
--include "*.py" --include "*.md" --include "*.txt" \
--include "examples/*.jsonl" --exclude ".venv" --exclude ".cache" --exclude "__pycache__"
# 3) set env: model + bucket (variables) and the token (a secret)
hf spaces env add flozi00/zero-gpu-training-space MODEL_ID HuggingFaceTB/SmolLM2-135M
hf spaces env add flozi00/zero-gpu-training-space BUCKET_ID flozi00/training-checkpoints
hf spaces env secret add flozi00/zero-gpu-training-space HF_TOKEN hf_...
Then open the UI: paste a trace, pick a method, click β Submit to queue (the request is cached β safe against a client timeout), then β‘ Train queue (runs all SFT, then DPO, then KTO). The number of optimizer steps is derived from the trace (see above). Each restart of the Space resumes from the latest checkpoint.
Files
app.pyβ Gradio UI (submit β queue β train, method-priority, resume)train.pyβ SFT/DPO/KTO trainer + dynamic grad-accum + resident model cache + resumejob_queue.pyβ timeout-resilient, method-priority job queue (RAM + optional bucket)store.pyβ HF Storage Bucket persistence (weights + state only)examples/β example traces for each methodrequirements.txtβ ZeroGPU-compatible pinned deps