Skip to content

perf(comm): add opt-in Triton AR+RMSNorm backend - #968

Open
Jeremy-wj wants to merge 3 commits into
lightseekorg:mainfrom
Jeremy-wj:perf/amd-triton-shmem-ar-rmsnorm
Open

perf(comm): add opt-in Triton AR+RMSNorm backend#968
Jeremy-wj wants to merge 3 commits into
lightseekorg:mainfrom
Jeremy-wj:perf/amd-triton-shmem-ar-rmsnorm

Conversation

@Jeremy-wj

@Jeremy-wj Jeremy-wj commented Aug 7, 2026

Copy link
Copy Markdown

Summary (Human)

Add an alternative triton-native fused backend for AR+RMSNorm op using PyTorch symmetric memory. Results show improved performance across a majority of tested configs compared to Iris, the current default fused backend. Initial end-to-end results compared to unfused are mixed but show a promising improvement for the WS4 case. Benchmarking performed on GLM-5.2 and GPT-OSS-120B. Project effort has been paused, so defaults remain unchanged. Changes also made to unfused fallback behavior, AR+RMSNorm backend selection, and graph padding behavior.

Full project documentation, implementation history, and benchmarking results are available on companion branch jeremwan/triton-shmem-experiments in the source fork.

Summary (AI)

  • Add an explicit TS_ARNORM_BACKEND=triton_shmem implementation of fused
    all-reduce + residual + RMSNorm for single-node MI350X tensor parallelism.
  • Use coarse-grained HIP-IPC data buffers with a symmetric-memory signal pad;
    collectively fall back to symmetric-memory buffers if IPC setup fails.
  • Preserve existing auto behavior, add an explicit fusion-disable control,
    and restore the complete unfused operation whenever the backend declines.
  • Fail closed for unsupported multi-node, attention-DP, speculative, and
    captured-output configurations.
  • Harden graph padding, captured-buffer lifetime, and cross-wavefront barrier
    ordering.

Validated scope: contiguous BF16 (tokens, hidden) tensors and BF16 weights on
single-node MI350X (gfx950), TP 2/4/8. This is explicit opt-in only; no model
profile or default dispatch policy is added.

The full investigation, profiles, and campaign artifacts are in the
finalized research snapshot.

Correctness note

Universal graph-padding sink routing previously landed in #531 and was narrowed
to DFLASH in temporary performance rollback #594. Ordinary non-speculative
GPT-OSS serving later reproduced slot-zero metadata corruption and passed after
the universal sink was restored. Current upstream is safer (reserved row 0,
batch-ordered page tables), so this PR treats terminal-sink routing as invariant
hardening rather than claiming the historical KV-underflow path is unchanged.

Performance evidence

The finalized research campaigns support an experimental backend, not default
promotion:

  • GPT-OSS-120B WS4: +1.27% output throughput and -1.30% median TPOT in a
    five-pair screen; WS2 was inconclusive and WS8 lost.
  • GLM-5.2-FP8: padded Triton beat Iris fused across the measured WS4/WS8
    captured-operator matrix on MI355X; full-model serving was not qualified.

These results were collected on the research snapshot, not rerun after the port
to upstream c0f41e0. Model-specific profiles remain reference material.

Test Plan

From the repository root.

CPU/runtime integration:

PYTHONPATH=python:tokenspeed-kernel/python:tokenspeed-kernel-amd/python \
  python3 -m pytest test/runtime/test_server_args_attention_backends.py -q
# 20 passed

PYTHONPATH=python:tokenspeed-kernel/python:tokenspeed-kernel-amd/python \
  python3 -m pytest test/runtime/test_dp_sampling_routing_metadata.py -q
# 16 passed

MI350X communication and fallback:

HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
PYTHONPATH=python:tokenspeed-kernel/python:tokenspeed-kernel-amd/python \
  python3 -m pytest \
  tokenspeed-kernel/test/ops/test_triton_shmem_communication.py -q
# 13 passed: WS 1/2/4/8, one-shot variants, two-shot, capture decline,
# and HIP graph replay

HIP_VISIBLE_DEVICES=0,1 \
PYTHONPATH=python:tokenspeed-kernel/python:tokenspeed-kernel-amd/python \
  python3 -m pytest \
  test/runtime/distributed/test_comm_ops.py::TestCommOps::test_declined_arrms_fusion_falls_back -q
# 1 passed

Two-rank benchmark smoke:

HIP_VISIBLE_DEVICES=0,1 \
PYTHONPATH=python:tokenspeed-kernel/python:tokenspeed-kernel-amd/python \
BENCH_WORLD_SIZES=2 BENCH_M_VALUES=1 BENCH_N_VALUES=2880 \
BENCH_BACKENDS=production_unfused,triton_shmem \
BENCH_N_WARMUP=3 BENCH_N_REPEAT=10 \
  python3 tokenspeed-kernel/test/ops/bench_triton_shmem_ar_rmsnorm.py

Both paths completed; unfused remained faster for this smoke row, so it is not
used as performance evidence.

@Jeremy-wj

Copy link
Copy Markdown
Author

Figures to support above claims in PR summary. Complete data available in full companion branch.
GPT-OSS-120B on MI350, fused triton-shmem vs. unfused baseline
gpt-oss-120b-e2e-by-world-size

GLM-5.2 on MI355, fused triton-shmem vs. unfused baseline and default fused Iris
glm-5 2-fp8-ws4-ws8-micro-deltas

@borontion
borontion marked this pull request as ready for review August 7, 2026 16:25
@borontion
borontion requested review from a team as code owners August 7, 2026 16:25
@borontion
borontion requested a review from qedawkins August 7, 2026 16:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb2a93b328

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1861 to +1862
if ts_state is not None:
_ts_mod.TRITON_SHMEM_AR_RMSNORM_STATES[ts_key] = ts_state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cache backend declines before retrying

When state creation returns None—for example on a non-gfx950 AMD GPU or after allocation/setup failure—the key remains absent, so every subsequent fused norm retries collective validation and allocation. In normal graph-enabled serving, a retry can therefore execute all_gather_object and memory setup during CUDA graph capture instead of taking a stable unfused path, potentially breaking capture; eager serving also incurs this work at every layer. Cache a permanent decline sentinel or disable this fusion after the first failed initialization.

Useful? React with 👍 / 👎.

import torch
import torch.distributed as dist

_HIP = ctypes.CDLL("libamdhip64.so")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move direct HIP bindings behind the third-party boundary

This new module loads and binds the vendor HIP runtime directly from ops/communication, bypassing the repository's required third-party boundary and registration path. Move the ABI wrapper under tokenspeed_kernel/thirdparty and expose it to ops through the established kernel registration/import mechanism so vendor-specific integration remains isolated.

AGENTS.md reference: AGENTS.md:L36-L37

Useful? React with 👍 / 👎.

return eligible


def triton_shmem_allreduce_residual_rmsnorm(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document arguments and returns for the public fused API

This function is exported through __all__ as a new public API but has no docstring describing its state and tensor arguments, optional output-buffer ownership, or returned tensors. Add an Args/Returns docstring so callers can safely use the output and lifetime contract.

AGENTS.md reference: AGENTS.md:L44-L44

Useful? React with 👍 / 👎.

@Jeremy-wj
Jeremy-wj force-pushed the perf/amd-triton-shmem-ar-rmsnorm branch 2 times, most recently from 14f6fdc to aba2ba6 Compare August 7, 2026 18:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aba2ba623e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# Slot 0 is scheduler-owned and can contain live request state. Every
# padded graph row must use the reserved sink row, which remains neutral
# across capture and replay for attention and sampling consumers.
sentinel = int(self.config.max_req_pool_size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep padded req-pool indices in page-table range

When a padded CUDA graph is replayed on a single-table attention backend, this sentinel is later passed to gather_page_table_with_padding, whose kernel uses each req_pool_indices value as a row index into the supplied page_table. That table is the batch-ordered DraftPageStaging.table, allocated with only max_bs rows, while config.max_req_pool_size is the reserved sink after the scheduler-owned range (max_bs + 1), so any replay with padded_bs > bs can read past the page-table buffer instead of using the dummy row. Keep the metadata-facing padding index in range (or pad the page table to include the sink row) while still using the sink row only for pool-indexed runtime state.

Useful? React with 👍 / 👎.

Comment on lines +1191 to +1193
except Exception as exc: # noqa: BLE001 - decline rather than crash forward
logger.warning("triton_shmem AR+RMSNorm state creation failed: %s", exc)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make triton_shmem state declines collective

If the constructor fails on only one rank after the eligibility all_gather_object—for example from a rank-local CUDA allocation/OOM or HIP runtime error—this catch returns None only there, while peers can cache a live triton_shmem state. The next fused norm then splits the group between the NCCL all_reduce fallback on the failed rank and Triton shmem barriers on the others, which can hang the process group; either let the exception abort all ranks or exchange a success flag so every rank takes the same fallback path.

Useful? React with 👍 / 👎.

Jeremy-wj and others added 3 commits August 7, 2026 22:43
Add a graph-safe MI350X backend with coarse HIP-IPC buffers while preserving Iris-first defaults and complete unfused fallback. Harden capture and topology guards, and cover the supported multi-GPU paths with focused correctness tests and a reproducible benchmark.

Signed-off-by: Jeremy Wang <w.jeremy220@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Jeremy Wang <w.jeremy220@gmail.com>
Signed-off-by: Jeremy Wang <w.jeremy220@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Jeremy Wang <w.jeremy220@gmail.com>
Signed-off-by: Jeremy Wang <w.jeremy220@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Jeremy-wj
Jeremy-wj force-pushed the perf/amd-triton-shmem-ar-rmsnorm branch from aba2ba6 to c204cbc Compare August 7, 2026 22:46
@qedawkins

Copy link
Copy Markdown
Contributor

@Jeremy-wj can you give a bit more detail behind choosing PyTorch symmetric memory over Iris? Mostly for my own edification since behind the scenes Iris does a lot of the IPC setup for us.

@Jeremy-wj

Copy link
Copy Markdown
Author

@qedawkins For IPC setup the new backend uses the existing pytorch symmetric memory API to set up symmetric memory, which already had functions set up in triton.py. HIP IPC is set up manually to ensure coarse-grained allocations (needed for performance) in _coarse_shmem.py and hip_ipc.py. All of this is enabled via setting the env vars TS_ARNORM_BACKEND and TS_TRITON_SHMEM_COARSE (default on).

If you were referring to motivation, the general idea is that pytorch symmetric memory is directly integrated as part of the torch+rocm stack and doesn't introduce external dependencies.

@qedawkins qedawkins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply, a few codex driven comments below. There was some offline discussion about motivation behind a new comms backend so I'll defer to those folks if they want to add details here

"""
world_size = group.size()
rank = dist.get_rank(group)
tensor = torch.empty(shape, dtype=dtype, device=device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial torch.empty allocation happens before the first cross-rank error exchange. If it fails on only one rank, that rank enters the fine-grained fallback while the other ranks block in all_gather_object, leaving the ranks in different collectives and deadlocking. Could we include allocation failure in the collective status protocol so every rank decides on the fallback together? A test injecting a rank-local allocation failure would also cover this path.

help="Enable allreduce fusion for improved decode performance. Auto-enabled on supported single-node TP configurations.",
)
parser.add_argument(
"--disable-allreduce-fusion",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be represented as a single tri-state option, such as --allreduce-fusion={auto,on,off}, instead of two independent booleans? The current representation has three valid states plus the invalid case where both flags are set. A tri-state would model the intended behavior directly and match the existing auto-mode configuration pattern in this file.

"are mutually exclusive"
)

arnorm_backend = os.environ.get("TS_ARNORM_BACKEND", "auto").strip().lower()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid duplicating ARNorm backend selection and triton_shmem policy in ServerArgs? tokenspeed-kernel already owns _arnorm_backend, backend dispatch, state eligibility, and per-call capability checks, while ordinary communication uses the existing AutoBackend/can_run pattern for this responsibility. The missing information appears to be runtime scheduling context such as DP or speculative execution guarantees. Could that context be passed to an ARNorm backend preflight API instead of parsing TS_ARNORM_BACKEND and hard-coding one backend's policy here? This would keep backend names and capabilities behind the backend boundary and avoid changing ServerArgs for every implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants