Multi-GPU & Multi-Node

--tp N selects tensor parallelism, sharding weights within layers. --layer-split N selects whole-layer placement on supported architectures. The flags are mutually exclusive; unsupported requests fail at startup. Layer split is local to one node. Only supported tensor-parallel architectures can use --tp-node-id / --tp-peers across nodes. Migrate old layer-placement commands from --tp N to --layer-split N (environment: TENSORSHARP_LAYER_SPLIT_DEGREE=N).

🧭

--tp N selects tensor parallelism, sharding weights within layers. --layer-split N selects whole-layer placement on supported architectures. The flags are mutually exclusive; unsupported requests fail at startup. Layer split is local to one node. Only supported tensor-parallel architectures can use --tp-node-id / --tp-peers across nodes. Migrate old layer-placement commands from --tp N to --layer-split N (environment: TENSORSHARP_LAYER_SPLIT_DEGREE=N).

When do you need it?

On the GGML backends, fused per-rank block graphs make --tp 2 decode faster than a single GPU on models that fit on one card too β€” 51.7 vs 37.3 tok/s on Gemma 4 E4B Q8_0 (2Γ— RTX 2000 Ada). If you need more concurrent throughput rather than lower single-stream latency, reach for continuous batching first β€” it is on by default and costs nothing to try.

Choose tensor parallelism or layer split

Tensor parallelism (--tp N) splits projections inside each layer across N GPUs. The ranks compute concurrently and combine partial results with collectives. Layer split (--layer-split N) assigns whole layers to N local GPUs and transfers hidden states at device boundaries. Layer split increases capacity; it does not guarantee a throughput improvement.

Qwen 3.8 Flash Next supports local --tp N on ggml_cuda for eligible FFN quantizations, and --layer-split N on GGML CUDA/Vulkan. DeepSeek V4.1 supports routed-MoE TP on GGML CUDA (degrees 2–8), while attention and caches retain layer placement. DeepSeek V4 and GLM 5.x also support local whole-layer placement; GLM has local native TP. The flags are mutually exclusive, model/backend checks reject unsupported modes, and these whole-model paths do not support multi-node groups.

# Whole layers on two local GPUs; the server accepts the same option
TensorSharp.Cli/bin/TensorSharp.Cli --model qwen38-flash-next.gguf \
    --backend ggml_cuda --layer-split 2

Config equivalent: { "layer-split": 2 }. Environment equivalent: TENSORSHARP_LAYER_SPLIT_DEGREE=2. Do not combine it with tensor-parallel or distributed settings; multi-node layer split is not implemented.

How tensor parallelism works

Each transformer block is rewritten into a pair of complementary shardings, so exactly one collective is needed per block half:

  1. Column-parallel projections

    QKV and gate/up split their output dimension β€” attention heads or the FFN intermediate size β€” across GPUs. No communication is needed: each rank simply produces its own slice of the activations.

  2. Independent per-rank compute

    Attention (or the activation function) runs on each GPU over only the heads that GPU owns, against that GPU's own KV cache.

  3. Row-parallel projections + AllReduce

    The output and down projections split their input dimension, so each rank computes a partial sum. One AllReduce adds the partials together and every rank ends the block holding the identical hidden state.

Norms, token embeddings, and the LM head are replicated on every rank rather than sharded β€” they are small, and replicating them removes a collective from the critical path.

Local TP β€” one process, several GPUs

Local TP lives in a single process. On the direct cuda backend one thread issues commands to all GPUs in sequence and CUDA streams provide the real parallelism; AllReduce runs as peer-to-peer device copies plus an element-wise add kernel. On the GGML backends a rank worker pool drives the GPUs concurrently β€” a GGML op submits and synchronizes in one call, so a sequential rank loop would run the cards strictly one after another.

# CLI β€” split the model across 2 GPUs
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2

# Same on the GGML CUDA backend
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend ggml_cuda --tp 2

# Pick which physical GPUs the ranks map to
TENSORSHARP_TP_DEVICES=0,2 dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll \
    --model model.gguf --backend ggml_cuda --tp 2

# Server β€” same flag (TENSORSHARP_TP_DEGREE=2 also works)
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll \
    --model model.gguf --backend ggml_cuda --tp 2

Or put it in a config file:

{ "model": "model.gguf", "backend": "cuda", "tp": 2 }

Distributed TP β€” several machines

Each node runs its own process over its own local GPUs and connects to every other node over TCP with a length-prefixed framing protocol. AllReduce is hierarchical: reduce locally over P2P inside each node, exchange over TCP between node representatives, then broadcast back down β€” so only 1/tp_local of the data ever crosses the network.

# 2 nodes x 2 GPUs each = global TP degree 4.
# Node 0:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
    --tp-node-id 0 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"

# Node 1 β€” same peer list, different node ID:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
    --tp-node-id 1 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"

The server can front such a cluster, but only as node 0 β€” the driver that owns sampling and serves HTTP. Every other node runs a TensorSharp.Cli worker with the same model, backend, and peer list. The TENSORSHARP_TP_* environment variables work in place of the flags.

# Node 0 β€” server / driver:
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model model.gguf --backend cuda \
    --tp 2 --tp-node-id 0 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"

# Node 1 β€” CLI worker:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
    --tp-node-id 1 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"
⚠️

Every node must pass the same --tp-peers list, in the same order, and a unique --tp-node-id (0-based, indexing into that list). The port β€” 9500 in the examples β€” is not a default: choose one and make sure it is reachable between all nodes. Peer traffic is unauthenticated and unencrypted, so keep the cluster on a trusted private network.

Nodes are usually started by hand a few seconds or minutes apart, so a node that comes up first keeps retrying its outbound connections for up to 120 seconds instead of failing on the first refused connection. Once the mesh is complete each node prints [TcpCommunicator] Rank r/N connected to all peers.

Supported architectures

Nearly every autoregressive architecture in TensorSharp runs under TP, and so does Qwen-Image-2.1's diffusion transformer; heterogeneous layers get their own sharding strategy. Layer-only architectures are marked separately and require --layer-split. This table describes local support; no GLM 5.x release claims distributed/cross-node TP β€” GLM-5.2, GLM-5.3 and GLM-5.3-Flash alike hard-refuse --tp-node-id/--tp-peers before the model is constructed.

ArchitectureTPStrategy / notes
Mistral 3βœ…Fused and separate QKV layouts, YaRN RoPE.
Gemma 4βœ…Dense and MoE, per-layer head dims; multimodal embeddings are injected into the TP path so vision/audio prompts survive. On GGML the fused whole-model MoE trunk splits inside each expert (gate/up column-parallel, down row-parallel) so global expert ids keep working β€” TS_GEMMA4_TP_FUSED_MOE=0 falls back to the whole-expert per-op path. Direct CUDA uses per-expert slicing.
Qwen 3.5 / 3.6 familyβœ…Block-cyclic V-head ownership for the GatedDeltaNet recurrent layers β€” each rank keeps its own delta/conv state, device-resident, and needs no cross-rank traffic for the recurrent path. On GGML the whole GDN block runs as one packed per-rank kernel, MoE uses expert parallelism (whole experts per rank, Megatron-split shared expert), and the LM head is column-parallel with no collective at all. Direct CUDA uses expert slicing. Bonsai2's PRISM-rotated qwen35 files are the exception: they need a single-device GGML backend and refuse --tp.
Qwen 3.8 Flash NextLocal GGML CUDA--tp N shards routed/shared FFN intermediate channels and output rows, with FP32 gathers; attention, GDN, QSA and PLE remain replicated. Eligible quantized layouts are checked at load; UD-Q2_K_XL’s IQ2_XS/IQ3_XXS FFNs are refused under TP. Physical-device qualification covers UD-IQ4_XS on A40 TP2/TP4; prefix checkpoints stay disabled under TP. --layer-split N remains available on GGML CUDA/Vulkan. Scope and model-card evidence β†’
GPT OSSβœ…MoE expert slicing, attention sinks, YaRN. Runs on the GGML backends too, though the GGML MoE path still walks experts per token per rank rather than using expert parallelism.
Nemotron-Hβœ…MoE expert slicing; Mamba2 SSM layers are computed on rank 0 and broadcast. Same GGML MoE caveat as GPT OSS.
Muse-Glimmerβœ…Dense, but with three shapes that need care: the fused [gate|up] is split per segment (a contiguous split silently hands one rank all of gate), the per-head QK RMSNorms are 1-D [head_dim] vectors and stay replicated (the Q norm also carries the folded qk_scale_factor), and the attention output gate is column-parallel by head and applied inside the per-rank region. Both AllReduces land on the raw matmul output, before the 1e-8 post-norms β€” reducing after a non-linear norm produces fluent but wrong output. 2 KV heads cap the degree at --tp 2.
DeepSeek V4.1 FlashPartial, local only--layer-split N selects whole-layer placement. --tp N selects routed-MoE TP (degrees 2–8) on GGML CUDA; attention and caches retain layer placement. Full attention TP and distributed groups are unsupported. The two modes cannot be combined. Historical full-checkpoint TP measurements were slower than layer split.
DeepSeek V4 Flashβ€” (layer split only)Different mechanism: DSV4's whole-model executors split the model by layer across GPUs rather than sharding every weight, so there is no per-layer AllReduce. --layer-split N selects N local GPUs for whole-layer placement; with no placement configuration it uses one device. The split balances the largest per-device load, counting the fixed residents β€” embedding table on the first device, output head and the whole DSpark drafter on the last β€” where they actually land.
GLM 5.xβœ… (local only)GGML GPU backends only; native TP is local/single-process for the whole family β€” GLM-5.2, GLM-5.3 and GLM-5.3-Flash. GLM-5.2 shards MLA heads and hidden rows inside every routed expert. GLM-5.3 (not Flash) is the same 79-block glm-dsa shape as 5.2 β€” 256 routed experts at top-8 plus one shared expert, MLA with the lightning indexer, rope base 8e6 β€” so --tp N is accepted for it untouched on the GLM-5.2 path, with no new code and no new flag; its MLA and indexer caches are replicated per rank, so the KV footprint multiplies by N and the context that fits shrinks accordingly. Nothing above one rank has ever been run for it β€” the only recorded arithmetic is an 8Γ— A40 46 GB box where --tp 8 needs 41.7 GiB per rank and does not fit β€” so treat it as an accepted mode, not a validated configuration. GLM-5.3-Flash also head-shards KDA with per-rank recurrent state; its MLA heads and routed-expert hidden rows are sharded likewise. Attention partials reduce before each nonlinear Sinkhorn hyper-connection. On GLM-5.3-Flash's eligible segmented fast path, routed-MoE partials reduce first, then every rank computes and adds the replicated shared expert locally; hyper-connections, pooled indexer, router, norms, dense layers and embedding remain unsharded and execute per rank, while output norm / LM head stay on rank 0. The segmented fast path is gated on the Flash boolean, so plain GLM-5.3 always runs the combined scheduler. For Flash, CPU MoE, tracing, partial TS_GLM_TP_SHARD, oversubscription, or missing native hyper-connection kernels select the combined scheduler fallback, where the shared expert runs once on rank 0. Use --layer-split N for whole-layer placement.
Hunyuan Denseβ€”Single device; tensor parallelism, layer split and distributed groups are refused at startup.
DiffusionGemmaβ€”Single device; tensor parallelism, layer split and distributed groups are refused at startup.
Qwen-Image-2.1βœ… (local only, diffusion transformer)--tp N on ggml_cuda / ggml_vulkan shards the diffusion transformer Megatron-style: each GPU holds 32/N whole attention heads and 12,288/N MLP columns, and the two row-parallel products in each block are summed across GPUs. LoRA factors are sharded along with their weights, and each GPU caches the prefix of its own heads. N must divide the 32 heads β€” 2, 4, 8 or 16 on one machine β€” and each weight's quantized blocks must stay whole when it is sharded, which the load checks per weight type; only 2 GPUs have been measured. The text encoder, vision encoder and VAE stay on the first GPU, and multi-node groups are refused. See measured below.
Wan / MiniMax-H3 (video)β€”Single device; tensor parallelism, layer split and distributed groups are refused at startup.

Two MoE shardings are in play. Expert slicing (direct CUDA, and GPT OSS / Nemotron-H everywhere) gives each GPU 1/tp of every expert's weights with a replicated router, so load stays balanced whichever experts a token selects. Expert parallelism (Qwen 3.5/3.6 on GGML) partitions whole experts instead, which keeps each rank down to a single batched ggml_mul_mat_id dispatch per projection rather than a per-(token, expert) loop. Gemma 4 and GLM 5.x on GGML use a third variant: the Megatron split goes inside every expert (gate/up column-parallel, down row-parallel), so every rank still sees the full expert table and a token's selected ids stay globally valid β€” which is what ggml_mul_mat_id requires, since it will not accept the same expert id twice in one token's selection.

Requirements & constraints

Measured results

2Γ— RTX 2000 Ada (16 GB each, PCIe, no NVLink), --backend ggml_cuda --tp 2, prefill 512 / decode 64, tokens per second:

Model1 GPU--tp 2
Gemma 4 E4B Q8_02760 / 37.32488 / 51.7
Gemma 4 26B-A4B IQ4_XS1845 / 48.52537 / 51.2
Qwen 3.5-9B Q8_01461 / 23.1399 / 24.4
Qwen 3.5-35B-A3B IQ4_XSdoes not fit184 / 18.1
Muse-Glimmer 30B UD-IQ2_XXS †1171 / 40.21569 / 63.2
Muse-Glimmer 30B Q8_0, 28.2 GB †does not fit1691 / 34.3

† Measured on 2Γ— RTX PRO 4000 Blackwell (24 GB each, PCIe), not the RTX 2000 Ada pair the other rows use β€” the two hardware sets are not directly comparable to each other, only each row against its own 1-GPU column. Muse-Glimmer is the one model here that beats a single GPU on both phases (1.34Γ— prefill, 1.57Γ— decode); its Q8_0 has no 1-GPU number because 28.2 GB of weights does not fit on a 24 GB card at all.

That is not universal. On 3Γ— RTX PRO 6000 (PCIe, no NVLink), GLM-5.2 UD-IQ2_XXS at prefill 2048 / decode 64 measures 915.9 / 43.9 tok/s on the plain layer split against 505.6 / 17.6 under --tp 3: each of the 78 layers needs two all-reduces of a [6144, n_tokens] hidden state, and on a PCIe host that costs more than the split saves. TP also holds a full-length cache on every rank, dropping the fitted context from 342,272 to 91,136 tokens, and it changes the reduction order β€” against the recorded llama.cpp goldens a 2-bit MoE reproduces 3 of 6 prompts under --tp 3 where the layer split reproduces 5 of 6. There, TP is a capacity feature rather than a latency one. How much TP buys depends on the interconnect and on how much of a layer can be split at all.

Decode β€” the memory-bound half TP is meant to help β€” reaches 1.39Γ— a single GPU on Gemma 4 E4B and 1.06Γ— on Qwen 3.5-9B, with both Gemma 4 models producing output byte-identical to their single-GPU runs. Prefill is compute-bound and pays the collectives, so it lands at or below the single-GPU figure for models that fit on one card. Qwen 3.5-35B-A3B does not fit a 16 GB card at all β€” TP is the only way to run it, and memory splits 9.4 + 8.0 GB across the pair.

The layer split, measured

A layer split shards no weights, so it should cost nothing and gain nothing β€” and that is what it measures. 2Γ— A100-80GB, Qwen3.8-Flash-Next-UD-Q2_K_XL (73.4 GiB), --layer-split 2 against one GPU:

Measure1 GPU--layer-split 2 (layer split)
Greedy outputByte-identical β€” same SHA-256
VRAMwhole model on one card24.2 + 26.2 GB
Prefill~1520–1550 t/s~1520–1550 t/s
Decode~56 t/s~56 t/s

llama.cpp on the same box behaves the same way: -sm layer takes pp1536 / tg128 from 1094 / 61.2 on one GPU to 1200 / 61.5 on two β€” about 10% prefill, nothing on decode β€” and -sm row refuses to load this architecture at all. So the reason to pass --layer-split N here is that the weights, the caches and the context do not fit on one card, not throughput. Startup prints which mode ran and the per-GPU layer/byte split. TS_Q4E_LAYER_SPLIT=20,28 gives explicit layer counts per GPU (llama.cpp's --tensor-split in spirit) and throws rather than silently ignoring a value it cannot honour β€” useful, because the automatic balance prices weights and cannot see the vision tower, which loads later and lands on GPU 0.

Qwen-Image-2.1's diffusion transformer under TP

Two NVIDIA A40s (46 GB each) in different CPU sockets, where peer access is not functional and NCCL used its shared-memory transport. Prefix cache on, step times exclude step 1:

Workload1 GPU step2 GPUs stepSpeedup
Generate 1024Β², 40 steps1.107 s0.823 s1.34Γ—
Edit, 1 reference, 1024Β², 40 steps1.326 s0.934 s1.42Γ—
Generate 2048Β², 20 steps6.817 s4.436 s1.54Γ—
Edit, 1 reference, 2048Β², 20 steps7.612 s4.849 s1.57Γ—
ggml_vulkan edit, 1024Β², 20 steps (host reduction)2.670 s3.092 s0.86Γ—

On Vulkan the reduction goes through host memory and outweighs the split, so --tp is slower there on this hardware. TP changes the order in which each block's partial sums are added, so images are not bit-identical to one GPU (34.2–51.5 dB PSNR against the single-GPU image on CUDA): composition and quality hold, but details can move. Detail: the repository's docs/models/qwenimage21.md card.

CUDA graph capture under TP

A tensor-parallel token is dozens of small per-rank submissions, and replaying them is worth about 45% of decode throughput β€” so graph capture stays on under TP (disable with TS_GGML_TP_CUDA_GRAPHS=0). On 4Γ—A40:

ModelCapture offCapture on
Qwen 3.5-9B, --tp 488 tok/s128.5 tok/s
Qwen 3.5-35B-A3B, --tp 271.3 tok/s104.1 tok/s β€” the difference between TP losing and winning against a single GPU

The collective transport is likewise chosen by measurement, not capability flags: at startup the group verifies that peer copies between the advertised device pairs actually deliver their bytes and that a real NCCL AllReduce completes, then picks the fastest transport that passes. Hosts that advertise peer access which never arrives (common on virtualized cloud instances) keep the NCCL collective with peer transport disabled rather than losing it β€” which matters past two GPUs, where the pinned-host pipeline does not apply and the alternative is reducing through host RAM at every layer boundary (4Γ—A40, Qwen 3.5-9B Q8_0 decode: 53.5 β†’ 75.1 tok/s).

Tuning & diagnostics

Local AllReduce prefers CUDA peer-to-peer DMA. At startup the group enables peer access for every pair that reports it, then runs a round-trip self-test β€” some topologies (L4 cards behind certain PCIe switches, IOMMU-enabled hosts, small BAR1 windows) claim peer access but silently transfer corrupt data, and any pair that fails is demoted to host staging permanently. Hardware with no peer access at all (A16 vGPU profiles, most consumer cards) stages through host memory from the start. The fallbacks are automatic; the switches below exist to force them for diagnosis.

VariableDefaultWhat it does
TENSORSHARP_TP_DEGREE1Local GPU count to split across (= --tp on both the CLI and the server).
TENSORSHARP_TP_DEVICES0..tp-1GPU ordinals the ranks map to, e.g. 0,2. GGML backends.
TENSORSHARP_TP_NODE_IDunsetThis node's 0-based ID (= --tp-node-id). Set together with the peer list.
TENSORSHARP_TP_PEERSunsetComma-separated host:port list of every node (= --tp-peers).
TENSORSHARP_TP_CONNECT_TIMEOUT_SECONDS120How long a node retries outbound connections to its peers. Raise it when a slow orchestrator staggers node startup.
TENSORSHARP_TP_RECV_TIMEOUT_SECONDS300Per-receive timeout on a peer socket, so a stalled peer fails the collective instead of hanging on the OS TCP keepalive (often 2+ hours).
TENSORSHARP_TP_DISABLE_P2Poff1 forces every cross-GPU transfer through host memory β€” exactly the path no-peer hardware takes. Use it to test whether a multi-GPU defect lives in the P2P DMA path.
TENSORSHARP_TP_HOST_ALLREDUCEoff1 runs the local AllReduce as device→host, sum on the CPU, host→device. Slower, but mirrors the known-good multi-node reduce exactly.
TS_GGML_TP_DEVICE_AR_THRESHOLD262144Element count above which AllReduce uses ggml's device collective instead of a host reduction. GGML activations already live in host RAM, so small payloads are cheaper to sum there.
TS_GGML_TP_PARALLELon0 drives the ranks sequentially instead of concurrently β€” a diagnostic, and a large slowdown.
TS_GGML_TP_CUDA_GRAPHSon0 turns CUDA graph capture off for multi-GPU runs. Capture is on by default because a tensor-parallel token is dozens of small per-rank submissions that replay far more cheaply than they re-issue (see above). The opt-out is translated into a native GGML_CUDA_DISABLE_GRAPHS before the first backend call, because ggml latches that value on first use.
TS_GEMMA4_TP_FUSED_MOEon0 falls back from Gemma 4's fused MoE trunk to the whole-expert per-op path.
GGML_CUDA_ALLREDUCEautonccl / internal / none, passed through to ggml's collective selection. Setting it explicitly also skips the NCCL pre-flight probe.
TS_GGML_TP_AR_PROBEonBehavioural pre-flight of the NCCL collective before model load: some cloud hosts advertise GPU P2P that never delivers, and NCCL's first AllReduce then spins both GPUs forever β€” the classic symptom is a TP model load that hangs at kernel warmup. On a failed probe TensorSharp reroutes to ggml's pinned-host-memory internal pipeline. 0 skips the probe; force ignores the cached per-host verdict (~/.cache/tensorsharp/tp-collective-probe).
TS_GGML_TP_AR_PROBE_MS10000Deadline for the probe's AllReduce before the collective is declared broken; 0 disables the probe.
GGML_CUDA_AR_BF16_THRESHOLD1 MBPayload size above which ggml converts F32 collectives to BF16. TensorSharp raises ggml's own default (always convert) so decode-sized reductions stay exact; 0 disables the conversion.

Startup logs make the topology explicit: Tensor parallelism: N GPUs (<device names>) for the local group, a TP: P2P disabled… line or self-test warning when a pair is demoted, and one [TcpCommunicator] Rank r/N connected to all peers. per node.

Shared state across a cluster

When several server processes front the same workload, one piece of state can live in Redis instead of in-process memory:

# Responses store on one shared Redis
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model model.gguf --backend cuda \
    --redis-url localhost:6379

Troubleshooting

SymptomLikely cause & fix
Startup fails: requested TP degree exceeds device countThe process sees fewer CUDA devices than --tp asks for. Check CUDA_VISIBLE_DEVICES and the driver.
Unsupported mode at startupChoose a backend and architecture that implement the requested mode. Migrate old whole-layer commands to --layer-split N; CPU/MLX and single-device architectures cannot use either multi-GPU mode. The runtime refuses unsupported requests instead of silently changing placement.
A dimension is not divisible by the TP degreePick a degree that divides numHeads, numKVHeads, and intermediateSize β€” usually a power of two.
A node hangs waiting for peersThe peer list, order, or port does not match on all nodes, or a firewall blocks the port. Raise TENSORSHARP_TP_CONNECT_TIMEOUT_SECONDS if the nodes simply start far apart.
Garbled output only with multiple GPUsSuspect the P2P DMA path. Re-run with TENSORSHARP_TP_HOST_ALLREDUCE=1, then TENSORSHARP_TP_DISABLE_P2P=1; if the output becomes correct, the topology's peer DMA is at fault.
Multi-GPU is slower than a single GPUPrefill is compute-bound and pays the collectives, so it can trail a single card on a model that already fits. Decode should be faster on the GGML backends β€” if it is not, check that the fused paths are on (TS_GEMMA4_TP_FUSED_MOE unset, TS_GGML_TP_FUSED_MATMUL unset) and that the run is single-process, since multi-node falls back to the per-op forward. On a PCIe host with no NVLink, a model whose layers split poorly can be slower under TP no matter what β€” GLM 5.x needs two all-reduces per layer, and GLM-5.2 is measurably slower under --tp 3 than on the plain layer split; plain GLM-5.3 has never been run above one rank, so the same reasoning applies to it without a measurement behind it. Use TP there for capacity, not speed.
πŸ“–

Repository reference: USAGE.md β†’ Tensor Parallelism & Distributed Inference, FEATURES.md, and the TensorSharp.Distributed project.