Files
freetoken/tests
Xiaoze Fan 2757bb5f91 feat(server)!: add --gpu to choose the GPU on multi-GPU machines (#117)
* feat(server)!: add --gpu to choose the GPU on multi-GPU machines

--gpu takes a GPU UUID (as nvidia-smi -L prints) or an nvidia-smi index.

It is applied as CUDA_VISIBLE_DEVICES in the parent before the workers spawn, so the engine still binds cuda:<rank>.

/v1/stats reports the engine's GPU under "gpus".

ft bench bw takes the same --gpu and writes one profile per GPU (benchbw/<gpu-uuid>.json); the legacy benchbw.json is still read by GPU name.

The daemon's /bench/profile returns the running serve's GPU profile.

BREAKING CHANGE: ft checkpoint --device is removed; use --gpu.

* refactor!: resolve --gpu via NVML and bind by UUID, not CUDA_VISIBLE_DEVICES

BREAKING CHANGE: ft bench bw --device is removed; use --gpu.

* refactor: name the id namespaces and drop the multi-device e4m3 scan

Splits the published id into _assigned_physical (UUID) and
_assigned_visible (CUDA ordinal). One process runs on one GPU, so
e4m3_native() judges that card instead of scanning every visible device.
2026-08-24 16:44:01 -07:00
..
2026-08-11 22:53:25 +00:00
2026-08-11 22:53:25 +00:00
2026-08-11 22:53:25 +00:00
2026-08-13 16:22:41 -07:00
2026-08-13 16:22:41 -07:00
2026-08-18 12:26:05 -07:00
2026-08-18 12:26:05 -07:00
2026-08-11 22:53:25 +00:00

FreeToken test suite

Directories mirror the python/freetoken/ subsystem a test primarily exercises — put a new test next to the module it protects. There is no per-model directory: a model-specific test lives in the subsystem it stresses, and says which model in its filename (kvcache/test_dsv4_pool.py, models/test_glm4_nvfp4.py).

directory subsystem under test
daemon/ freetoken.daemon — torch-free supervisor (import safety, serve manager)
server/ freetoken.server — OpenAI/Anthropic/Responses APIs, streaming, parsers, accounting, maintenance state
scheduler/ freetoken.scheduler — chunked prefill, cache manager, commit/window locking, status reporting, KV usage, cost accounting
kvcache/ freetoken.kvcache — paged pools (incl. DSV4's), rebuild, cache-cost accounting; the three prefix caches live in kvcache/radix/ behind a shared reference model (see its README)
tokenizer/ freetoken.tokenizer — tokenize/detokenize request plumbing, thinking-mode resolution
engine/ freetoken.engine — cache budget planning, config resolution, backend gating
kernels/ freetoken.kernel / freetoken.layers — attention, rope, fused ops, JIT cache, pinned memory
moe/ freetoken.moe — offload cache, CPU/GPU expert kernels, quantized backends (fp8/nvfp4/mxfp4/q4_0)
models/ freetoken.models — the registry, and the loading machinery every model shares (sharding, qkv/expert merge, streaming layers into banks)
e2e/ real-server gates: AIME 24/25/26 generation, runtime cache rebuild

The CLI surface itself is not unit-tested. ft dispatch, ft ctl, ft launch and install.sh are thin, they change often, and a test written after the fact only restates whatever the code currently does — running the command is the real gate. What does live here is the logic reachable through those commands when it has its own failure mode: server/test_parser_auto_selection.py covers the architecture -> parser inference behind --tool-call-parser auto.

Running

uv run pytest tests/                 # full suite, ~2-4 min on a GPU box
uv run pytest tests/ -m "not slow"   # skip the handful of tens-of-seconds tests
uv run pytest tests/kvcache/         # one subsystem

GPU-dependent tests skip themselves when CUDA is unavailable. Marlin NVFP4 tests skip unless vllm is importable (dedicated venv with vllm>=0.14,<0.15).

needs_weights-marked tests skip unless the env var pointing at a real local checkpoint is set:

env var used by
FREETOKEN_TEST_MODEL e2e/test_aime.py — local model directory
FREETOKEN_AIME_SERIES e2e/test_aime.pyaime24 | aime25 | aime26 (default aime25)
FREETOKEN_AIME{24,25,26}_JSONL e2e/test_aime.py — jsonl for the selected series
FREETOKEN_AIME_REQ e2e/test_aime.py — 1-based problem id, comma list (1,3,7), or all (default 1)
FREETOKEN_AIME_MAX_TOKENS e2e/test_aime.py — per-sample token budget (default 16384)
FREETOKEN_AIME_SAMPLES e2e/test_aime.py — pass@N sample count when sampling (default 3)
FREETOKEN_AIME_MIN_FREE_GIB e2e/test_aime.py — required free GPU memory (default 70)
FREETOKEN_TEST_MOE_CACHE_SIZE e2e/test_aime.py — >0 switches to the offload MoE backend with this cache size
FREETOKEN_TEST_MEM_RATIO e2e/test_aime.py — offload-mode memory_ratio (default 0.9)
FREETOKEN_REBUILD_TEST_MODEL e2e/test_cache_rebuild.py — a SMALL local model dir; boots a real server (falls back to FREETOKEN_TEST_MODEL)
FREETOKEN_GEMMA4_GGUF_GLOB models/test_gemma4_gguf_rope.py — glob matching a local gemma-4 GGUF file

test_aime.py takes its sampling protocol from the checkpoint's own generation_config.json (pass@N at the recommended temperature, or a single greedy sample when the checkpoint recommends greedy). To run an fp8 / offload checkpoint, point FREETOKEN_TEST_MODEL at the fp8 dir and set FREETOKEN_TEST_MOE_CACHE_SIZE=8192; FREETOKEN_AIME_REQ=2 FREETOKEN_AIME_MAX_TOKENS=32768 is the offload recipe (req 2 with a 32k budget).

Budget the token allowance for sampling, not for greedy: on Qwen3.5-35B-A3B-FP8 the three recommended-sampling samples of aime25 req 1 ran 4.1k / 10.7k / 10.0k tokens against greedy's ~4k, and req 2 needs ~21k even greedy — hence the 32k budget for the harder problems.

Run these before opening a PR — they are cheap, and a gate nobody trips rots silently:

FREETOKEN_REBUILD_TEST_MODEL=<small model dir> uv run pytest tests/e2e/test_cache_rebuild.py

What earns a place here

A test must be able to fail on a plausible regression of production code.

Weigh that against how the bug would be found otherwise. Application-facing surfaces — the terminal UI, the access-log filter, the request ring, the status and health endpoints, the CLI — announce their own breakage the moment anyone uses them, and they change shape often enough that a unit test mostly restates today's implementation. Those belong to e2e/ and to actually running the thing. What earns a unit test is logic that fails silently: a wrong number that still looks like a number, a model family that quietly stops having its tool calls parsed, a cache that returns the wrong slot. Prefer covering that where it lives rather than through the surface that happens to expose it.

Writing a test in the same commit as the feature is right; what matters is where its expectations come from. A test that checks the result against something independent — a PyTorch reference, a round-trip, a CPU mirror, a live registry — keeps working when the implementation is rewritten, and can disagree with it. A test whose assertions restate the branch structure the author just wrote can only ever agree, so it never fails on anything except a deliberate change, and then it is edited to match. If you cannot name what the expectation is checked against, the test is unlikely to earn its place. Bug-repro tests should drive the current call pattern of the fixed code, not the pre-fix one; wire-format and kernel tests should assert against an independent reference (round-trip, dequant oracle, CPU mirror), not against the implementation's own output. Registry-style tests should iterate the live registry rather than hand-maintained whitelists, so new entries are covered automatically.