Repository navigation
[Perf] aten::add.Tensor via FlagGems is slower than DAS boxing on DCU #252
Description
[Performance] aten::add.Tensor via FlagGems is significantly slower than DAS boxing on DCU
Description
On Hygon DCU, aten::add.Tensor produces correct results through both the Torch-FL DAS boxing route and the mixed FlagGems route. However, the FlagGems route shows a significant per-call performance regression for the small, non-contiguous, scalar, and broadcast inputs used by ESM-2.
The regression is reproducible with a standalone synchronized microbenchmark and is also visible during a complete ESM-2 embedding inference. Further investigation with hipprof indicates that the actual FlagGems DCU addition kernel is not slow. Most of the latency is spent outside the device kernel, in metadata processing, the Python/Triton wrapper, configuration lookup, argument preparation, and the host-side kernel launch path.
Environment
- Hardware: Hygon DCU / BW1000
- Microbenchmark node and device:
a02r4n07, physical device0; both boxing and FlagGems tests were run on this node and device - End-to-end ESM-2 evidence:
a02r2n06, physical device0; this was a separate same-device A/B experiment - Environment activation:
source /work1/share/scnethpc2601/flagos/activate.sh - Python: 3.10
- PyTorch:
2.10.0+cpu - HIP:
6.3.26113 - Torch-FL configuration: installed
backends_dcu_flaggems.conf - Device visibility:
HIP_VISIBLE_DEVICES=0,CUDA_VISIBLE_DEVICES=0 - Model workload: ESM-2 8M embedding, three protein sequences, token shape
(3, 101), FP32
Runtime Configuration
source /work1/share/scnethpc2601/flagos/activate.sh
export HIP_VISIBLE_DEVICES=0
export CUDA_VISIBLE_DEVICES=0
export FLAGOS_USE_FLAGGEMS=1
unset FLAGOS_BACKEND_CONFIG FLAGOS_USE_FLAGGEMS_CPPThe DAS boxing control was run in a separate Python process with FLAGOS_USE_FLAGGEMS unset. Both microbenchmark routes used the same node, physical device, input shapes, dtype, warmup count, repetition count, and synchronization method. The end-to-end ESM-2 results came from a separate same-device A/B experiment and should not be treated as the same measurement run as the standalone microbenchmark.
Minimal Performance Reproduction
The following script contains all four test cases used in the comparison. They cover the non-contiguous residual input, non-contiguous attention input, scalar addition, and one-dimensional bias broadcasting observed in ESM-2. Each case is warmed up 30 times and then measured over 200 repetitions, with an explicit device synchronization after the measured loop.
Save it as add_tensor_microbench.py:
import argparse
import time
import torch
import torch_fl
cases = {
"residual_101x3x320_noncontig": (
torch.randn(3, 101, 320, device="flagos:0").transpose(0, 1),
torch.randn(101, 3, 320, device="flagos:0"),
),
"attention_60x101x16_noncontig": (
torch.randn(101, 60, 16, device="flagos:0").transpose(0, 1),
torch.randn(60, 101, 16, device="flagos:0"),
),
"ffn_101x3x1280_scalar": (
torch.randn(101, 3, 1280, device="flagos:0"),
1.0,
),
"bias_3x101x33": (
torch.randn(3, 101, 33, device="flagos:0"),
torch.randn(33, device="flagos:0"),
),
}
parser = argparse.ArgumentParser()
parser.add_argument("--warmup", type=int, default=30)
parser.add_argument("--repeat", type=int, default=200)
args = parser.parse_args()
def run_case(left, right, warmup, repeat):
for _ in range(warmup):
torch.ops.aten.add.Tensor(left, right, alpha=1)
torch.flagos.synchronize()
started = time.perf_counter()
for _ in range(repeat):
output = torch.ops.aten.add.Tensor(left, right, alpha=1)
torch.flagos.synchronize()
elapsed_us = (time.perf_counter() - started) * 1e6 / repeat
assert torch.isfinite(output).all().item()
return elapsed_us
for name, (left, right) in cases.items():
elapsed_us = run_case(left, right, args.warmup, args.repeat)
print(f"{name}: {elapsed_us:.3f} us/call")The same script must be run in two separate Python processes because the Torch-FL backend route is selected when torch_fl is imported. The backend should not be changed inside an already initialized process.
DAS boxing control
Run the following command with FLAGOS_USE_FLAGGEMS unset so that aten::add.Tensor uses the DAS CUDA boxing route:
ssh a02r4n07
source /work1/share/scnethpc2601/flagos/activate.sh
export HIP_VISIBLE_DEVICES=0
export CUDA_VISIBLE_DEVICES=0
unset FLAGOS_USE_FLAGGEMS
unset FLAGOS_USE_FLAGGEMS_CPP
unset FLAGOS_BACKEND_CONFIG
python -u add_tensor_microbench.py --warmup 30 --repeat 200FlagGems test
After the boxing process exits, start a new process on the same node and physical device. Set FLAGOS_USE_FLAGGEMS=1 so that aten::add.Tensor uses the FlagGems Python/Triton route:
ssh a02r4n07
source /work1/share/scnethpc2601/flagos/activate.sh
export HIP_VISIBLE_DEVICES=0
export CUDA_VISIBLE_DEVICES=0
export FLAGOS_USE_FLAGGEMS=1
unset FLAGOS_USE_FLAGGEMS_CPP
unset FLAGOS_BACKEND_CONFIG
python -u add_tensor_microbench.py --warmup 30 --repeat 200The inputs, test cases, warmup count, repetition count, and synchronization method are identical between the two commands. The only route-affecting difference is whether FLAGOS_USE_FLAGGEMS is set.
Microbenchmark Results
The table reports the synchronized batch-average latency per call after 30 warmup iterations and 200 measured iterations. All values are in microseconds.
| Test case | Input shape/layout | DAS boxing | FlagGems | Additional latency | FlagGems / boxing |
|---|---|---|---|---|---|
| Residual addition | (101,3,320), first input non-contiguous |
15.497 µs |
309.517 µs |
+294.020 µs |
19.97x |
| Attention addition | (60,101,16), first input non-contiguous |
15.481 µs |
305.277 µs |
+289.796 µs |
19.72x |
| FFN scalar addition | (101,3,1280) + 1.0 |
18.132 µs |
336.548 µs |
+318.416 µs |
18.56x |
| Bias broadcast addition | (3,101,33) + (33,) |
15.449 µs |
292.517 µs |
+277.068 µs |
18.93x |
All four test cases produced finite outputs. The reported interval contains 200 operator calls followed by one device synchronization, divided by 200. This avoids creating and recording a separate event pair for every individual operator call. Both routes completed successfully with exit code 0.
End-to-End ESM-2 Evidence
During one complete ESM-2 embedding forward pass, aten::add.Tensor is executed 33 times. The following table shows the aggregated per-overload measurements from that forward pass:
| Route | Total latency for 33 calls | Average latency per call |
|---|---|---|
| DAS boxing | 1680.9 µs |
50.94 µs |
| FlagGems | 11846.0 µs |
358.97 µs |
| Additional FlagGems latency | 10165.1 µs |
308.03 µs |
| FlagGems / boxing | 7.047x |
7.047x |
Overriding only aten::add.Tensor to use boxing reduced the ESM-2 embedding median end-to-end latency from 42.663 ms to 33.520 ms, an improvement of approximately 9.143 ms. The FlagGems and boxing outputs passed the numerical comparison with rtol=1e-4 and atol=1e-5, so the performance difference is not caused by divergent results.
Internal Stage Investigation
For the representative non-contiguous residual input (101,3,320), the following steady-state timings were collected after warmup from the FlagGems implementation:
| FlagGems stage | Latency | Work performed |
|---|---|---|
pointwise_dynamic.prepare_args, corresponding to op.meta() |
approximately 93 µs |
Tensor/scalar validation, dtype promotion, broadcast-shape calculation, non-contiguous stride analysis, output preparation, and StridedBuffer construction |
instantiate |
approximately 2.7 µs |
Lookup of an already generated overload; this rules out repeated Triton compilation as the main cause |
Generated Triton overload, within op.impl() |
approximately 166 µs |
Triton wrapper execution, configuration-key and cache lookup, grid calculation, argument packing, queue submission, and kernel launch |
Actual add_func_kernel_rank_3 |
approximately 4.374 µs |
Device-side elementwise addition measured with hipprof |
Relevant source locations in the repository are:
FlagGems/src/flag_gems/utils/pointwise_dynamic.pyFlagGems/src/flag_gems/utils/libentry.pyFlagGems/src/flag_gems/ops/add.py
The actual DCU kernel takes only approximately 4.374 µs, while the complete FlagGems aten::add.Tensor call takes approximately 358.97 µs in the ESM-2 operator profile. This indicates that the regression is dominated by fixed host-side work outside the arithmetic kernel rather than by insufficient device-side addition throughput.
Expected Behavior
For small elementwise additions, the FlagGems route should provide steady-state latency comparable to DAS boxing, or the backend should provide a general mechanism that avoids paying the full Python/Triton dispatch cost for every invocation. In the current measurements, the actual kernel takes approximately 4.374 µs, whereas the complete ATen call takes approximately 358.97 µs, indicating excessive host-side overhead.
Possible Backend Fix Directions
- Cache
op.meta()results for repeated dtype, shape, stride, and layout signatures, including broadcasted shapes, strides, and output metadata. - Cache complete launch metadata for generated overloads, including the configuration key, grid, and argument descriptors, instead of rebuilding it on every invocation.
- Reduce repeated work in the
op.impl()Python wrapper and the C++/Python bridge; consider a backend C++ fast path for common binary elementwise operators. - Add a replay benchmarker for the DCU backend, or skip unnecessary event-timing autotuning after the configuration for a signature is already known.
These are intended as Torch-FL/FlagGems backend changes. Reproducing or addressing this issue should not require users to modify model code.
Regression Test Proposal
A DCU performance regression test could cover the four representative cases above and perform the following checks:
- Verify that both boxing and FlagGems outputs contain only finite values.
- Compare numerical results between the two routes with an explicit tolerance.
- Record synchronized per-call latency using identical warmup and repetition counts.
- Report or fail when the FlagGems/boxing ratio exceeds a project-defined threshold.
The performance threshold should remain configurable per hardware platform. On the tested DCU system, a ratio above 2x could initially be treated as a regression signal rather than as a universal fixed threshold.
Checklist
- Compared DAS boxing and FlagGems on the same node and physical device
- Included a self-contained minimal performance reproduction
- Covered four input patterns observed in a real workload
- Included end-to-end ESM-2 evidence
- Included ATen-level, FlagGems internal-stage, and
hipprofkernel evidence - Verified finite outputs and numerical consistency
- Suggested backend-level fix directions and a regression test
Activity
lvyufeng commentedon Sep 15, 2026
标题?
wzx226226 commentedon Sep 15, 2026
[-][BUG][/-][+][Perf] aten::add.Tensor via FlagGems is slower than DAS boxing on DCU[/+]lvyufeng commentedon Sep 28, 2026
Re-measured on current
main. The regression still reproduces when the FlagGems route is forced, but the shipped DCU config no longer takes that route, so this is no longer reachable in a stock install. Details and the numbers below.What changed since the report
Two things in the reproduction commands no longer hold on
main:FLAGOS_USE_FLAGGEMSdoes not select anything on DCU any more. The per-op override is nowFLAGOS_OP_<op>=<backend>with dots doubled (torch_fl/_env.py,LoadBackendConfigincsrc/aten/common.cc), e.g.FLAGOS_OP_add__Tensor=flaggems. The issue's two commands (FLAGOS_USE_FLAGGEMSunset vs set) now run the same route, so the A/B has to be expressed as an explicit override.add.Tensoris pinned tocudainbackends_dcu.conf. PR fix: repair DCU SDPA backend selection and pointwise routing for Qwen-Image #345 moved eleven pointwise overloads to boxing as part of the Qwen-Image-2512 work;add.Tensoris one of them (torch_fl/configs/backends_dcu.conf:769). The pin is for an independent correctness reason — with a bf16 tensor beside a Python float, ATen's wrapped number puts anf64into FlagGems' pointwise kernel and the HCU backend aborts inTruncFOpConversion::createDestOps(FlagGems #6212) — not because of this perf issue. The effect on this issue is that the FlagGems arm is now reachable only by overriding that one entry, and a user running the installed conf gets boxing.Method
The two arms are therefore
FLAGOS_OP_add__Tensor=cuda(what the shipped conf routes)FLAGOS_OP_add__Tensor=flaggems(what #252 measured)The four cases, warmup, repetition count, synchronization and input shapes are the issue's, unchanged. Two deliberate deviations from the issue's script:
torch_flloads it, so the two arms cannot share an interpreter; the issue says this. A case's reading also moves by more than an order of magnitude when it shares an interpreter with the cases before it, so the per-case process boundary is what makes the four numbers comparable rather than a nicety.flagos:0, not created on the device. A devicetorch.randnis itself routed through the conf, so it is a different measurement under each arm.The route each arm actually took was confirmed with
FLAGOS_LOG=dispatch, not inferred from the selector:with the executing frame at
flag_gems/ops/add.py:107 in add— i.e. the FlagGems Python/Triton path the issue is about, on the arm that is supposed to reach it.Result
Two consecutive runs,
flagos:0, warmup 30 / repeat 200, synchronized batch average:residual_101x3x320_noncontigattention_60x101x16_noncontigffn_101x3x1280_scalarbias_3x101x33All four cases produced finite outputs on both arms, and both arms exit 0.
The regression is unchanged in shape: three of four cases reproduce at 18.6x–19.7x against the report's 18.93x–19.97x, and the ordering is the same apart from the scalar case. Two differences worth recording rather than smoothing over:
Environment
gfx936, DTK 26.04, HIP6.3.26113torch 2.10.0+cpu(DCU boxing install),flag_gems 5.4.0rc2.post1+g437ba3938e95bea7(fix: make gen_vendor_confs.py the sole writer of the vendor confs #461) + the [BUG][DCU][Distributed] Build and package _flagos_nccl against libtorch_hip and RCCL #366 wheel-bridge commit;torch_fl/configs/backends_dcu.confand every DCU source are byte-identical betweene95bea7and currentmain6b699995(that commit is MUSA-only), so the configuration measured here ismain'sflagos:0, 8 devices visible, noHIP_VISIBLE_DEVICESpinningProvenance gap, stated rather than glossed: this run is in a container whose hostname is
98c077ef0619, not on the report'sa02r4n07/a02r2n06. The A/B is same-node, same-device, same-process-boundary discipline within this run, so the ratio is comparable; the absolute numbers are not a reproduction on the report's hardware. If a same-node comparison againsta02r4n07matters, the harness below runs as-is there.Where the residual lives
Unchanged from the report's own investigation: the device kernel is a few microseconds and the rest is host-side —
pointwise_dynamic.prepare_args, the generated wrapper, config/dtype/stride metadata and launch preparation, per call, with no signature-keyed caching of the prepared metadata. Nothing in Torch-FL changed that, and the fix directions the issue lists (cacheop.meta()by layout signature, cache launch descriptors, a C++ fast path for common binary elementwise ops) are all FlagGems-side. That work is tracked upstream by FlagGems #6570 ([Performance][DCU] FlagGems pointwise_dynamic adds high steady-state latency to aten::add.Tensor, the same report, open) and by the generic root cause in FlagGems #4812 (pointwise_dynamic/libentry per-call CPU dispatch overhead dominates small-tensor latency).Disposition
Closing this: the shipped DCU configuration routes
add.Tensorto boxing for an independent correctness reason, so no installed DCU user reaches the slow path and the "Expected Behavior" section's framing ("or the backend should provide a general mechanism that avoids paying the full Python/Triton dispatch cost") is now the whole of the remaining ask — and it belongs to FlagGems, where it is filed. Please reopen if thecudapin is ever lifted foradd.Tensor, or if a DCU conf is shipped that routes it to FlagGems again; the tables above are the baseline to compare against. The2xregression threshold the issue proposes is not implemented as a test — a threshold test whose arms both need a DCU runner and whose expected ratio is currently 19x would be red on arrival, and this repo measures routes on demand rather than in CI (tests/manual/flaggems_overload_survey.py).Harness used for the two runs
tests/manual/dcu_add_tensor_ab.pyin style (one subprocess per case/route, like the existingdcu_pointwise_replay.py):The one environment note for running it on the shared install: keep
/usr/local/liband/usr/local/lib64offLD_LIBRARY_PATHso the checkout'slibtorch_flis the one that loads, and setFLAGOS_ACCELERATOR=dcuplus the DTK roots.