Qwen3.5 Inference Performance Engineering on Hygon DCU: From Prefill/Decode Hotspots to End-to-End Gains
Abstract
In large-model inference optimization, one of the easiest things to misinterpret is treating a local speedup of a certain kernel directly as an improvement in service throughput. Even if a kernel is 1.08x faster in isolated tests, the complete service may be only a few percent faster, or even almost unchanged.
This article takes Qwen3.5-27B BF16 inference on the Hygon DCU gfx936 path as an example, using profile data, kernel microbenchmarks, and same-container endpoint A/B data already completed locally, to answer three questions:
- What operators limit Prefill and Decode respectively;
- Why BF16 M=1 GEMM/GEMV becomes the main performance wall for Decode;
- What verification a local optimization must go through before it can be judged to have actually improved the complete inference service.
This article only discusses DCU inference performance engineering. All performance numbers in the article come from existing measurement records in a fixed local environment; standalone kernel data, profile data, and end-to-end service data are labeled separately and are not mixed into a single conclusion.
1. First Split Inference into Prefill and Decode
A generation request can usually be divided into two stages:
- Prefill: processes a longer input at once, converting the prompt into an initial state;
- Decode: then generates token by token; each step usually processes only one new token while reading previously saved state.
These two stages look like they belong to the same model, but their hardware behavior is very different.
Prefill has a larger query dimension, matrix multiplication is closer to a large GEMM, and Attention can also be parallelized over larger token blocks. Decode usually has a query dimension of 1, and many matrix operations degrade into M=1 GEMM or GEMV: the amount of computation is not large, but each step must read very large weights and bears a large amount of kernel launch and runtime scheduling overhead.
A simplified time model can represent the complete request:
1 | request time = prefill GEMM |
Therefore, “Attention is fast” or “a certain GEMM is fast” only explains one part of the total time. What really deserves attention is: what proportion this part accounts for in the target workload, and whether it recurs in every generated token.
2. Experimental Environment and Measurement Protocol
The local measurements cited in this article use the following environment and definitions:
| Item | Configuration |
|---|---|
| Accelerator | Hygon DCU, gfx936-class path |
| Model | Qwen3.5-27B |
| Weight compute type | BF16 original weights |
| PyTorch | 2.10.0 |
| Inference framework | vLLM local runtime environment |
| Standalone warmup | 10 times |
| Standalone measured iterations | 40 times |
| Endpoint test | fixed request set, concurrency 1, same-container A/B |
| Main metrics | output throughput, mean/p99 TPOT, TTFT, output text, failure count |
Measurements are divided into three layers:
- Profile: answers where time is spent;
- Standalone/CUDAGraph microbench: answers the local upper bound of the target kernel;
- Endpoint A/B: answers how much benefit the complete service actually receives.
These three layers cannot replace each other. Profile is suitable for locating the problem, not for directly serving as a throughput result; microbench is suitable for screening implementations, not for directly representing the service; only endpoint A/B can verify the full-chain effect.
The profile, RPB2 endpoint, and Tail-M counterexample in this article come from different historical experimental subjects: they are used to explain performance phenomena at different layers, not additive results from the same source combination, and the improvement numbers in this article cannot be added together.
3. What a Long-Context Profile Showed
The local profile recorded the stage timings of an official 8K prompt-path request with page832, prefix caching disabled, and all-Tail-Q vendor attention:
1 | q=4096 prefill total: 2720.145 ms |
3.1 Prefill: After Attention, Large GEMM Becomes the Main Time Bucket
In q=4096 Prefill, the main time items are as follows:
| Time item | Duration |
|---|---|
| Attention | 238.718 ms |
| aten::mm | 1840.596 ms |
| aten::mm share of q=4096 Prefill | 67.67% |
This set of data gives a very direct judgment: after Attention has already been reduced, continuing to make small adjustments to Attention boundary parameters will have gains limited by its share of total time. At this point, what is more worth studying is the data access, tile organization, and actual call shapes of large GEMM.
Here aten::mm is not an abstract “model time”, but the aggregate of a batch of real matrix multiplication calls. To continue optimizing, it must be split into specific M, N, K shapes, dtype, call frequency, and whether it is covered by other operators, rather than only looking at the total bucket name.
3.2 Decode: Linear/GEMV Takes the Vast Majority of Time
The Decode statistics from the same profile are as follows:
| Time item | Time per token |
|---|---|
| Linear total | 38.078 ms |
| LLMM1 | 23.470 ms |
| rocBLAS GEMV | 14.607 ms |
| Linear share of Decode | 88.33% |
Observed per Decode token:
1 | LLMM1: 176 launches/token |
The sources of the 129 GEMV calls can be summarized by module as:
1 | 64 MLP down projection |
This shows that the optimization focus for Decode differs from Prefill. Decode is not simply about finding a “faster Attention”; it must handle a large number of small matrix operations: they are repeated at every step, the saving per call is small, but after multiplying by the number of layers and generated tokens, they may form an appreciable end-to-end difference.
4. Why BF16 M=1 Operations on DCU Are Especially Difficult
When the M dimension of matrix multiplication is 1, compute parallelism is relatively limited, while the weight matrix is still large. In this case, the kernel is often closer to being limited by weight reads and memory access efficiency, rather than by theoretical FLOPS.
A simplified M=1 computation can be written as:
1 | y[j] = sum_k x[k] * W[j, k] |
Each output row must traverse the K-dimensional weights. Performance is affected jointly by the following factors:
- whether weight loading is contiguous and whether accesses can be coalesced;
- how many output rows a block processes;
- how threads within a wave cooperate during K-dimensional reduction;
- conversion overhead between BF16 reads and FP32 accumulation;
- whether output writeback causes extra synchronization;
- whether kernel launch and graph replay have already become the main cost.
Therefore, optimization of an M=1 kernel is usually not simply increasing the thread count. Too many threads increase reduction and synchronization cost, while too few threads cannot sufficiently hide weight-read latency. Truly effective configurations often only suit a fixed set of model shapes.
The RPB2 experiment in this article keeps BF16 original weights and retains the verified FP32 pair accumulation, only adjusting the output-row blocking of the same LLMM1 implementation. Here RPB can be understood as rows per block: a block is responsible for a different number of output rows, and the organization of weight reads and reduction changes accordingly.
5. Local Standalone Results for BF16 M=1 GEMM
On the same DCU, with the same BF16 inputs and the same compiled extension, RPB4 and RPB2 are compared:
| Weight shape | RPB4 | RPB2 | Speedup | Max difference |
|---|---|---|---|---|
| 14336 x 5120 | 0.114160 ms | 0.106480 ms | 1.0721x | 0 |
| 16384 x 5120 | 0.131040 ms | 0.120960 ms | 1.0833x | 0 |
| 34816 x 5120 | 0.272480 ms | 0.250400 ms | 1.0882x | 0 |
The test conditions were warmup 10 times and 40 formal measurement iterations. The outputs of all three shapes are bitwise identical to the reference path, and repeated runs are stable. Estimated from the weight matrix bytes divided by the measured kernel time, the effective weight bandwidth is about 1.38-1.42 TB/s; this is an effective bandwidth estimate for this kernel, not a direct measurement of DCU peak HBM bandwidth.
This set of data shows that the RPB2 gain is a real local execution gain, but it has not yet answered the service-layer question. Standalone only measured the target matrix multiplication; complete Decode also includes other Linear operations, state reads, sampling, and runtime overhead.
6. From Kernel to Service: Same-Container A/B Is the Key Gate
To determine whether the local gain can propagate to complete inference, A/B was performed locally in the same container, with the same model, the same request set, and the same runtime conditions. Both the baseline configuration and RPB2 use BF16 original weights. The endpoint metrics are as follows:
| Tier | Baseline output tok/s | RPB2 output tok/s | Throughput change | Baseline p99 TPOT | RPB2 p99 TPOT |
|---|---|---|---|---|---|
| 4K-8K | 18.8369 | 19.5961 | +4.03% | 46.9627 ms | 44.9923 ms |
| 8K-16K | 14.5837 | 15.0306 | +3.06% | 47.5920 ms | 45.5662 ms |
| 16K-32K | 11.7495 | 12.0286 | +2.38% | 49.8507 ms | 47.8545 ms |
The relationship between endpoint results and standalone is typical: the local kernel is 1.07x-1.09x, while the service layer does not show the same 7%-9% change, but rather 2.38%-4.03%. The reason is that RPB2 only affects part of the repeatedly executed Linear work, and the remaining time is still composed of other operators and runtime.
The correctness and stability checks are as follows:
1 | 4K-8K: 2644 vs 2644 output tokens, 10/10 text byte-identical |
TTFT is basically unchanged:
| Tier | Baseline p99 TTFT | RPB2 p99 TTFT |
|---|---|---|
| 4K-8K | 2049.317 ms | 2047.251 ms |
| 8K-16K | 4620.050 ms | 4614.045 ms |
| 16K-32K | 7044.241 ms | 7037.622 ms |
This matches the expectation for the optimization location: RPB2 mainly acts on Decode M=1 Linear, and the Prefill path for the first token has no obvious change, so TTFT is basically unchanged while TPOT improves.
7. Why a 1.08x Kernel Does Not Automatically Become a 1.08x Service
This phenomenon can be understood with a rough Amdahl model. Suppose the target kernel accounts for a proportion f of total time and the local speedup is s. Under ideal conditions, the overall speedup upper bound is approximately:
1 | service speedup ≈ 1 / ((1 - f) + f / s) |
When s = 1.08, if the target kernel covers only part of the total time, the overall gain is necessarily less than 8%. The actual service is also affected by more factors:
- the matrix shapes of different layers are not exactly the same;
- some calls still use the original implementation;
- kernel launch, graph replay, and state access cannot be eliminated by the target kernel;
- the large GEMM in Prefill and the small GEMM in Decode are located in different time phases;
- measurements also contain fluctuations caused by request length, cache, and system scheduling.
Therefore, the correct question for end-to-end A/B is not “how many times faster is this kernel”, but:
1 | How many real calls does it cover? |
The reason the local RPB2 results are meaningful is precisely that it simultaneously satisfies three points: the target calls appear many times in Decode, the local gain repeats across multiple real shapes, and the output and request behavior remain consistent.
8. A Counterexample: Standalone Gain, Endpoint Almost Unchanged
The same batch of local experiments also provided a very representative counterexample. After screening 145 shapes and M combinations across five types of large GEMM, 106 shapes simultaneously satisfied:
1 | BF16 output hash consistent with default path |
Looking at these shapes alone, they all look like optimizations worth integrating. After integrating them into the complete service, however, the result for the fixed mid-tier request set was only:
1 | output throughput: 16.583027 -> 16.586315 tok/s |
Another gate/up GEMM scheme that preserves exact output also achieved an average gain of only +0.3250% in steady-state same-container A/B. This does not mean the standalone measurements were wrong; it means that the time share, call coverage, or overlapability of these calls in the complete request is insufficient to form an obvious service gain.
This counterexample is very important:
Standalone speedup is a local upper bound; endpoint A/B is the product-layer gain.
If schemes are selected only based on microbench, it is easy to stack dozens of small positive gains into a seemingly large prediction, but at runtime they may overlap each other, be drowned out by other time items, or hit only very few real requests.
9. How Correctness and Stability Should Be Verified
Performance optimization cannot just record a duration number. For BF16 kernels on DCU, at least the following should be checked simultaneously.
9.1 Kernel Layer
- whether the output is finite;
- whether the output shape and stride are correct;
- the maximum absolute difference or hash compared with the reference path;
- whether repeated runs are stable;
- whether it is still correct for different batches, different M values, and boundary shapes.
9.2 Service Layer
- whether each request completes;
- whether the output token count is consistent;
- whether the generated text is byte-identical, or meets a preset accuracy standard;
- whether mean/p99 TPOT improves;
- whether TTFT regresses;
- whether there are failed requests, timeouts, or abnormal retries.
9.3 Measurement Layer
- whether the two configurations use the same model and the same extension;
- whether they are compared in the same container and with the same request set;
- whether compilation, first load, and cache setup times are handled separately;
- whether sufficient warmup is used;
- whether repeated runs confirm the direction, rather than trusting a single result.
In particular, avoid treating “the code path exists” as “the kernel has been hit”. The most reliable approach is to record the real dispatch, input shapes, and selected implementation at runtime, then correspond the markers with performance results one-to-one.
10. A Reusable DCU Inference Optimization Workflow
Step 1: First Distinguish Prefill and Decode
Measure TTFT, TPOT, and stage timings separately. Do not use Decode optimization methods to explain Prefill, and do not use conclusions about Prefill large GEMM to replace Decode M=1 analysis.
Step 2: Use Profile to Find the Time Wall
First confirm whether the cost is large GEMM, GEMV, Attention, state reads/writes, or runtime overhead. Only by knowing the time share can you know whether a local speedup has enough physical budget.
Step 3: Restore the Real Call Shapes
Record M, N, K, dtype, batch, call count, and output writeback method. Microbenchmarks on generalized matrices can only serve as references and cannot replace real shapes.
Step 4: Screen in Standalone
First use fixed warmup and multiple iterations to measure local time, while checking correctness and determinism. For implementations clearly below the threshold, do not rush to integrate them into the service.
Step 5: Do Same-Container Endpoint A/B
Keep the model, dataset, concurrency, sampling settings, and runtime environment consistent, and record throughput, TPOT, TTFT, output length, and failure count separately. A candidate must repeatedly show a positive signal across multiple tiers or multiple real shapes.
Step 6: Keep Negative Results
A scheme that is positive in standalone but has no endpoint gain is also a valuable engineering conclusion. It can help subsequent work avoid repeated testing and bring the performance budget closer to the real service instead of stopping at the kernel table.
11. Conclusion
The local measured data presents a relatively clear DCU inference performance chain:
- The main contradiction in Prefill can fall on large GEMM; in the q=4096 profile, aten::mm accounts for 67.67%;
- The main contradiction in Decode is a large number of Linear/GEMV operations; Linear accounts for 88.33% of Decode in that profile;
- A small local improvement in the BF16 M=1 kernel can propagate to the endpoint after repeated calls; RPB2 brings 2.38%-4.03% throughput improvement across three same-container A/B tiers;
- Local speedup does not guarantee end-to-end gain; 106 standalone-positive and exact tail large GEMM shapes only produced +0.0198% after integration;
- Correctness, dispatch hit, repeatability, and measurement protocol are as important as the kernel’s own time.
For inference optimization on Hygon DCU, the most reusable thing is not a fixed block parameter, but a verification order:
First use profile to find the real time wall, then use real shapes for kernel microbench, and finally use same-container endpoint A/B to verify whether it improves the complete service.
Only when the three layers of evidence point in the same direction does local “fast” deserve to be called service-layer “fast”.
The performance numbers in this article come from existing measurement records in a fixed local environment and are only used to illustrate engineering observations under this model, software stack, and DCU path. Different DCU models, drivers, DTK, compilation caches, request distributions, and system loads may produce different results; standalone kernel speedups should not be directly extrapolated as general service throughput.

