Benchmarking EP8-Tuned: Validating Parallelism Optimizations for Kimi K2.6 on PCIe Blackwell
Introduction
In the sprawling journey of deploying the Kimi K2.6 model across 8× RTX PRO 6000 Blackwell GPUs, a single message captures the critical moment where theory meets measurement. Message [msg 11527] is a benchmark execution — a Python script dispatched via bash to the CT200 server, running a systematic throughput evaluation of the EP8-tuned SGLang service. On its surface, it is a straightforward performance test: fire prompts at a model server, measure tokens per second, sweep concurrency levels. But beneath this simplicity lies the culmination of a deep reasoning chain about parallelism tradeoffs, PCIe bottlenecks, and the art of squeezing throughput from a MoE model on a multi-GPU system with no NVLink.
This article examines that message in detail: why it was written, the decisions embedded in its code, the assumptions it makes, the knowledge it consumes and produces, and the thinking process that shaped it.
Context: The Parallelism Odyssey
To understand message [msg 11527], one must first understand the journey that led to it. The assistant had been systematically exploring parallelism strategies for Kimi K2.6, a Mixture-of-Experts model with 384 experts and a compressed MLA attention mechanism. The hardware — 8× RTX PRO 6000 Blackwell GPUs connected only via PCIe Gen5 — presented a specific challenge: without NVLink, inter-GPU communication is a scarce resource, and the choice of parallelism strategy determines whether the system is compute-bound or communication-bound.
The assistant had benchmarked four strategies earlier in the session:
- TP8 (pure tensor parallelism): 26.3 tok/s at C=1, ~808 tok/s peak
- PP8 (pipeline parallelism): 36.9 tok/s at C=1, ~396 tok/s peak
- EP8 (expert parallelism): 65.3 tok/s at C=1, ~961 tok/s peak
- EP4 (expert parallelism with TP2 groups): intermediate results EP8 had emerged as the clear winner, achieving 2.5× the single-stream throughput of TP8 by eliminating AllReduce on the MoE layers — the dominant compute component. Instead of sharding each expert across all 8 GPUs (requiring a full AllReduce after every MoE layer), EP8 placed 48 complete experts on each GPU and used All-to-All token dispatch. This meant each GPU computed its assigned experts independently, with no synchronization penalty for the MoE layers. The user then asked the assistant to theorize the optimal setup ([msg 11522]), and the assistant produced a detailed analysis ([msg 11523]) identifying three potential improvements: higher
max_running_requests(256+),num_continuous_decode_steps(batch multiple decode steps before yielding to the scheduler), and the DeepEP All-to-All backend. The user responded with "Try all those things" ([msg 11524]), and the assistant deployed the tuned service in [msg 11525], waited for it to become ready in [msg 11526], and then — in [msg 11527] — ran the benchmark.
The Message Itself
The message contains a single bash command that executes a Python script via heredoc. The script is a self-contained benchmark harness:
import json, time, urllib.request, concurrent.futures
CT200 = "10.1.2.200"
PORT = 30001
MODEL = "/root/models/Kimi-K2.6"
def api(prompt, max_tokens=512, timeout=600):
payload = json.dumps({"model": MODEL,
"messages": [{"role":"user","content":prompt}],
"temperature": 0, "max_tokens": max_tokens})
req = urllib.request.Request(f"http://{CT200}:{PORT}/v1/chat/completions",
data=payload.encode(), headers={"Content-Type":"application/json"})
t0 = time.perf_counter()
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.load(r)
wall = time.perf_counter() - t0
ct = data.get("usage", {}).get("completion_tokens", 0)
return ct/wall if wall > 0 else 0, wall, ct
The script performs two phases: a single-request throughput test (C=1) across five diverse prompts, and a concurrency sweep from C=1 through C=512 using a ThreadPoolExecutor. The output shows:
=== EP8 tuned (maxreq=256, continuous=8) ===
C=1 (max=512):
65.2 tok/s | 512 tok/7.9s
65.1 tok/s | 512 tok/7.9s
63.2 tok/s | 56 tok/0.9s
65.2 tok/s | 512 tok/7.8s
65.0 tok/s | 512 tok/7.9s
Concurrency sweep (max=2048):
C= 1: agg= 61.3 tok/s wall= 33.4s avg=2048tok ok=1 fail=0
C= 4: agg= 99.4 tok/s wall= 38.4s avg=955tok ok=4 fail=0
C= 8: agg= 195.3 tok/s wall= 47.7s avg=1164tok ok=8 fail=0
C= 16: agg= 345.2 tok/s wall= 60.7s avg=1310tok ...
The output is truncated at C=16 with an ellipsis, suggesting the full sweep results were cut off in the conversation display.## Why This Message Was Written: The Motivation
The message exists because the assistant needed to answer a concrete empirical question: did the tuning parameters actually improve throughput? The assistant had theorized that increasing max_running_requests from the default (likely 64) to 256, and enabling num_continuous_decode_steps=8, would improve aggregate throughput at high concurrency. But theory is cheap on a PCIe-bound system — the only way to know was to measure.
The motivation is deeply practical. The user's ultimate goal was to generate data using this model, and every percentage point of throughput improvement translates directly to faster data generation. The assistant had already demonstrated that EP8 was the best parallelism strategy; now it needed to verify that the tuning knobs actually moved the needle. The benchmark script was designed to produce the kind of data that would either validate the tuning decisions or send the assistant back to the drawing board.
There is also a meta-motivation: the assistant was building a systematic understanding of how this specific model behaves on this specific hardware. Each benchmark run adds a data point to a mental model of the system's performance envelope. The concurrency sweep, in particular, reveals where the system transitions from compute-bound to communication-bound or scheduler-bound behavior — knowledge that informs future deployment decisions.
Decisions Embedded in the Code
The benchmark script encodes several deliberate design decisions:
Choosing the OpenAI-compatible API endpoint over raw sampling. The assistant uses /v1/chat/completions rather than the more direct /v1/generate or a programmatic API. This adds overhead — JSON serialization, HTTP framing, token counting — but it tests the system as it would be used in production. The assistant is measuring real-world throughput, not synthetic kernel performance.
Five diverse prompts with a fixed 512-token generation for C=1. The prompts range from trivial ("What is 2+2?") to complex ("Write a Python JSON parser that handles nested objects."). This tests whether the model's generation speed varies significantly with prompt difficulty — and notably, the results show remarkable consistency: 65.2, 65.1, 63.2, 65.2, 65.0 tok/s. The one outlier (63.2 tok/s with only 56 tokens) is the trivial prompt that naturally terminates early. The consistency confirms that K2.6's decode speed is largely independent of the prompt content, which is expected for an autoregressive model where the per-token compute is constant.
Max tokens of 2048 for the concurrency sweep. This is a deliberate choice to ensure each request generates enough tokens to amortize the prefill cost and measure steady-state decode throughput. With only 512 tokens, the prefill phase (which processes the prompt) would dominate the timing. At 2048 tokens, the decode phase dominates, giving a cleaner measurement of the system's sustained throughput.
A ThreadPoolExecutor with a cap of 256 threads. The min(C, 256) cap prevents thread explosion at very high concurrency levels (C=384, C=512). This is a practical concession to Python's threading limitations — 512 threads would incur significant context-switching overhead that has nothing to do with the model server's performance.
A 900-second timeout per request. This is generous — 15 minutes — and reflects the expectation that at very high concurrency, individual requests may take a long time to complete due to queueing. The script also detects timeouts and stops the sweep early, which is a pragmatic guard against hung requests.
Warmup requests before measurement. The script sends two "Say OK" requests with 16 max tokens before starting the real benchmark. This is critical for GPU inference servers, which often perform JIT compilation (e.g., Triton kernel compilation) on the first request. Without warmup, the first benchmark request would include compilation overhead, skewing results.
Assumptions Made by the Assistant
The benchmark script and its interpretation rest on several assumptions:
The model server is in steady state. The assistant assumes that after the warmup requests and the first few benchmark requests, the server has compiled all necessary kernels and is operating at peak performance. This is a reasonable assumption for SGLang with Triton backends, but it's worth noting that some kernels (especially for edge cases like very short or very long sequences) may be compiled lazily.
The network is not a bottleneck. The assistant runs the benchmark from a remote machine (the CT200 server's IP is 10.1.2.200, and the benchmark script runs locally). This introduces network latency and bandwidth constraints. The API calls go over the local network, which on a 10.1.x.x subnet is likely a high-speed internal network, but the assistant implicitly assumes this overhead is negligible compared to the GPU inference time. The 7.9-second wall time for a 512-token generation at 65 tok/s suggests the network overhead is indeed small — the expected decode time is 512/65 ≈ 7.88 seconds, matching the measured wall time almost exactly.
The concurrency model is realistic. Using a ThreadPoolExecutor with concurrent HTTP requests is a reasonable approximation of real-world concurrent load, but it's not identical. Real workloads have variable inter-arrival times, different prompt lengths, and different generation lengths. The benchmark's fixed 2048-token generations with uniform arrival (all requests submitted simultaneously) represent a worst-case burst scenario.
The metric matters. The assistant measures aggregate throughput (total tokens / total wall time). This is the right metric for data generation, where the goal is to produce as many tokens as possible in a given time. But it's not the only metric — latency percentiles, time-to-first-token, and request completion time are also important for interactive applications. The assistant implicitly assumes the use case is batch generation, not interactive serving.
Input Knowledge Required
To fully understand this message, one needs:
Knowledge of SGLang's parallelism model. The --tp-size 8 --ep-size 8 flags define the world size and expert parallelism configuration. Understanding that EP is nested inside TP in SGLang — that tp_size is the total world size and ep_size subdivides it — is essential to interpreting the results. The assistant had previously discovered this the hard way when TP4 EP2 caused a division-by-zero error ([msg 11518]).
Knowledge of Kimi K2.6's architecture. The model uses Mixture-of-Experts with 384 experts, MLA (Multi-head Latent Attention) with a compressed KV cache, and INT4 quantization via Marlin kernels. The assistant's earlier analysis ([msg 11523]) explained why EP8 wins: the MoE layers dominate compute, and EP8 eliminates their AllReduce overhead.
Knowledge of PCIe communication patterns. The assistant's reasoning about AllReduce vs. All-to-All, about ring communication overhead, and about load imbalance in expert routing all rely on understanding how data moves between GPUs over PCIe. Without this, the benchmark results are just numbers — with it, they tell a story about where the bottlenecks are.
Knowledge of the tuning parameters. max_running_requests=256 increases the scheduler's request queue depth, allowing more requests to be batched together. num_continuous_decode_steps=8 makes the server process multiple decode steps before returning control to the scheduler, reducing Python-level scheduling overhead. Both are advanced SGLang parameters that require understanding of how the inference server's event loop works.
Output Knowledge Created
This message produces several pieces of knowledge:
The tuned EP8 configuration achieves ~65 tok/s at C=1. This is essentially identical to the untuned EP8 result (65.3 tok/s), confirming that the tuning parameters don't affect single-stream throughput — they only matter at higher concurrency. This is expected: at C=1, there's no batching benefit from max_running_requests, and num_continuous_decode_steps only helps when there are multiple requests to batch.
The concurrency sweep shows scaling behavior. The partial results show aggregate throughput increasing from 61.3 tok/s at C=1 to 345.2 tok/s at C=16. The scaling factor is roughly 5.6× from C=1 to C=16, which is sublinear (ideal would be 16×). This is expected — as concurrency increases, queueing delays and memory bandwidth contention reduce per-request throughput, even as aggregate throughput rises.
The benchmark methodology is validated. The script runs successfully, the warmup works, the API responds correctly, and the results are consistent. This means the assistant now has a reusable benchmark harness for future experiments.
The truncated output leaves a question open. The ellipsis at C=16 means we don't see the peak throughput or the saturation point. This is a limitation of the conversation display, not the benchmark itself — the full output likely continued to higher concurrency levels, showing where the system plateaus and eventually degrades.
The Thinking Process Visible in the Message
The assistant's thinking is visible in the structure of the benchmark script itself. Every element reveals a prior consideration:
The warmup phase shows the assistant knows about Triton JIT compilation and wants to avoid measuring it. The diverse prompts show the assistant considered whether prompt content affects throughput and decided to test it. The concurrency sweep with a timeout guard shows the assistant anticipated the possibility of hung requests and built a safety net. The ThreadPoolExecutor cap shows the assistant considered Python's threading overhead.
But the most telling element is what's not in the script: there's no comparison to the untuned EP8 baseline within the same benchmark run. The assistant doesn't run the old configuration back-to-back with the new one. This suggests the assistant is relying on the earlier benchmark results ([msg 11513]) as a baseline, comparing across runs. This is a methodological weakness — GPU server performance can vary due to thermal state, memory fragmentation, or background processes — but it's a pragmatic choice given the time cost of restarting the server for each configuration.
The assistant also doesn't test the DeepEP All-to-All backend (--moe-a2a-backend deepep), which was one of the three suggested improvements. This omission is notable. It may be because DeepEP requires a separate installation, or because the assistant decided to test the simpler tuning parameters first before introducing a new backend with its own failure modes.
Mistakes and Incorrect Assumptions
The most significant potential mistake is the assumption that the benchmark results are directly comparable to the earlier EP8 results. The earlier benchmark ([msg 11513]) used different prompts, different max tokens, and potentially different server state. The 65 tok/s at C=1 matches almost exactly, which is reassuring, but the concurrency sweep results may not be directly comparable due to the different max_tokens settings (2048 vs whatever was used before).
Another subtle issue: the benchmark script uses max_tokens=2048 for the concurrency sweep, but the model's --context-length is set to 32768. If the prompt + generation exceeds the context length, the server may truncate or error. The prompts are short (a few sentences), so prompt + 2048 tokens is well within the limit, but this is an implicit assumption worth noting.
The assistant also assumes that the num_continuous_decode_steps=8 parameter is actually taking effect. SGLang's implementation of this parameter may have constraints — for example, it may only apply when there are enough queued requests to fill the batch, or it may interact poorly with the EP8 All-to-All dispatch. The benchmark results would reveal whether it's working (higher peak throughput than the untuned run), but the truncated output prevents us from seeing that comparison.
Conclusion
Message [msg 11527] is a moment of empirical validation in a long chain of reasoning about parallelism, communication, and throughput. It represents the assistant's commitment to measuring rather than guessing — to running the actual experiment rather than relying solely on theory. The benchmark script is a carefully crafted instrument, encoding dozens of decisions about what to measure, how to measure it, and how to interpret the results.
The partial results confirm that EP8-tuned maintains the ~65 tok/s single-stream throughput that made EP8 the winning parallelism strategy, and the concurrency sweep shows healthy scaling to at least 345 tok/s at C=16. Whether the tuning parameters actually improved peak throughput over the untuned EP8 configuration remains an open question — one that the full benchmark output, presumably continuing beyond the truncated display, would answer.
In the broader narrative of deploying Kimi K2.6 on PCIe Blackwell GPUs, this message is the checkpoint where the assistant verifies that its optimizations are on the right track before proceeding to the next phase: deploying DFlash speculative decoding, benchmarking on NVLink hardware, and ultimately writing the comprehensive findings report that synthesizes everything learned.