Recipe · 2× RTX PRO 6000 · Zig engine

GLM 5.3 Flash on two RTX PRO 6000s, with TensorFold's Zig engine.

Serve GLM-5.3-Flash from one Linux host with two 96 GB RTX PRO 6000 Blackwell GPUs, through an OpenAI-compatible API, with 4 streams at once and image input. It runs TensorFold's native Zig/CUDA server, one rank on each GPU in one container, with 93 engine patches on top and my own EXL3 quant. 245.5 tok/s for one request, with both cards capped at 400 W. One command checks the host, builds the image, downloads the weights, starts both ranks and runs a smoke test.

Get the free recipe

Install with an AI agent

Install this model

Paste this into an AI coding agent that can run commands on the machine that will serve the model (the head node, for a Spark cluster), such as Claude Code or Codex. It reads the recipe, adapts it to your setup, starts the server and checks that it works.


        
A bright point of light with silver streaks spreading to the right, beside the words GLM-5.3-Flash, 2x RTX PRO 6000

Performance

Measured, not promised.

One request245.5tok/s, prose
Four at once436.6tok/s combined
Prefill6,033.1tok/s, 32K prompt
First token54 msone stream

The recipe's official numbers: two RTX PRO 6000 GPUs in one host, with both cards capped at 400 W.

Decode, prose

Temperature 0.7, thinking on: all streams together, per stream, and the time to first token.

StreamsTotal tok/sPer streamFirst token
1245.5245.554 ms
2336.6169.174 ms
3385.5132.099 ms
4436.6110.3122 ms

Prefill

How fast a prompt is read, by prompt size, and the time to first token.

PromptTokens countedtok/sFirst token
4K4,1295,943.1753 ms
8K8,2205,753.01.49 s
16K16,4146,006.22.79 s
32K32,8026,033.15.49 s
64K65,5645,961.511.06 s
128K131,1035,787.722.71 s
256K262,1795,503.147.70 s

The other recipe for this hardware, made with Marc Seal of Aevonix Research, runs TensorFold v0.6.5 with its own patch series. It publishes its numbers at 300 W, at temperature 0 with thinking off, so the two sets are not a like-for-like comparison.

Speed preview

What 245.5 tok/s feels like.

Sample text played at the measured decode speed, at about four characters a token: one request at 245.5 tok/s, or four at once at 110.3 tok/s each (436.6 combined). Switch between them in the window.

TensorFold Zig · GLM-5.3-Flash · 2× RTX PRO 6000
245.5 tok/sSample text, played at the measured speed

The weights

Downloaded for you, at a pinned revision.

The default

My 4 bpw EXL3 quant

Mia-AiLab/GLM-5.3-Flash-EXL3-4bpw-TensorFold, about 164 GiB. The first start downloads it into DATA_DIR/model and mounts it read-only, and an interrupted download resumes on the next start. There is nothing to set.

The default drafter, DFlash2, is downloaded the same way when it is missing. It is non-commercial: for commercial use, set DRAFTER=mtp or DRAFTER=none.

Already have a checkpoint?

Serve a directory you already have as it is, with no download:

MODEL_SOURCE=local
MODEL_DIR=/path/to/GLM-5.3-Flash-EXL3-checkpoint

Stock TensorFold reads only 4-bit experts; the recipe's engine patches and config overlay also serve mixed-width quants this way, and the checkpoint itself is never changed.

ABLIT=1

The Ablit weights

Mia-AiLab/GLM-5.3-Flash-EXL3-4bpw-TensorFold-Ablit: the same checkpoint, abliterated, at a pinned revision. The repository is gated, so open its page on Hugging Face, log in and agree to its terms, create a read token with the same account, and set both in scripts/local.sh:

ABLIT=1
HF_TOKEN=hf_...

Then ./start.sh restart. It downloads about 164 GiB into DATA_DIR/model-ablit, beside the published checkpoint, and fits its own window. Once downloaded, the weights need no token, and ABLIT=0 goes back to the published checkpoint, which stays in the cache.

Thinking is off by default in this recipe, which is also how the Ablit weights give their best results. They keep the stock DFlash2 drafter, which accepts a little less than on the published checkpoint.

The engine

Powered by TensorFold.

TensorFold, by Ash Hart, is an open-source engine that serves language models on Apple Silicon and NVIDIA GPUs through an OpenAI-compatible API. This recipe runs its Zig engine, tensorfold-native, built in Docker from commit f350cd3e with Zig 0.17.0 for the RTX PRO 6000's sm_120.

Two ranks, one container

One container runs both. Rank 0 serves the API and holds the vision tower; rank 1 follows its steps. If either fails, the other stops. Each rank holds half of the experts, attention and KDA heads, and vocabulary.

One host, over PCIe

NCCL runs over loopback, with peer-to-peer between the two cards and a copy-engine prompt exchange. Peer access is checked but not required: without it, NCCL falls back to slower paths.

One shared KV pool

With the FP8 cache on shared pages, every stream draws from one pool of 512-row pages. CONTEXT is then one request's cap, and a request that doesn't fit waits for pages.

In this recipe

93 patches applied in order at image build, most of them mine, a few carried from other authors with their names on them. The image tag is a hash of the Dockerfile, the patches and the pins, so editing scripts or docs never forces a rebuild.

On top of TensorFold

What the 93 patches do.

PatchesWhat they do
0001-0004EXL3 experts at mixed whole bit widths (for mixed-width quants), and 3-bit prompt kernels
0005-0012Decode: FP8 attention loads, the shared expert on a side stream. Prefill: one expert call a pass, no MTP pass beside DFlash2, 2-stage int8 prompt GEMMs
0013Upstream #1623: DFlash2 reads complete prompt taps at TP>1
0014-0016No target MTP layer beside DFlash2 (about 2 GiB a GPU back), and a pinned host-RAM prompt-state tier (8 GiB a rank by default)
0017-0025FP8 KV on shared pages: paged E4M3 attention kernels, page planning, snapshots, admission
0026-0033Decode: EXL3 load-ahead (upstream #1505 and #1534), fused MoE glue with new EXL3 decode tiles, KDA in one launch, one broadcast a follower step
0034-0035A one-shot peer-to-peer all-gather for decode exchanges, and the copy-engine prompt exchange
0036-0040DFlash2 copy rounds without the drafter pass, 16-row copy windows
0042-0048Review fixes, "fits:" refusals that name what would fit, the device prompt-cache share into the pool
0049-0063Paired prompt chunks (upstream #1423) extended to DFlash2, with a smaller twin buffer
0067-0068Decode DSA select on many CTAs (long context); prompt sparse attention merged in the kernel (frees 353 MiB a GPU)
0069-0071, 0077, 0083-0084Audited programmatic dependent launches along the decode chain; KDA step warps by row count
0072-0075Decode glue: greedy picks in one launch, two-problem projection launches, fused router select-and-group, a shared window's KDA chains and latent attention in one launch each
0078-0082Faster EXL3 prompt experts and packed int8 prompt GEMMs (prefill); grouped EXL3 decode reads each trellis word once, as a dependent-launch chain
0086, 0088-0091Split hc_finish, batched shared-window cache writes, hc_partial8 split by outputs, decode DSA index scores for 4-16-row windows
0092-0093A JSON answer stays JSON beside offered tools: response_format, guided_json and structured_outputs replies are never parsed as tool calls, on chat, Responses and Messages. A reply grammar is freed when its stream cannot start

The rest are the opt-in settings below, plus test and contract-record changes. The full table is in the README.

Tested before they stayed

Every default-on patch was A/B tested against the image before it: greedy (T=0) replies identical, plus a measured gain. Patches 0067-0091 each keep an exact off switch for diagnosis, and before loading weights the two ranks compare a fingerprint of their settings, the checkpoint and the binary.

Off unless you ask

Two settings make prefill faster by changing rounding, so they stay off. ABSORB_MMA=1 only changes the fp32 summation order (+6.6% prefill), but replies are no longer byte-identical. W8A8=1 is a real rounding of activations (+10%, or +19% with both): validate it on your own eval before turning it on.

What you need

Two GPUs, one host.

Two RTX PRO 6000s

Two 96 GB RTX PRO 6000 Blackwell GPUs in one Linux x86_64 host, with a driver for CUDA 13.

Free GPU memory

At least 88,000 MiB free on each GPU, and under 4,096 MiB in use, which leaves room for a desktop on one of them. Rank 0 is the heavier one: keep it on the GPU without the desktop (GPUS=1,0 with the monitors on GPU 0).

Docker

Docker with the NVIDIA Container Toolkit and BuildKit, and your login in the docker group. Plus curl, python3, flock, tar, gzip, sha256sum and nvidia-smi, and hf (huggingface_hub[cli]) for the downloads.

Disk and memory

180 GiB free for the checkpoint (about 164 GiB), 10 GiB for DFlash2 and 40 GiB for the image build; the start checks for them. The ranks stage each layer through host memory while they load, and it warns below 16 GiB available.

Quick start

One command, start to finish.

1

Clone and start

git clone https://github.com/MiaAI-Lab/GLM-5.3-Flash-2x-RTX-PRO-6000-TensorFold.git
cd GLM-5.3-Flash-2x-RTX-PRO-6000-TensorFold
DRY_RUN=1 ./start.sh   # print the whole plan: docker build, both rank command lines, docker run, smoke test
./start.sh             # check, build the image once, download the weights once, start both ranks, wait for the API, smoke-test

The downloads need the hf command: python3 -m pip install -U "huggingface_hub[cli]". The first start builds the image, a few minutes with BuildKit's caches, and downloads the weights. Each rank also hashes every checkpoint shard once; that hash is cached, so later starts only read and load the weights. It checks that /v1/models lists the model, sends a smoke request and prints GLM-5.3-Flash-EXL3 is now LIVE! on port 8020.

To change a setting, copy scripts/local.sh.example to scripts/local.sh and edit it.

2

Talk to it

Any OpenAI client works with base_url = "http://<host>:8020/v1" and the model GLM-5.3-Flash. Thinking is off by default; a request turns it on with "chat_template_kwargs": {"enable_thinking": true}.

curl -s http://127.0.0.1:8020/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "GLM-5.3-Flash",
  "messages": [{"role": "user", "content": "Hello"}]
}'
./start.sh restart          # apply changed settings
./stop.sh                   # stop, and archive the logs under logs/
docker logs -f glm53-zig    # rank 0's log (rank 1's: logs/current/rank1.log)
curl -s http://127.0.0.1:8020/health

If something stops it

The checks print a code. The ones you are most likely to see:

SymptomWhat to do
E_DOWNLOADThe download failed: check free space and your connection, and run ./start.sh again to resume.
E_HF_TOKENABLIT=1 and the gated weights can't be fetched. A 401 means Hugging Face doesn't accept the token, a 403 that its account hasn't agreed to the terms yet. Or set ABLIT=0.
E_GPU_MEMORY / E_GPU_BUSYAnother workload holds a GPU: stop it. Raise MAX_GPU_USED_MIB only for a display GPU.
E_PORTPort 8020 or 29583-29584 is in use. Stop what holds it, or set PORT / NCCL_PORT.
--parallel N needs ... GiBThe window or stream count doesn't fit. CONTEXT=auto retries by itself; otherwise lower CONTEXT or PARALLEL.
Out of memory mid-requestThe desktop or prompt scratch grew into the reserve. Raise MEMORY_RESERVE_GIB, or lower PARALLEL / CONTEXT.
W_P2PNVML reports no peer-to-peer between the cards (IOMMU/ACS, BIOS). NCCL still works over host memory, but more slowly.
A slow first startEach rank hashes the whole checkpoint once. WAIT_TIMEOUT (2400 s) covers it; raise it if needed.

The full list is in the README.

Images

It can see, too.

Image input is on by default. Send an image_url part in a user message. The vision tower runs on rank 0 and takes about 1.8 GiB there; VISION=0 serves text only and gives that back.

To free GPU memory

  • Lower PARALLEL
  • Pack more: DENSE=q6 or q4, or HEAD=q8
  • VISION=0 (rank 0 only)
  • DRAFTER=mtp

Memory

The largest window that fits.

VRAM and system RAM are separate pools here: every weight must fit on the GPUs, and nothing is offloaded to host RAM. The engine's context is fixed at start and it refuses a window that doesn't fit, so CONTEXT=auto starts by asking for 1,048,576 tokens. If that is refused, start.sh works out the window the engine's own figures allow, starts once more, and keeps it in .state/context-fit.tsv for later starts.

So the window you get depends on your cards and your settings, and I don't quote one number for it. The startup line in rank0.log shows what was measured on yours, and start.sh warns if fewer than four streams came up. The default reserve is 4 GiB a GPU; fractions are allowed, and with the 4 bpw checkpoint 3.5 fits four streams on pages.

Settings

The ones you might change.

Settings are layered: the defaults in scripts/config.sh, then .env, then scripts/local.sh, then exported environment variables, which win over everything else. ./start.sh restart applies a change. The full list is in the README.

VariableDefaultMeaning
MODEL_SOURCEdownloaddownload fetches the pinned 4 bpw checkpoint when it is missing; local serves MODEL_DIR as it is
ABLIT / HF_TOKEN0 / unset1 serves the gated Ablit weights; the read token for that download is never passed to the ranks or printed
GPUS0,1GPU indices in rank order: the first runs rank 0
PARALLEL4Concurrent streams. A number is admitted only if it fits; auto serves up to 8, as many as fit
CONTEXTautoUp to 1,048,576 tokens of prompt plus reply; auto is the largest that fits
KV / KV_PAGESfp8 / onThe KV cache (bf16 is exact), and every stream drawing from one shared pool of pages
DENSE / HEADq8 / bf16Dense linears packed at load, and the LM head: bf16, q8, q6 or q4
MEMORY_RESERVE_GIB4Memory per GPU left out of the engine's budget, which absorbs a desktop's growth
DRAFTERdflash2Inco AI's DFlash2 (non-commercial), mtp (the checkpoint's MTP head) or none
THINKING / REASONING_EFFORT0 / highThinking off by default, and its effort when a request turns it on
MAX_TOKENS32768The default reply limit
VISION1Image input (rank 0's tower)
PORT / HOST8020 / 0.0.0.0The API's port and address

The API

/v1/chat/completions with the model GLM-5.3-Flash, /v1/models and /health. It listens on every address by default. Export TENSORFOLD_API_KEY and every route except /health needs Authorization: Bearer KEY.

Logs

logs/current points to the running session: rank0.log, rank1.log, runner.sh, models.json and smoke.json. ./stop.sh warns about any requests still open, gives rank 0 20 s to finish them, archives the logs as logs/glm53-zig-<time>.tar.gz and removes the container. It only ever stops this recipe's own.

Licenses

Free, with a few rules.

  • The recipe is Apache 2.0. It holds scripts, a Dockerfile, a chat template and patches: no TensorFold, no weights, no binaries.
  • The checkpoint's quantization is Apache 2.0, and the base model, GLM-5.3-Flash, stays under Z.ai's MIT. The Ablit weights are MIT and gated on Hugging Face, where their terms apply.
  • The DFlash2 drafter is CC BY-NC-ND 4.0: research and evaluation only, commercial use needs a licence from Inco AI, and the recipe doesn't redistribute it. DRAFTER=mtp or DRAFTER=none serves without it.
  • TensorFold is Apache 2.0 from 0.6.0; its earlier code keeps its MIT notice.
  • NVIDIA's CUDA images stay under NVIDIA's own terms.

Credits

Built on the work of others.

The full list is in the recipe's CREDITS.md, and the required notices in its NOTICE.md.

Run it on your two GPUs.

Free and open, with every patch published.