This guide is for ML researchers, infrastructure engineers, and developers operating or modifying the deepseek-ai/DeepSeek-V3 reference inference repository. It documents architecture, configuration, deployment, internals, and security at the depth necessary to run the code in production-adjacent settings without consulting the upstream paper.
The scope is strictly the source code at commit 9b4e978 (5 Python files, 4 JSON configs, one requirements file). Anything not present in that source is not documented here.
1. Architecture Overview
DeepSeek-V3's inference repo is a flat, layered architecture: a single-file Transformer at the core, two preparatory utilities (HF conversion and FP8 dequantization), one CLI driver, and a Triton kernel layer. There is no plugin system, no service layer, no dependency injection — module-level constants control behavior, and dist-aware Linears handle parallelism.
1.1 Inference pipeline diagram
┌──────────────────────────────────────────────────────────────────────────┐
│ CHECKPOINT PREPARATION │
│ │
│ HF release (safetensors) │
│ │ │
│ │ fp8_cast_bf16.py (optional, for non-FP8 hardware) │
│ │ - reads model.safetensors.index.json │
│ │ - calls weight_dequant() (Triton) per FP8 weight │
│ │ - writes BF16 shards + new index │
│ ▼ │
│ HF (BF16 or original FP8) │
│ │ │
│ │ convert.py │
│ │ - renames params (self_attn->attn, mlp->ffn, ...) │
│ │ - skips model.layers.61 (MTP head, unused) │
│ │ - shards by dim or by expert index │
│ ▼ │
│ /weights/.../model{rank}-mp{mp}.safetensors + tokenizer files │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ RUNTIME (per rank) │
│ │
│ torchrun -> WORLD_SIZE / RANK / LOCAL_RANK env vars │
│ │ │
│ ▼ │
│ generate.main() │
│ │ dist.init_process_group("nccl") if WORLD_SIZE > 1 │
│ │ torch.cuda.set_device(LOCAL_RANK) │
│ │ set_default_dtype(bfloat16) │
│ │ │
│ │ ModelArgs(**json.load(config)) │
│ │ │
│ ▼ │
│ Transformer(args) ┌────────────────────────────────────────┐ │
│ │ │ ParallelEmbedding (vocab // world_size)│ │
│ │ │ Block × n_layers │ │
│ │ │ ├── RMSNorm (attn_norm) │ │
│ │ │ ├── MLA (Multi-head Latent Attn) │ │
│ │ │ │ ColumnParallel(wq, wq_b,wkv_b) │ │
│ │ │ │ RowParallel(wo) │ │
│ │ │ │ KV cache: kv_cache + pe_cache │ │
│ │ │ │ (or k_cache + v_cache naive) │ │
│ │ │ ├── RMSNorm (ffn_norm) │ │
│ │ │ └── MLP (dense layers <n_dense) │ │
│ │ │ OR MoE (Gate + Experts + shared) │ │
│ │ │ Gate -> top-k experts │ │
│ │ │ routed Experts (sharded) │ │
│ │ │ shared MLP (replicated) │ │
│ │ │ RMSNorm │ │
│ │ │ ColumnParallel head -> all_gather │ │
│ │ └────────────────────────────────────────┘ │
│ │ │
│ │ AutoTokenizer.from_pretrained(ckpt_path) │
│ │ warm-up generate("DeepSeek", 2 tokens) │
│ │ load_model(model, model{rank}-mp{world_size}.safetensors) │
│ ▼ │
│ {interactive REPL || --input-file batch} │
│ │ │
│ │ per turn: │
│ │ tokenizer.apply_chat_template(messages) │
│ │ generate(model, tokens, max_new_tokens, eos, temperature) │
│ │ │ │
│ │ ▼ │
│ │ Transformer.forward(tokens[prev_pos:cur_pos], prev_pos) │
│ │ │ │
│ │ ▼ │
│ │ sample(logits, temperature) OR argmax │
│ │ │
│ ▼ │
│ tokenizer.decode(...) -> printed completion │
└──────────────────────────────────────────────────────────────────────────┘
1.2 Component inventory
| Component | Path | Role |
|---|---|---|
Transformer |
inference/model.py:738 |
Top-level model: embed, layer stack, final norm, LM head. |
Block |
inference/model.py:696 |
One transformer layer: attention + (MLP or MoE) + norms. |
MLA |
inference/model.py:396 |
Multi-Head Latent Attention. KV cache, two impls (naive, absorb). |
MLP |
inference/model.py:500 |
Standard SiLU(W1·x) ⊙ W3·x → W2 dense feed-forward (used in dense layers and as shared experts). |
MoE |
inference/model.py:636 |
Routed Mixture-of-Experts: Gate + sharded Expert list + shared MLP. |
Gate |
inference/model.py:535 |
Top-k gating with optional group-limited routing. |
Expert |
inference/model.py:601 |
Single expert (same shape as MLP, no parallelism — sharded across ranks by index). |
ColumnParallelLinear |
inference/model.py:208 |
Splits output features across ranks; no collective in forward. |
RowParallelLinear |
inference/model.py:237 |
Splits input features; all_reduce in forward. |
Linear |
inference/model.py:166 |
Custom Linear with FP8/BF16 path selection; the parallel ones inherit it. |
ParallelEmbedding |
inference/model.py:89 |
Vocab sharding + masked all-reduce. |
RMSNorm |
inference/model.py:270 |
F.rms_norm wrapper with learnable scale. |
precompute_freqs_cis |
inference/model.py:297 |
YaRN-style RoPE frequency table. |
apply_rotary_emb |
inference/model.py:378 |
Complex-valued rotary embedding application. |
act_quant, weight_dequant, fp8_gemm |
inference/kernel.py |
Triton kernels: per-block FP8 activation quant, dequant, scaled GEMM. |
generate |
inference/generate.py:31 |
Token-by-token decode loop. |
sample |
inference/generate.py:14 |
Gumbel-style multinomial sampler. |
convert.main |
inference/convert.py:33 |
HF → sharded checkpoint. |
fp8_cast_bf16.main |
inference/fp8_cast_bf16.py:12 |
FP8 → BF16 checkpoint cast. |
1.3 Technology stack
| Layer | Component | Version | Notes |
|---|---|---|---|
| Hardware abstraction | NVIDIA CUDA | required | device="cuda" is hard-coded in generate.py |
| Tensor library | PyTorch | 2.4.1 (pinned) |
bfloat16 default dtype, NCCL backend. |
| GPU kernels | Triton | 3.0.0 (pinned) |
act_quant_kernel, weight_dequant_kernel, fp8_gemm_kernel (autotuned). |
| Tokenizer | Hugging Face Transformers | 4.46.3 (pinned) |
AutoTokenizer.from_pretrained only. |
| Checkpoint format | safetensors | 0.4.5 (pinned) |
safe_open, load_file, save_file, load_model. |
| Distributed | torch.distributed (NCCL) |
bundled w/ PyTorch | tensor parallelism only. |
Note
The repository has no test suite, no Dockerfile, no CI configuration, no setup script. It is shipped as runnable reference code; downstream users are expected to integrate it.
1.4 Data flow for a single token
For each step in generate():
Transformer.forward(tokens[:, prev_pos:cur_pos], prev_pos)is called on every rank with the same input.ParallelEmbeddinglooks up rows in its local vocab slice, masks the rest, andall_reduces ifworld_size > 1.freqs_cisis sliced to[start_pos:start_pos+seqlen].- Each
Blockrunsattn(attn_norm(x), ...)thenffn(ffn_norm(x)). The firstn_dense_layersblocks useMLP; the rest useMoE. MLAcomputes Q (with optional LoRA), K/V latent + rope, fills KV cache, applies softmax-scaled attention, projects back viawo(RowParallel — hasall_reduce).MoEgates each token ton_activated_expertsofn_routed_expertsexperts; only experts whose index falls in this rank's slice run; shared experts run on every rank; per-expert outputs are summed andall_reduced.- After all blocks,
RMSNormthenColumnParallelLinearhead produces local logits; anall_gatherconcatenates the vocab slices across ranks. - The full vocab logits return to
generate()which samples and writes the next token.
Note
Steps 6 and 7 are the only places per-token cross-rank communication is non-trivial: MoE's
all_reduceover expert outputs and the head'sall_gatherover vocab slices.
2. System Requirements
2.1 Hardware
| Resource | Minimum | Recommended (671B FP8) |
|---|---|---|
| GPU | 1× CUDA GPU with ≥ 32 GB (16B BF16) | 8× H100 / H200 80 GB (FP8 native) |
| GPU interconnect | PCIe acceptable for single GPU | NVLink / NVSwitch for multi-GPU; InfiniBand for multi-node |
| CPU | any modern x86_64 / ARM | 16+ cores per node |
| RAM | 32 GB | 256 GB+ for staging conversion |
| Disk | depends on model | ~700 GB for FP8 671B; ~1.4 TB for BF16 cast |
| Network | LAN | 200 Gbps+ for multi-node NCCL |
Note
FP8 (
torch.float8_e4m3fn) is supported natively on Hopper (H100/H200), Ada (some L40-class), and Blackwell. On Ampere (A100, A40, RTX 3090) FP8 is emulated by Triton — use the BF16 cast for these.
2.2 Software
- Linux x86_64 (or any platform PyTorch 2.4.1 + Triton 3.0.0 supports). Windows is not officially supported by Triton.
- Python 3.10–3.12 (PyTorch 2.4 wheel coverage).
- CUDA toolkit compatible with PyTorch 2.4 (CUDA 12.1 wheel is the typical stack).
- NCCL bundled with PyTorch.
2.3 Capacity sizing
Working memory per rank for the 671B FP8 model on 8-way tensor parallel:
| Component | Approximate per-rank memory |
|---|---|
| Sharded FP8 weights | ~85 GB |
kv_cache (max_batch_size × max_seq_len × kv_lora_rank × dtype) |
~60 MB / batch slot at 16K context |
pe_cache (max_batch_size × max_seq_len × qk_rope_head_dim) |
~8 MB / batch slot at 16K context |
| Activations (transient) | a few GB |
| Triton autotune workspace | small, transient |
Important
Per-rank weight memory drops linearly with
world_sizefor parallel-Linear weights, but dense layer weights and shared experts replicate across ranks. Going from 8 to 16 ranks does not halve total memory if your config has shared experts.
3. Configuration Reference
3.1 Configuration sources
| Source | Format | Read by | Purpose |
|---|---|---|---|
inference/configs/*.json |
JSON | generate.py (passed via --config) |
Model architecture (ModelArgs fields). |
| Environment vars | shell vars | generate.py |
Distributed setup (WORLD_SIZE, RANK, LOCAL_RANK). |
| CLI flags | argparse | each script's __main__ |
Per-run options (paths, batch/interactive, sampling). |
| Module-level constants | Python | model.py |
gemm_impl, attn_impl, block_size — edit-source-only. |
3.2 ModelArgs — complete field reference
The ModelArgs dataclass in model.py (line 20) is the full architecture surface. Defaults are the 16B-shape values.
| Field | Type | Default | Description |
|---|---|---|---|
max_batch_size |
int | 8 |
Sizes KV cache buffers; asserts on batch input. |
max_seq_len |
int | 4096 * 4 = 16384 |
Maximum supported sequence length; sizes freqs_cis and KV cache; gates generate() with an assertion. |
dtype |
Literal["bf16", "fp8"] |
"bf16" |
If "fp8", Linear.dtype is set to torch.float8_e4m3fn; weights use FP8 storage and linear() routes through fp8_gemm. |
scale_fmt |
Optional[str] |
None |
When "ue8m0", act_quant_kernel quantizes the per-block scale to a power of two. Used by V3.1. |
vocab_size |
int | 102400 |
Token vocabulary size; must be divisible by world_size. |
dim |
int | 2048 |
Model embedding dimension; the "hidden size". |
inter_dim |
int | 10944 |
MLP intermediate dimension (dense layers). |
moe_inter_dim |
int | 1408 |
MoE expert intermediate dimension. |
n_layers |
int | 27 |
Number of Block modules. |
n_dense_layers |
int | 1 |
First N layers use dense MLP instead of MoE. |
n_heads |
int | 16 |
Number of attention heads (must be divisible by world_size). |
n_routed_experts |
int | 64 |
Total number of routed experts in each MoE block. |
n_shared_experts |
int | 2 |
Multiplier on moe_inter_dim for the shared MLP parallel to routed experts. |
n_activated_experts |
int | 6 |
Top-k experts activated per token. |
n_expert_groups |
int | 1 |
If >1, experts are partitioned into groups for "limited routing". |
n_limited_groups |
int | 1 |
Number of groups a token's experts may come from (only meaningful when n_expert_groups > 1). |
score_func |
"softmax" | "sigmoid" |
"softmax" |
Activation applied to gate logits. 671B/V3.1 use "sigmoid". |
route_scale |
float | 1.0 |
Multiplier applied to gate weights before they reach the expert combiner. |
q_lora_rank |
int | 0 |
If 0, Q is a single ColumnParallelLinear. If >0, Q is wq_a → q_norm → wq_b (LoRA-shaped low-rank decomposition). |
kv_lora_rank |
int | 512 |
Low-rank dimension for the KV latent (the central feature of MLA). |
qk_nope_head_dim |
int | 128 |
Per-head dim for the non-positional Q/K split. |
qk_rope_head_dim |
int | 64 |
Per-head dim for the rotary-positional Q/K split. |
v_head_dim |
int | 128 |
Per-head dim for V projections. |
original_seq_len |
int | 4096 |
Reference sequence length for YaRN scaling. |
rope_theta |
float | 10000.0 |
RoPE base period. |
rope_factor |
float | 40 |
YaRN scaling factor when max_seq_len > original_seq_len. |
beta_fast |
int | 32 |
YaRN fast-correction rotation count. |
beta_slow |
int | 1 |
YaRN slow-correction rotation count. |
mscale |
float | 1.0 |
Multiplier for the extended-context softmax-scale correction. |
3.3 Per-config differences (shipping JSONs)
| Field | 16B | 236B | 671B | V3.1 |
|---|---|---|---|---|
vocab_size |
102400 | 102400 | 129280 | 129280 |
dim |
2048 | 5120 | 7168 | 7168 |
inter_dim |
10944 | 12288 | 18432 | 18432 |
moe_inter_dim |
1408 | 1536 | 2048 | 2048 |
n_layers |
27 | 60 | 61 | 61 |
n_dense_layers |
1 | 1 | 3 | 3 |
n_heads |
16 | 128 | 128 | 128 |
n_routed_experts |
64 | 160 | 256 | 256 |
n_shared_experts |
2 | 2 | 1 | 1 |
n_activated_experts |
6 | 6 | 8 | 8 |
n_expert_groups |
(default 1) | 8 | 8 | 8 |
n_limited_groups |
(default 1) | 3 | 4 | 4 |
route_scale |
1.0 | 16.0 | 2.5 | 2.5 |
score_func |
(softmax) | (softmax) | sigmoid | sigmoid |
q_lora_rank |
0 | 1536 | 1536 | 1536 |
kv_lora_rank |
512 | 512 | 512 | 512 |
mscale |
0.707 | (default 1.0) | (default 1.0) | (default 1.0) |
dtype |
(bf16) | (bf16) | fp8 | fp8 |
scale_fmt |
(None) | (None) | (None) | ue8m0 |
Important
Fields shown as
(default ...)are not present in the JSON; they take theirModelArgsdefault. Keys that are present override the default.
3.4 Module-level constants in model.py
These are not configurable via JSON; they require a source edit.
| Constant | Default | Effect |
|---|---|---|
world_size |
1 |
Initialized at module load; overridden by Transformer.__init__ from dist.get_world_size(). |
rank |
0 |
Same — initialized at load, overridden in Transformer.__init__. |
block_size |
128 |
Block size for FP8 quantization (per-block scale granularity). Must divide dim and inter_dim. |
gemm_impl |
"bf16" |
When "bf16", FP8 weights are dequantized before F.linear. When "fp8", activations are quantized and fp8_gemm is called. |
attn_impl |
"absorb" |
"absorb" keeps a low-rank latent KV cache; "naive" keeps full per-head K/V cache. |
Caution
Transformer.__init__mutates module-level globals (world_size,rank) and class attributes (Linear.dtype,Linear.scale_fmt). This means constructing twoTransformerinstances with differentargs.dtypein the same process will silently corrupt the first. Don't mix configs in one Python process.
3.5 CLI flag reference (all scripts)
3.5.1 convert.py
| Flag | Type | Required | Description |
|---|---|---|---|
--hf-ckpt-path |
str | yes | Source HF safetensors directory. |
--save-path |
str | yes | Destination directory for sharded shards. |
--n-experts |
int | yes | Total expert count (must match n_routed_experts in target config). |
--model-parallel |
int | yes | Tensor-parallel degree; must divide --n-experts. |
3.5.2 fp8_cast_bf16.py
| Flag | Type | Required | Description |
|---|---|---|---|
--input-fp8-hf-path |
str | yes | Directory of FP8 safetensors (with model.safetensors.index.json). |
--output-bf16-hf-path |
str | yes | Destination BF16 directory. |
3.5.3 generate.py
| Flag | Type | Default | Description |
|---|---|---|---|
--ckpt-path |
str | required | Sharded checkpoint dir. |
--config |
str | required | JSON config path. |
--input-file |
str | "" |
One prompt per line; runs in batch mode. |
--interactive |
flag | False | Drops to >>> REPL. |
--max-new-tokens |
int | 200 |
Generation length cap. |
--temperature |
float | 0.2 |
<= 0 triggers greedy argmax. |
4. Deployment & Scaling
4.1 Deployment topologies
The repo supports four launch shapes:
| Topology | torchrun flags |
When to use |
|---|---|---|
| Single-GPU | --nnodes 1 --nproc-per-node 1 |
Development; 16B config; smoke testing. |
| Single-node multi-GPU | --nnodes 1 --nproc-per-node N |
Production for 16B/236B; convenience for 671B if you have 8× H100 in one box. |
| Multi-node | --nnodes M --nproc-per-node N --node-rank R --master-addr A --master-port P |
671B/V3.1 across racks; useful when single-node memory is short. |
python direct (single-GPU) |
env vars set manually | Debugging; setting WORLD_SIZE=1 RANK=0 LOCAL_RANK=0 python generate.py ... works because dist.init_process_group is skipped. |
4.2 Pre-launch checklist
- [ ]
--n-expertsmatches the JSON'sn_routed_experts. - [ ]
--model-parallel(inconvert.py) equals total ranks (--nnodes × --nproc-per-node). - [ ] Sharded files exist:
model0-mp{world_size}.safetensors…model{world_size-1}-mp{world_size}.safetensors. - [ ] Tokenizer files (anything matching
*token*) are inside--ckpt-path. - [ ]
vocab_sizedividesworld_sizeevenly (102400and129280accommodate 1, 2, 4, 5, 8, 10, 16, 20, 32 …). - [ ]
dimdividesworld_size(covers parallel Linear assertions). - [ ] All ranks can reach the master address on the master port.
- [ ]
nvidia-smishows the expected GPUs as available.
4.3 Scaling considerations
Tip
Tensor-parallel degree controls memory footprint per rank but adds NCCL traffic per layer. For the 671B / V3.1 model:
- 8-way TP on a single node with NVLink/NVSwitch is the sweet spot.
- Going to 16-way across two nodes adds inter-node
all_reduceper layer; expect a noticeable per-token latency hit if interconnect is sub-200 Gbps.- There is no built-in pipeline parallelism, so you cannot get around inter-node bandwidth by partitioning layers across nodes.
Note
Throughput does not increase with
--max-batch-sizepast the point where one rank's KV cache exceeds GPU memory. Pure-batch parallelism is constrained by KV memory, not compute. The repo offers no built-in continuous batching.
4.4 Determinism
generate.py calls torch.manual_seed(965) once at startup. Subsequent sample() calls draw from the global RNG. Determinism across runs requires:
- Identical config JSON.
- Identical seed (already fixed at 965).
- Identical hardware and PyTorch/Triton versions.
- Same
--temperatureand prompt sequence.
Warning
Triton autotune may select different kernel configs across runs (caches under
~/.triton/cache). Identical numerical output run-to-run is not guaranteed; bit-exactness on FP8 GEMM in particular is hardware-dependent.
5. Operations
5.1 Operating procedures
5.1.1 Cold start
- Verify checkpoint integrity: confirm
model{i}-mp{N}.safetensorsexists foriin[0, N). - Confirm
nvidia-smireports all expected GPUs idle. - Launch:
torchrun --nnodes ... --nproc-per-node ... generate.py --ckpt-path ... --config ... [--interactive | --input-file ...]. - First user prompt may take longer than subsequent ones because of the warm-up generation, Triton autotune, and optional weight materialization paths. Subsequent forwards are steady-state.
5.1.2 Health checks
The repo does not ship liveness/readiness endpoints. To verify a running session:
- Rank 0 prints
args(the resolvedModelArgs) immediately after construction. - Rank 0 also accepts and responds at
>>>once warm-up completes. nvidia-smishould show stable GPU utilization spikes during decode.
5.1.3 Graceful shutdown
- Interactive mode: type
/exitat the prompt. The script breaks the loop and callsdist.destroy_process_group()ifworld_size > 1. - Batch mode: the script terminates after processing all prompts.
- Forced:
Ctrl-Con the launcher killstorchrun, which signals all workers. NCCL communicators may take a moment to tear down.
5.2 Logging
There is no structured logging. Diagnostic output:
- Rank 0 prints the resolved
ModelArgs. - Progress bars (
tqdm/trange) appear inconvert.pyandfp8_cast_bf16.py. fp8_cast_bf16.pyprintsWarning: Missing scale_inv tensor for {name}, skipping conversionto stdout when an FP8 weight has no scale.generate.pyprints completions in batch mode withPrompt:/Completion:labels.- Non-zero ranks have
printoverridden to a no-op.
Tip
To capture rank-0 output for offline debugging, redirect with
tee:torchrun ... generate.py ... 2>&1 | tee run.logOther ranks' stderr / stdout still go to the launcher's defaults — useful when chasing NCCL errors.
5.3 Known operational issues
| Issue | Source | Workaround |
|---|---|---|
| Identity contamination (#1224) | weights | Inject a system prompt in messages. |
| Wrong-language replies (#1226) | weights | System prompt + temperature tuning. |
| Repetition loops (#1123) | sampler | Raise --temperature or fork sample() with a repetition penalty. |
| Garbled chat formatting (#1093) | tokenizer chat template | Verify tokenizer_config.json is the version intended for chat. |
Note
All four issues live in the released weights / tokenizer, not in this repo's Python. Mitigations are operational, not code fixes.
5.4 Backup & restore
Checkpoints are immutable artifacts; standard filesystem backup applies. The repo does not maintain mutable runtime state beyond the in-process messages list (cleared on /exit or /clear). There is nothing to back up between runs.
6. Performance
6.1 Where the time goes
A decode step is:
Transformer.forward(tokens[:, prev_pos:cur_pos], prev_pos)— typicallycur_pos - prev_pos == 1after prefill.- Per layer: one
MLA.forward+ oneMLP.forwardorMoE.forward. MLAdoes Q/KV projections, applies rotary onq_pe/k_pe, computes attention scores viaeinsum, softmax, AV viaeinsum, andwoprojection.MoE.forwardruns the gate (a singlelinear), bincount over selected indices, andn_activated_expertsper-token feed-forward computations (loop over experts in this rank's slice).
6.2 Memory by model
Approximate weight footprint at the shipping dtypes:
| Model | Weight dtype | Total params | Approx. weight bytes | KV cache (per slot, 16K ctx, absorb) |
|---|---|---|---|---|
| 16B | bf16 | ~16B | ~32 GB | ~70 MB |
| 236B | bf16 | ~236B | ~472 GB | ~70 MB |
| 671B | fp8 (e4m3) | ~671B | ~700 GB | ~70 MB |
| V3.1 | fp8 (e4m3, ue8m0 scale) | ~671B | ~700 GB | ~70 MB |
Note
KV-cache figures are for the
attn_impl = "absorb"path:kv_cacheis(batch, max_seq_len, kv_lora_rank=512)pluspe_cache(batch, max_seq_len, qk_rope_head_dim=64). Switching toattn_impl = "naive"multiplies cache size byn_local_heads * (qk_head_dim + v_head_dim) / (kv_lora_rank + qk_rope_head_dim), often ~30-40× larger.
6.3 The FP8 path
When args.dtype == "fp8":
Transformer.__init__setsLinear.dtype = torch.float8_e4m3fn(one byte per weight element). This applies to everyLinear/ColumnParallelLinear/RowParallelLinearconstructed afterward.- Each FP8 weight gets a companion
scaleparameter of shape(ceil(out / 128), ceil(in / 128)), dtypefloat32— block-wise inverse scales. linear()(line 131) inspectsweight.element_size():> 1byte → standardF.linear.== 1andgemm_impl == "bf16"→ dequantize via Tritonweight_dequantand runF.linearin BF16.== 1andgemm_impl == "fp8"→ quantize activations viaact_quantand runfp8_gemm(autotuned Triton kernel).
Tip
The default
gemm_impl = "bf16"is the safe path: it loads FP8 weights to save disk/memory but runs the actual matmul in BF16. To get the FP8 throughput win, setgemm_impl = "fp8"inmodel.py(it is a module-level constant).
6.4 MoE expert routing
The MoE.forward loop is sparse over experts:
counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist()
for i in range(self.experts_start_idx, self.experts_end_idx):
if counts[i] == 0:
continue
expert = self.experts[i]
idx, top = torch.where(indices == i)
y[idx] += expert(x[idx]) * weights[idx, top, None]
Per rank, only experts with experts_start_idx <= i < experts_end_idx are constructed (others are None). Each rank does work proportional to the number of tokens whose top-k included an expert in its slice. The shared MLP runs on every rank.
Note
Per-rank load is uneven across ranks, especially with small batches: a rank may have zero tokens routed to its experts on a given step. The
dist.all_reduce(y)at the end ofMoE.forwardwaits for the slowest rank.
6.5 Triton autotune
fp8_gemm_kernel is decorated with:
@triton.autotune(configs=fp8_gemm_configs, key=['N', 'K'])
fp8_gemm_configs enumerates 36 (BLOCK_SIZE_M, BLOCK_SIZE_N, num_stages) combinations (3 × 3 × 4) at fixed BLOCK_SIZE_K=128, num_warps=8. Triton runs the search the first time a new (N, K) shape appears. Cached results live under ~/.triton/cache.
Tip
First-token latency includes autotuning. The 2-token warm-up generation in
generate.main()triggers most paths before the user types — but odd context lengths or batch sizes can hit fresh(N, K)combinations and cause one-time pauses.
6.6 Performance tuning levers
| Lever | Default | Effect |
|---|---|---|
--temperature 0 |
n/a | Greedy decoding; bypasses sampler RNG draw. Marginal. |
gemm_impl = "fp8" (source edit) |
"bf16" |
Genuine FP8 throughput; precision drops slightly. |
attn_impl = "naive" (source edit) |
"absorb" |
Slightly faster compute, much larger KV cache. Use only for short contexts. |
Lower max_seq_len |
16384 |
Smaller KV caches → more headroom for batch / shards. |
Lower max_batch_size |
8 |
Smaller KV caches; same per-prompt latency. |
Higher --nproc-per-node |
n/a | Lower per-rank weight memory; more NCCL traffic. |
7. Security
7.1 Licensing
The repository ships two LICENSE files (in the file tree, contents not surfaced in the briefing):
LICENSE-CODE— the license for this Python code.LICENSE-MODEL— the license for the model weights (separate document).
Caution
Verify both licenses before commercial deployment. The model license is distinct from the code license, and DeepSeek's model licenses include use-case restrictions that do not apply to the code. Do not assume code-license terms govern the weights.
7.2 Weights provenance and integrity
load_model and safetensors.load_file perform structural validation but do not verify cryptographic signatures. Treat the checkpoint as code: it controls the model's behavior end-to-end.
Warning
safetensorsfiles are safe to load (no pickle), but a malicious checkpoint with poisoned weights can produce arbitrary text output. If you take a checkpoint from an untrusted source, you are running an untrusted model. Hash-verify against the publisher's manifest.
7.3 Multi-tenant cautions
This repo is not designed for multi-tenant serving:
messagesis a single in-process list; there is no session isolation.- The interactive REPL has no auth, no rate limiting, no input validation beyond what the tokenizer provides.
- KV cache is sized for
max_batch_sizeat construction; mixing tenants in one process leaks attention context across them via the cache.
Important
If you need to expose this to multiple users, wrap each invocation in a separate Python process (or use a serving framework like vLLM that handles isolation properly). Do not multiplex tenants inside one
generate.py.
7.4 What NOT to log
The repo itself logs nothing user-content-related by default. If you wrap it for production, do not log:
- Raw user prompts (PII, secrets, sensitive context).
- Generated completions verbatim (may leak training-data quotes or sensitive synthesis).
- The full
messageslist (combines prompts and replies). - Tokenizer outputs (token IDs are reversible to text).
What's safe to log: latency, token counts, error types, rank/world identifiers, config name (config_*.json), --temperature, --max-new-tokens.
7.5 Network exposure
The repo opens no sockets directly. NCCL opens TCP connections among ranks (default master port 29500 or whatever you pass via --master-port). These should remain on a private interconnect.
Caution
Never expose NCCL master ports on a public network. NCCL has no authentication; an attacker reachable on the master port can disrupt or hijack communication. Bind NCCL to a private subnet or use Unix-domain sockets via PyTorch's launcher options.
7.6 Input validation
generate.py validates only:
world_sizedividesvocab_size(assertion inParallelEmbedding).- World-size divisibility for parallel Linears (assertions).
- Prompt length ≤
max_seq_len(assertion ingenerate()). - Prompt count ≤
max_batch_sizein batch mode (assertion). - At least one of
--input-file/--interactiveis set (assertion). --n-experts % --model-parallel == 0(assertion inconvert.py).
There is no content filter, prompt-injection guard, jailbreak detector, or output safety filter. All those are application concerns layered on top.
7.7 Dependency surface
inference/requirements.txt pins four direct dependencies. Their transitive surface is:
torch==2.4.1— PyTorch core.triton==3.0.0— kernel compiler.transformers==4.46.3— onlyAutoTokenizeris used.safetensors==0.4.5— checkpoint I/O.tqdm(transitively, used inconvert.pyandfp8_cast_bf16.py).
Warning
Dependencies are pinned. Audit them against your security policy; in particular,
transformersis a large package and you only useAutoTokenizer. Locking to4.46.3is a deliberate compatibility choice — newer versions can change tokenizer template behavior.
7.8 Threat model summary
| Threat | Mitigation in repo | Mitigation needed at deployment |
|---|---|---|
| Malicious checkpoint | none | hash-verify against publisher; sandbox the loader. |
| Resource exhaustion via long prompts | max_seq_len assertion |
rate-limit at the wrapper layer. |
| Information leakage in logs | no default logging | scrub at the wrapper. |
| Cross-tenant leakage | none (single-process design) | one process per tenant, OR adopt a server. |
| Unauthorized NCCL access | none | bind NCCL to private interface. |
8. Developer Internals
8.1 Repository layout
DeepSeek-V3/
├── LICENSE-CODE # Code license (separate from model license).
├── LICENSE-MODEL # Model-weights license.
├── README.md # Project README (not exposed in briefing).
├── README_WEIGHTS.md # Weights-specific notes.
├── figures/ # Plot images (benchmark.png, niah.png).
└── inference/ # The entire runnable codebase.
├── requirements.txt
├── convert.py # HF -> sharded checkpoint.
├── fp8_cast_bf16.py # FP8 -> BF16 cast.
├── generate.py # CLI driver + decode loop.
├── kernel.py # Triton kernels (act_quant, weight_dequant, fp8_gemm).
├── model.py # Transformer, MLA, MoE, all parallel primitives.
└── configs/
├── config_16B.json
├── config_236B.json
├── config_671B.json
└── config_v3.1.json
8.2 Class hierarchy (model.py)
nn.Module
├── ParallelEmbedding
├── Linear
│ ├── ColumnParallelLinear
│ └── RowParallelLinear
├── RMSNorm
├── MLA
├── MLP
├── Gate
├── Expert
├── MoE
├── Block
└── Transformer
dataclass
└── ModelArgs
13 classes total. Linear is the base for both parallel variants, which only override __init__ (asserting divisibility and partitioning features) and forward (adding collectives where needed).
8.3 The MLA forward pass — annotated
MLA.forward(x, start_pos, freqs_cis, mask):
- Compute Q. With
q_lora_rank == 0, Q is a singlewq @ x. Otherwise Q iswq_b @ q_norm(wq_a @ x)— a low-rank decomposition. - Reshape Q to
(bsz, seqlen, n_local_heads, qk_head_dim)and split per-head intoq_nope(no positional) andq_pe(rotary), sizedqk_nope_head_dimandqk_rope_head_dimrespectively. - Apply rotary embedding to
q_pe. - Compute KV via
wkv_a @ x, split into akv_lora_rank-sized latent and aqk_rope_head_dim-sizedk_pe. Apply rotary tok_pe. - Branch on
attn_impl:"naive"— project the latent throughwkv_bto get full per-head K and V, write them tok_cache/v_cache, compute scores viaeinsum("bshd,bthd->bsht", q, k_cache)."absorb"— foldwkv_binto Q's no-positional projection (q_nope = einsum("bshd,hdc->bshc", q_nope, wkv_b[:, :qk_nope_head_dim])), write the latent tokv_cacheandk_petope_cache, compute scores as the sum of two einsums (one againstkv_cache, one againstpe_cache).
- Add mask if present. Softmax in float32 (cast back to
x.dtype). - Branch on
attn_implagain for AV product:"naive"—einsum("bsht,bthd->bshd", scores, v_cache)."absorb"—einsum("bsht,btc->bshc", scores, kv_cache)then project to V viaeinsum("bshc,hdc->bshd", x, wkv_b[:, -v_head_dim:]).
wo(x.flatten(2))— RowParallel projection back todim(withall_reduceifworld_size > 1).
Note
The "absorb" trick saves cache memory: instead of storing K and V at full head dimensionality, only the latent is stored. Reconstruction happens on-the-fly during the QK and AV einsums. This is the central design that distinguishes MLA from standard MHA.
8.4 The MoE forward pass — annotated
MoE.forward(x):
- Flatten to
(N, dim)whereN = batch * seqlen. Gatereturns(weights, indices)of shapes(N, n_activated_experts).- Allocate
y = zeros_like(x). bincount(indices.flatten())gives per-expert hit counts (a CPU-sidetolist()is used to skip zero-count experts cheaply).- For each expert
iin this rank's slice (experts_start_idx ≤ i < experts_end_idx): if its count is 0, skip. Otherwise, find the(idx, top)pairs whereindices == i, run that batch of tokens throughexpert(x[idx]), weight byweights[idx, top], accumulate intoy. - Run the shared
MLPon the entire input (not gated):z = self.shared_experts(x). dist.all_reduce(y)to sum routed-expert contributions across ranks.- Return
(y + z).view(shape).
The Gate itself optionally implements group-limited routing:
- If
n_groups > 1: scores are reshaped to(N, n_groups, n_routed_experts/n_groups), group scores are computed (max with no bias, top-2 sum with bias), toptopk_groupsgroups are selected per token, the rest are masked to-inf, and finally top-k experts are picked across the (now-restricted) score tensor. - This is what
n_expert_groups: 8, n_limited_groups: 4means in the 671B/V3.1 configs: tokens may only route to experts within 4 of the 8 groups.
8.5 The Triton kernels — annotated
8.5.1 act_quant_kernel
Per-program (i.e., per CUDA threadblock) work:
- Load a
BLOCK_SIZE-sized chunk of activations as float32. - Compute the absolute max, clamp to
1e-4, divide by448.0to get the scale. - If
scale_fmt == "ue8m0", round the scale up to the nearest power of two (2^ceil(log2(s))). - Quantize:
y = x / s, cast tofloat8_e4m3fn(y_ptr.dtype.element_ty). - Store
yands.
448.0 is the maximum representable magnitude in e4m3fn.
8.5.2 weight_dequant_kernel
Per-program 2D tile:
- Load a
BLOCK_SIZE × BLOCK_SIZEtile of FP8 weights as float32. - Load the single corresponding scale from
s_ptr. - Multiply and store as the default dtype (BF16 in normal use).
The scale layout assumes one scale per (BLOCK_SIZE, BLOCK_SIZE) block of the weight matrix.
8.5.3 fp8_gemm_kernel
Standard tiled matmul, FP8 inputs, float32 accumulator, scaled per-block:
accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
a_s is per-row, per-K-tile (one scale per BLOCK_SIZE_K chunk along K). b_s is per-block. The inner loop walks K, advancing all four pointer sets (a_ptrs, b_ptrs, a_s_ptrs, b_s_ptrs).
Note
The autotune search space is
BLOCK_SIZE_M ∈ {16, 32, 64} × BLOCK_SIZE_N ∈ {32, 64, 128} × num_stages ∈ {3, 4, 5, 6}with fixedBLOCK_SIZE_K=128, num_warps=8. The autotuner re-runs only when(N, K)changes —Mis treated as a runtime quantity and does not trigger a new search.
8.6 Code patterns and conventions
- Module-level globals control behavior.
world_size,rank,gemm_impl,attn_impl,block_sizeare all module-level constants inmodel.py.world_sizeandrankare re-assigned atTransformer.__init__time; the others are edit-source-only. - Class attributes for FP8/BF16 selection.
Linear.dtypeandLinear.scale_fmtare class attributes set byTransformer.__init__based onargs.dtype/args.scale_fmt. EveryLinear-derived layer constructed afterward inherits. - Asserts as input validation. No raised exceptions, no error classes. The repo uses
asserteverywhere (assert vocab_size % world_size == 0,assert max(prompt_lens) <= model.max_seq_len, etc.). Strip-debugging (python -O) would silently disable these. - Explicit per-rank shard naming. Files are
model{rank}-mp{world_size}.safetensors; each rank loads its own. There is no central index that re-shards on demand. @torch.inference_mode()on the forward. Wraps bothTransformer.forwardandgenerate(). This disables autograd globally during decode — about a 1-2% memory saving overtorch.no_grad().- Hard-coded
device="cuda". The decode loop allocatestokens = torch.full((..., total_len), -1, dtype=torch.long, device="cuda"). CPU-only execution is impossible without source edits. - Rank-0-only printing.
global print; if rank != 0: print = lambda *_, **__: None. Reassigns the builtin in thegenerate.mainscope so all subsequent prints are silent on non-zero ranks. - No type checker enforcement. Many functions have type hints, but there is no
mypyconfig. - No tests. No
tests/, nopytest.ini, no test fixtures.
8.7 Module reference
8.7.1 inference/model.py
Top-level constants:
world_size = 1
rank = 0
block_size = 128
gemm_impl: Literal["bf16", "fp8"] = "bf16"
attn_impl: Literal["naive", "absorb"] = "absorb"
Public API (everything users typically import): ModelArgs, Transformer, Block, MLA, MLP, MoE, Gate, Expert, Linear, ColumnParallelLinear, RowParallelLinear, RMSNorm, ParallelEmbedding, precompute_freqs_cis, apply_rotary_emb, linear.
A __main__ block at the bottom builds a default-shaped Transformer with random tokens and prints the output shape — useful as a smoke test.
8.7.2 inference/generate.py
Public functions: sample, generate, main.
Global side effects in main(): re-bound print, torch.cuda.set_device, torch.set_default_dtype, torch.set_num_threads, torch.manual_seed. These persist for the lifetime of the process.
8.7.3 inference/convert.py
Public function: main(hf_ckpt_path, save_path, n_experts, mp).
The mapping dict at the top of the file is the source of truth for HF parameter renaming. Adding support for new parameter names means extending this dict.
8.7.4 inference/fp8_cast_bf16.py
Public function: main(fp8_path, bf16_path).
Imports weight_dequant from kernel. Caches loaded files in a dict, evicting in insertion order when more than 2 are resident.
8.7.5 inference/kernel.py
Public functions: act_quant, weight_dequant, fp8_gemm. Plus the underlying Triton kernels (act_quant_kernel, weight_dequant_kernel, fp8_gemm_kernel).
fp8_gemm_configs is a module-level autotune config list.
8.8 Testing strategy
There are no tests. To validate a change:
- Run the
__main__block ofmodel.py(random-weight smoke test):cd inference && python model.py. This builds a Transformer with default args, runs a forward, and prints the output shapetorch.Size([2, 102400]). - Run
generate.pywith--input-fileagainst a known-good checkpoint and compare completions byte-for-byte. - For kernel changes, write a small driver that compares Triton output to a PyTorch reference (
torch.einsumfor GEMM, manual quant foract_quant).
Tip
Before merging architecture changes, run the same prompt at
--temperature 0(greedy) on both old and new code. Any drift should be intentional and explained.
8.9 Potential improvements
The briefing's extracted/summary.tsv shows zero error classes and zero API endpoints — both expected for this style of repo. Concrete improvements that match what the code actually does:
- Configurable
attn_impl/gemm_impl. Make theseModelArgsfields rather than module-level constants. Allows--attn-impl naivefor debugging without source edits. - Per-rank logging. A simple
logging.getLogger()with per-rank prefix beats the current "silence all but rank 0" pattern when investigating multi-rank issues. - Structured exceptions. Replace some
asserts with named exceptions (ConfigMismatchError,PromptTooLongError) so callers can branch. - Type-checked configs.
pydanticoverdataclasswould catch JSON typos like"n_actived_experts"instead of silently using the default. - Tests. Even a single end-to-end test that loads a tiny synthetic checkpoint and generates 5 tokens would catch regressions.
- Continuous batching / dynamic batching. Out of scope for a reference repo, but the obvious next step for serving.
8.10 Contributing
The repo has no CONTRIBUTING.md. Standard GitHub flow applies: fork, branch, PR. Active community issues focus on the model behavior, not the inference code (the most common labels are [BUG] filed against the running model, e.g. #1224, #1226).
9. Integration Points
9.1 Inputs
| Input | Type | Format | Source |
|---|---|---|---|
| Sharded checkpoint | filesystem | model{rank}-mp{world_size}.safetensors |
convert.py output. |
| Tokenizer | filesystem | HF tokenizer files (tokenizer.json, tokenizer_config.json, …) inside --ckpt-path |
Copied verbatim by convert.py from HF release. |
| Model config | filesystem | JSON keyed by ModelArgs field names |
inference/configs/*.json. |
| Distributed env | env vars | WORLD_SIZE, RANK, LOCAL_RANK |
Set by torchrun. |
| User input | stdin or file | text | >>> prompt or --input-file. |
9.2 Outputs
| Output | Type | Destination |
|---|---|---|
| Completions | text | stdout (rank 0). |
Resolved ModelArgs |
text | stdout, once at startup (rank 0). |
| Per-rank checkpoint shards | filesystem | --save-path for convert.py. |
| BF16 checkpoint shards + index | filesystem | --output-bf16-hf-path for fp8_cast_bf16.py. |
9.3 Internal integrations
generate.py→model.py. ImportsTransformer,ModelArgs. CallsTransformer.forward, readsmodel.max_seq_len.generate.py→ Hugging Facetransformers. ImportsAutoTokenizer. Usesfrom_pretrained,apply_chat_template,encode,decode,batch_decode,eos_token_id.generate.py→safetensors. Importsload_model(loads weights into a built model).model.py→kernel.py. Importsact_quant,weight_dequant,fp8_gemm. Used by the FP8 path oflinear().fp8_cast_bf16.py→kernel.py. Importsweight_dequant.model.py→torch.distributed. Callsdist.is_initialized,dist.get_world_size,dist.get_rank,dist.all_reduce,dist.all_gather.generate.py→torch.distributed. Callsdist.init_process_group,dist.broadcast_object_list,dist.destroy_process_group.
9.4 External integrations
The repo does not directly integrate with vLLM, SGLang, TGI, or any other serving framework. Those frameworks integrate with the Hugging Face checkpoint (the source format), not with the post-convert.py sharded layout. To use this code as the inference engine for a serving framework, you would write a thin wrapper that:
- Loads the model via
Transformer(args)+load_model. - Exposes a
generate(prompt_tokens, max_new_tokens, eos_id, temperature)function. - Bridges to the framework's expected request/response format (OpenAI-compatible JSON, gRPC, etc.).
There is no example wrapper in this repo.
9.5 Hooks for extension
Extension points the existing code naturally provides:
| Extension goal | Where to hook |
|---|---|
| Custom sampler | sample() in generate.py (line 14). |
| Custom chat template | tokenizer's tokenizer_config.json, not this repo. |
| Streaming output | generate() loop (line 31) — yield each next_token instead of returning the list. |
| Stop sequences | inside the for cur_pos loop in generate(). |
| LoRA adapters at inference | wrap Linear to apply additional low-rank adapters in forward. |
| Continuous batching | replace the batch model: generate.py currently expects all prompts to be the same length post-pad. |
Tip
The smallest meaningful extension is a streaming generator: turn
generate()into a Python generator withyieldafter eachtokens[:, cur_pos]write. This unlocks token-by-token streaming in any wrapper, with about 5 lines of change.