Cross-cutting rename of the current llama.cpp endpoint on the RTX 3090
(port 7771) from frank-small to frank-secondary:
- Service trio: scripts/{frank-small.env,frank-small.service,
serve-frank-small.sh,download-frank-small.sh} → frank-secondary.*
- Test: tests/frank_small_unit.bats → tests/frank_secondary_unit.bats
- Runbooks: serverAppConfigs-docs/LLM/frank-small-*.md →
frank-secondary-*.md (9 files); the FreeToken runbook stays
freetoken-frank-small.md (it documents the retired CUDA-era service)
- Identifiers: FRANK_SMALL→FRANK_SECONDARY, llm_small→llm_secondary,
LLM_SMALL_*→LLM_SECONDARY_*, deploy_llm_small→deploy_llm_secondary
- Historical references to the OLD frank-small (FreeToken/CUDA on the
3090, and the Dankserver service that became frank-medium) are
preserved verbatim — they describe what the service was called
at the time.
9.6 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| configuration | FreeToken frank-secondary Runbook | RETIRED 2026-09-15 - FreeToken v0.1.2 serving batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 as frank-secondary on Dankserver (RTX 3090 era). |
|
2026-08-30 |
FreeToken frank-secondary Runbook
RETIRED 2026-09-15. The RTX 3090 left Dankserver and the successor frank-medium-rocmfpx (charlie12345/ROCmFPX llama.cpp on 2x Radeon AI PRO R9700) took the :7770 endpoint and the port. This page is kept as the retirement record of the FreeToken/CUDA era; the unit was archived to
~/.AGENTTRASH/20260915-frank-secondary-retire/host-units/and the repo files toscripts/.AGENTTRASH/.
The frank-secondary service on Dankserver is served by
FreeToken v0.1.2 — an edge-native MoE serving engine that keeps
expert weights in host RAM with an LRU GPU cache. Model history on this
runtime: official Qwen/Qwen3.6-35B-A3B-FP8 (since 2026-09-02) →
batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 (2026-08-30,
same format and pool budget; re-landed 2026-09-01 after two
compressed-tensors FP8 candidates failed to load — see Quantization
Compatibility). The engine itself replaced the llama.cpp
Tiel-Coder-35B-A3B (UD-Q6_K_XL) deployment.
Model
| Field | Value |
|---|---|
| HuggingFace | batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 |
| Lineage | Qwen/Qwen3.6-35B-A3B → huihui-ai abliterated → this FP8 (quantizer batsclamp) |
| Architecture | qwen3_5_moe (Qwen3_5MoeForConditionalGeneration) |
| Quant | native modelopt block-fp8 e4m3, dynamic activations, 128×128 weight_block_size, bf16 weight_scale_inv — byte-format identical to the official FP8 repo |
| BF16 kept | lm_head, embed_tokens, all norms, router gates, mamba state params, visual encoder, MTP module |
| Experts | un-fused per-expert layout (mlp.experts.{0..255}.*) matching official Qwen FP8 — same FreeToken loader path |
| Size on disk | ~35 GB (26 safetensors shards) |
| Path | /home/dank/models/huihui-qwen36-35b-a3b-abliterated-fp8 |
| Layers | 40 (10 full-attention + 30 GDN/linear-attention) |
| Experts | 256 per layer, 8 used per token |
| KV heads | 2 × 256 dim |
| Context | 262,144 (model max) |
| Chat template | froggeric/Qwen-Fixed-Chat-Templates (fixed Qwen3.6 template, shipped in tokenizer_config.json; FreeToken reads it from the checkpoint — no override) |
Important: this is the Huihui abliterated finetune (refusals removed) — not the base Qwen weights served before 2026-08-30, and not the earlier Tiel-Coder finetune. Expect a direct-answers assistant.
Runtime
| Field | Value |
|---|---|
| Engine | FreeToken v0.1.2 (ft CLI) |
| Venv | /home/dank/freetoken/.venv (uv-managed CPython 3.12) |
| Dependencies | torch 2.11.0+cu130, flashinfer 0.6.17, apache-tvm-ffi |
| CUDA toolchain | pip-installed nvidia-cuda-nvcc/crt/nvvm/runtime 13.2.86 |
| API | OpenAI-compatible (/v1/chat/completions, /v1/models) |
Pool Configuration
The GPU budget (~17.6 GiB after weights + fixed caches) is split MoE-first between expert residency and KV pages:
| Pool | Size | VRAM | Notes |
|---|---|---|---|
| MoE cache | 2,048 slots (20%) | 6.0 GiB | LRU expert cache, out of 10,240 total |
| KV cache | 453,005 tokens | 8.6 GiB | ~20 KiB/token (10 attn layers only) |
| Mamba state | 24 slots | 1.4 GiB | Per-sequence GDN state (caps concurrency) |
Live Resize
KV and MoE can be re-partitioned without restart:
ft ctl --base-url http://127.0.0.1:7770 cache --kv N --moe M --wait 300
- Growing KV (shrinking MoE): works live.
- Shrinking KV (growing MoE): requires restart (old + new pools cannot coexist in VRAM during the swap → OOM).
- The fit check refuses overcommit: if the plan doesn't fit, the old cache is retained and serving continues.
Benchmark Results (2026-09-02, base FP8 checkpoint)
See freetoken-benchmark-sweep for the full bs1/bs3/bs4/bs6 sweep across KV pool sizes 8k–700k.
At the production config (20% residency, bs4):
| Metric | Value |
|---|---|
| bs1 decode | ~30 tok/s |
| bs4 aggregate | ~87 tok/s |
| bs4 per-request | ~22 tok/s |
| Prefill (4k) | ~1,130 tok/s |
Quantization Compatibility
FreeToken v0.1.2 supports these safetensor quants for qwen3_5_moe:
| Format | Detection | Works |
|---|---|---|
| BF16 (no quant config) | default | ✅ (experts too large for 60 GB RAM) |
| modelopt block-fp8 (128×128) | quant_method: "fp8" + weight_block_size |
✅ (current) |
| NVFP4 (modelopt) | quant_algo: "NVFP4" |
✅ (best for VRAM) |
| NVFP4 (compressed-tensors) | 4-bit float, group_size=16, strategy=tensor_group | ✅ |
| Per-tensor FP8 (modelopt) | scalar weight_scale |
✅ |
| Per-channel FP8 (compressed-tensors) | weight_scale: [O, 1] |
❌ loader bug |
| Block-fp8 128×128 (compressed-tensors) | quant_method: "compressed-tensors" + block_structure: [128, 128] |
❌ dequant shape bug |
| Quark W8A8 INT8 | per-channel scales | ❌ loader bug |
| GGUF | only gemma4 arch |
❌ for qwen3_5_moe |
2026-09-01 live verification (Dankserver): both compressed-tensors
FP8 shapes fail on FreeToken v0.1.2, each a clean exit at load (37 GB
fits RAM — no swap-thrash). ornith-ai/Ornith-1.5-35B-A3B-FP8
(per-channel) aborts in expert-source resolution:
ValueError: Missing MoE expert source layers: {'gate_up': ...} — its
expert tensors do not follow the mlp.experts.{N}.gate_proj/up_proj/ down_proj naming the MoE provider scans for.
lamianlbe/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-fp8 (block
128×128, standard naming) resolves experts but aborts in dequant:
RuntimeError: The size of tensor a (2048) must match the size of tensor b (...) in _dequant_fp8_weight, which multiplies
weight × weight_scale directly instead of expanding the block scales.
modelopt block-fp8 remains the only FP8 path that loads.
Sampling
frank-secondary samples by default, and our config never said so.
scripts/serve-frank-secondary.sh passes no sampling arguments at all — only
--model, --served-model-name, --host, --port, --moe-cache-size,
--num-tokens and --max-running-requests. The behaviour comes from the
runtime's own default:
# freetoken/server/args.py:31
sampling_defaults: str = "model"
--sampling-defaults model fills temperature / top_k / top_p from the
checkpoint's generation_config.json for any request that omits them.
This checkpoint ships:
{ "do_sample": true, "temperature": 1.0, "top_k": 20, "top_p": 0.95 }
Verified behaviourally 2026-09-12 — three identical requests with no
sampling parameters sent at all produced three different reasoning traces
(sha 13ca02…/487 chars, d49928…/540, 7b81b8…/577). Greedy decoding
would be byte-identical.
This is the opposite of frank-large
| frank-secondary (FreeToken) | frank-large (halogen) | |
|---|---|---|
Silent client (no temperature) |
samples at temp 1.0 / top_k 20 / top_p 0.95 | greedy — no server-side sampler defaults |
| Deterministic across runs | No | Yes |
| Where the default comes from | checkpoint generation_config.json |
none; the request decides |
A client that talks to both endpoints and sends no sampler gets
non-deterministic answers from :7770 on Dankserver and reproducible ones from
:7770 on Dankclaw. If a workflow needs determinism from frank-secondary it must
send temperature: 0 explicitly, or the service must be relaunched with
--sampling-defaults none (framework defaults only).
Related request behaviour
- Thinking consumes the budget. At
max_tokens: 200all three probe runs returnedfinish_reason: "length"with emptycontentand the whole budget spent inreasoning_content. Budget for thinking, or the answer never appears. - The API does not validate unknown fields — a request carrying
totally_bogus_field: 123returns 200, as doeschat_template_kwargs: {"preserve_thinking": true}. A 200 is not evidence that a key was understood.
Files
| File | Purpose |
|---|---|
scripts/serve-frank-secondary.sh |
Serve script (launches ft serve) |
scripts/frank-secondary.env |
Tunables (model path, pool sizes) |
scripts/frank-secondary.service |
Systemd unit |
Limitations
- Model identity: serves the Huihui abliterated FP8 finetune, not the Tiel-Coder coder finetune from the llama.cpp era.
- JIT compilation: first startup compiles flashinfer/tvm-ffi kernels
(~5–15 min cold). Cached afterward in
~/.cache/flashinferand~/.cache/tvm-ffi. - CUDA toolchain: driver-only install (no system CUDA toolkit). The venv provides pip-installed nvcc 13.2.86 for JIT compilation. All nvidia-cu13 packages must be at the same version.
- Env var collisions: never name tunables
TEMP,HOST, orGPU_MAX_HEAP_SIZE(HIP runtime reads these).
Operational quirks
modelfield is required in/v1/chat/completionsbodies — FreeToken rejects requests without it with a 422 (Field required), unlike llama.cpp's server which defaults to the loaded model. Send"model": "frank-secondary".- Load on this swap took well under the old llama.cpp numbers: weights → experts (parallel build) → CUDA graph capture → Uvicorn in ~2 minutes; the 2026-08-30 swap verified ~40 tok/s decode on a 282-token completion.
Citations
- FreeToken paper: arXiv 2608.16157
- FreeToken PyPI:
freetoken[accel]v0.1.2 - HuggingFace model:
batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 - Upstream weights:
huihui-ai/Huihui-Qwen3.6-35B-A3B-abliterated,Qwen/Qwen3.6-35B-A3B-FP8 - Fixed chat template:
froggeric/Qwen-Fixed-Chat-Templates