Inkling-Small¶
Thinking Machines' 276B-parameter mixture-of-experts model (12B active), served text only in BF16 at 256K context on
one 8-card Gaudi 2 node. vllm-gaudi has no path for it, so it runs from the Inkling-Small port,
built on top of the stock releases/v0.30.0 image.
| Model | thinkingmachines/Inkling-Small (Apache-2.0) |
| Hardware | 8x Gaudi 2 (TP 8), one node |
| Image | port image built from vllm-gaudi releases/v0.30.0 (058f107), vLLM ced6857a, Gaudi SW 1.24.1 |
| Precision | BF16 (532 GB of weights) |
| Context | 262,144 served (KV pool 327,680 tokens) |
| Concurrency | --max-num-seqs 16 |
| Parsers | tools and reasoning inkling_py (from the port) |
| Recipe | recipes/thinkingmachines/Inkling-Small/recipe.yaml |
Build the image¶
From a checkout of this repository, on top of a stock releases/v0.30.0 image built as in
Getting started:
docker build -f ports/inkling-small/Dockerfile --build-arg BASE=<your-vllm-gaudi-v0.30.0-image> -t <your-inkling-small-image> .
install.sh checks the vllm-gaudi and vLLM commits, installs the inkling_hpu plugin and patches the runner's decode
window. It stops on any other base. The image carries this repository at /opt/gaudi-cookbook.
Launch¶
Inside the port image, with COOKBOOK=/opt/gaudi-cookbook:
export PT_HPU_LAZY_MODE=1
export VLLM_SKIP_WARMUP=true
export VLLM_USE_V2_MODEL_RUNNER=0
export VLLM_CONTIGUOUS_PA=false
export VLLM_GRAPH_RESERVED_MEM=0.4
export INKLING_PREFILL_BIAS_BUDGET=805306368
export INKLING_DECODE_WINDOW_PLUS1=1
export INKLING_MOE_SINGLE_REDUCE=1
export INKLING_LOCAL_WINDOW_PREFILL=1
export INKLING_STEP_HOIST=1
export VLLM_LONG_PREFILL_SHARE=0.5
export VLLM_LONG_PREFILL_SHARE_ASYNC=1
export VLLM_LONG_PREFILL_MIN_TOKENS=32768
export VLLM_ENGINE_ITERATION_TIMEOUT_S=7200
export VLLM_RPC_TIMEOUT=7200000
export VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=5400
# Fixes: the v0.30.0 profile plus this recipe's opt-in fixes, in the pack's order.
# Stops with a non-zero exit code if the image does not match.
python3 "$COOKBOOK/fixes/apply_all.py" --profile v0.30.0 vllm-long-prefill-share || exit 1
vllm serve thinkingmachines/Inkling-Small \
--tensor-parallel-size 8 \
--dtype bfloat16 \
--max-model-len 262144 \
--num-gpu-blocks-override 2560 \
--max-num-seqs 16 \
--max-num-batched-tokens 2048 \
--block-size 128 \
--max-cudagraph-capture-size 2048 \
--tokenizer-mode hf \
--gpu-memory-utilization 0.75 \
--trust-remote-code \
--enable-prompt-tokens-details \
--enable-auto-tool-choice \
--tool-call-parser inkling_py \
--reasoning-parser inkling_py
VLLM_USE_V2_MODEL_RUNNER=0: vLLM picks its V2 model runner for Inkling by default; the HPU runner is V1.VLLM_CONTIGUOUS_PA=false: the conv-state slots in decode need each request's block list in logical order. The model refuses to start with contiguous PA.--num-gpu-blocks-override 2560pins the KV pool at 327,680 tokens (1.25 full-length requests). The full budget gives 3,135 blocks, but 256K prefills then leave only about 2.7 GiB of HBM per card; at 2,560 blocks it is 4.4 to 5.6 GiB, at the same speed.--gpu-memory-utilization 0.75,VLLM_GRAPH_RESERVED_MEM=0.4andINKLING_PREFILL_BIAS_BUDGET(768 MiB per prefill bias sub-block) are one memory guard: loosening any one of them brought back device out-of-memory errors during graph replay at around 100K tokens.--tokenizer-mode hfmakes vLLM use the checkpoint'schat_template.jinja.--block-size 128is required: the conv-state snapshots are per 128-token block.- The four
INKLING_*switches are explained in the port README. The long-prefill share keeps other streams decoding while a prompt of 32K tokens or more prefills.
Required fixes¶
The v0.30.0 profile covers the generic fixes this configuration depends on: prompt-order, sampling-rows-refresh,
structured-output-rows, so-row-owners, bad-words-index, logitsprocs-rows, async-inputs, resumed-prefill-logits,
gather-logprobs-int32, prompt-logprobs, logprobs-decode-guard, seeded-sampling and rank-exception-fatal. The opt-in
vllm-long-prefill-share adds the scheduler's long-prefill time share. The port's own runner patch is applied at
image build, not here.
Verified¶
Run on one 8-card Gaudi 2 node: a stock releases/v0.30.0 image (058f107, vLLM ced6857a), the port's
install.sh run as the Dockerfile runs it (install OK), then the Launch block above unchanged. The fix step
reported every fix of the v0.30.0 profile and vllm-long-prefill-share as patched. At startup the port logged one
KV group (FullAttentionSpecx42) and KV pool: 2560 blocks = 327680 tokens = 1.25 x max_model_len 262144.
ports/inkling-small/tests/hpu/run_gates.sh quick:
| Gate | Result |
|---|---|
| Preflight: plugin, parsers, KV layout and the CPU tests inside the image | 21 passed, 0 skipped |
Smoke: arithmetic, tool call, JSON mode, tool_choice required |
pass |
| Greedy repeats, reasoning split, streamed reasoning and tool call without block markers | pass |
| Needles at both ends of 3K and 6K prompts (2 and 3 prefill chunks) | 4/4 |
| Needles at 8K, 32K and 131K, depths 0.1 / 0.5 / 0.9 | 9/9 |
| 200 logprobs requests at concurrency 8 | 200/200 clean |
| Prefix-cache hit vs cold output, 14 prompts (gate: every hit recalls its code, and any split from the cold run sits at a near-tie) | every hit recalled its code; 9 to 11 of 14 byte-identical over three runs, every split at a near-tie. The gate first required byte-identical output and failed on the first run; it was changed after that run, because repeats with no cache hit also split at near-ties on this model |
| 16 concurrent 4K needles | 16/16 |
| A 264,111-token prompt | HTTP 400 in 0.4 s |
| Server log: one KV group, the pool line, decode window of 5 blocks | pass |
A 256,796-token prompt (under the limit) was served, in 154 s for 8 output tokens.
ports/inkling-small/tests/hpu/run_gates.sh long, the same server:
| Gate | Result |
|---|---|
| Needles at 200K and 250K, depths 0.1 / 0.5 / 0.9 | 6/6 (TTFT about 107 s and 145 s) |
| Three 100K-token prompts resident at once | all three ran together, 0 preemptions |
| A 250K prompt while 16 streams decode (long-prefill share 0.5) | needle found, 168 s for the 250K request; the streams ran at 28.3 tok/s before and 34.7 during |
Performance¶
From the run above, one 8-card node, BF16. Single runs; do not carry them to other hardware or flags.
| What | Result | Measured as |
|---|---|---|
| Weight load from network storage | 4,079 s (server ready 73 min after launch) | single cold start |
| Request time, needle prompt + 24 output tokens, at 8K / 32K / 131K | 3 / 12 / 50 s | first request at each length, graphs already compiled by an earlier pass |
| Same, first use of each length | 23 / 30 / 53 s (the last prompt was 127.6K tokens) | first pass after startup, includes graph compiles |
| 16 concurrent 4K needle requests | 24 s for all 16 | second pass, warm |
| 256,796-token prompt, 8 output tokens | 154 s | single request, first use of that length |
More numbers from the port's development (decode per stream, switch A/Bs, long-prefill share) are on the port page, each with how it was measured.
Known issues¶
- Cold start is dominated by reading 532 GB of weights; from network storage it took 35 to 75 minutes.
- New prompt lengths compile graphs on first use (skip-warmup): the first 32K request took 23.5 s against 9.7 s warm.
- Text only. Image and audio inputs are not supported; the towers are not built.
What not to do¶
- Do not turn on contiguous PA, merged prefill, an FP8 KV cache, speculative decoding or a block size other than 128: the model refuses to start, because the conv-state snapshots cannot serve them.
- Do not raise
--gpu-memory-utilizationor lowerVLLM_GRAPH_RESERVED_MEMto grow the KV pool; see the memory guard above. - Do not use vLLM's built-in
inklingparsers on Gaudi: they need a Rust extension that the Gaudi build does not have, and fail on the first chat request.