Skip to content

[Kernel] Feed merged qkv into Kimi K3 KDA attention via a vllm-ascend-local kda_attention_qkv op - #15143

Closed
Levi-JQ wants to merge 1 commit into
vllm-project:releases/v0.26.0rcfrom
coder-fny:conv1d
Closed

Levi-JQ wants to merge 1 commit into
vllm-project:releases/v0.26.0rcfrom
coder-fny:conv1d

Conversation

@Levi-JQ

@Levi-JQ Levi-JQ commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does / why we need it?

NPU profiling of the Kimi K3 KDA (gated delta-net) layer shows the merged fused_qkv projection output is split into q/k/v just so the attention op can take three separate tensors, after which the pieces are re-concatenated (torch.cat) before the fused CausalConv1d. The front 3 Slices + 1 ConcatD add no semantic value — the conv weights are already packed as [conv_size, 3*C] and the kernel natively processes q/k/v together — and are part of a split → cat → conv → chunk layout round-trip that dominates the conv sub-graph time.

This PR keeps the merged qkv all the way to the conv instead of splitting it just to re-concatenate. It registers a vllm-ascend-local vllm::kda_attention_qkv custom op with the merged-qkv signature, using the same opaque-boundary mechanics as the upstream vllm::kda_attention op (the impl resolves the layer from no_compile_layers and calls its _forward eagerly). The shared upstream op signature is deliberately not changed, so the upstream op, its (q, k, v) schema, and the reference path on other platforms stay untouched and vllm-ascend keeps working against stock vLLM. The result is bit-identical (qkv == cat(split(qkv))).

The op is appended to compilation_config.splitting_ops at layer init so piecewise cudagraphs still split the compiled graph at the attention boundary (model construction runs before compilation; FULL cudagraph and eager modes don't consult the list).

The change is pure Python (vllm_ascend/ops/kimi_kda.py only) — no kernel or .so rebuild required.

Does this PR introduce any user-facing change?

Additive only:

  • New custom op vllm::kda_attention_qkv (registered on the vllm-ascend side); the upstream vllm::kda_attention op and its (q, k, v) schema are untouched.
  • AscendKimiGatedDeltaNetAttention._forward (private method) signature changes to take the merged qkv.
  • vllm::kda_attention_qkv is appended to compilation_config.splitting_ops at KDA layer init (no user action needed).
  • No numerical change: bit-identical end to end.

How was this patch tested?

  • End-to-end A/B (editable install, single-node TP16/DP1/EP16 on Atlas A3, cudagraph FULL_DECODE_ONLY, dspark speculative decoding): the same greedy prompt run on pre-PR code and on this PR produces token-identical outputs.
  • Profile comparison over the same 128-token decode window (/start_profile → identical decode request → /stop_profile, torch_npu.profiler.analyse, rank0 op_statistic.csv): the ConcatD / CausalConv1d / RecurrentKda numbers above; CausalConv1d and RecurrentKda invocation counts and durations flat, confirming conv and SSM run exactly as before (pure I/O optimization).
  • No unit test was added: the change is a graph-level I/O re-plumbing verified bit-identical end to end; there is no new kernel or numerical code path to assert on.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi! Thank you for contributing to the vLLM Ascend project. The following points will speed up your PR merge:‌‌

  • A PR should do only one thing, smaller PRs enable faster reviews.
  • Every PR should include unit tests and end-to-end tests ‌to ensure it works and is not broken by other future PRs.
  • Write the commit message by fulfilling the PR description to help reviewer and future developers understand.

If CI fails, you can run linting and testing checks locally according Contributing and Testing.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request optimizes the Kimi K3 KDA attention layer by removing unnecessary I/O reshaping operations (split/concat) surrounding the fused CausalConv1d. By updating the underlying NPU operator to natively handle the split q/k/v output layout, the PR eliminates significant latency in the attention sub-graph while maintaining compatibility with existing workflows.

Highlights

  • Op Optimization: Eliminated redundant slice and concat operations around the fused CausalConv1d in the Kimi K3 KDA attention layer, significantly reducing overhead.
  • C++ Kernel Update: Updated the CausalConv1d operator definition and shape inference to support an optional split_qkv mode, allowing the kernel to output q, k, and v tensors directly.
  • Torch Binding Expansion: Added a new 3-output variant (npu_causal_conv1d_custom_3io) to the torch bindings to support the optimized layout.
  • Build System Improvement: Added a SKIP_BUILD_ACLNN environment variable to allow skipping the ACLNN build process during setup.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

Suggested PR Title:

[Ops][Feature] Support split-output variant for CausalConv1d in KDA

Suggested PR Summary:

### What this PR does / why we need it?
This PR introduces a split-output variant of the `CausalConv1d` operator (`npu_causal_conv1d_custom_3io`) to support KDA. By writing `q`, `k`, and `v` directly into three separate pre-allocated tensors, it eliminates the need for external chunking, rearranging, and contiguous operations, improving performance. The PR updates the operator definition, shape inference, PyTorch bindings, and meta functions, and refactors the KDA attention forward pass to pass the merged `qkv` tensor directly.

However, there are critical issues in the implementation:
1. In `npu_causal_conv1d_custom_3io`, the `EXEC_NPU_CMD` call for `aclnnCausalConv1d` omits the first output `y`, which shifts the subsequent outputs (`y_q`, `y_k`, `y_v`) into incorrect parameter slots, leading to compilation or runtime errors. An undefined `at::Tensor()` placeholder must be passed for `y`. Additionally, the existing `npu_causal_conv1d_custom` function must be updated to match the new signature.
2. The shape inference logic in `InferShapeCausalConv1d` assumes the input tensor is exactly 2D. It should be made dimension-agnostic to handle other configurations robustly.

### Does this PR introduce _any_ user-facing change?
No, this is an internal optimization for the Ascend backend's KDA implementation.

### How was this patch tested?
No tests were added in this PR. It is recommended to add unit tests verifying the correctness of the new `npu_causal_conv1d_custom_3io` operator and its shape inference logic.

Comment thread csrc/torch_binding.cpp Outdated
Comment on lines +682 to +698
EXEC_NPU_CMD(aclnnCausalConv1d,
x,
weight,
bias_opt,
conv_state,
query_start_loc_opt,
cache_indices_opt,
initial_state_mode_opt,
num_accepted_tokens_opt,
activation_mode,
pad_slot_id,
run_mode,
/*split_qkv=*/true,
y_q,
y_k,
y_v
);

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.

critical

Adding the split_qkv attribute and optional outputs y_q, y_k, y_v to the CausalConv1d OpDef changes the generated aclnnCausalConv1d API signature. The generated signature expects all attributes and outputs in the exact order defined in the OpDef.

In npu_causal_conv1d_custom_3io, the first output y is omitted from the EXEC_NPU_CMD call, which shifts the remaining outputs (y_q, y_k, y_v) into the wrong parameter slots and leaves the last parameter missing. This will cause a compilation error or runtime crash.

To fix this, pass an undefined at::Tensor() for the unused y output.

Additionally, please note that the existing npu_causal_conv1d_custom function (not modified in this diff hunk) must also be updated to pass /*split_qkv=*/false and three undefined at::Tensor() placeholders for y_q, y_k, and y_v to match the new signature and avoid compilation failures.

    EXEC_NPU_CMD(aclnnCausalConv1d,
                    x,
                    weight,
                    bias_opt,
                    conv_state,
                    query_start_loc_opt,
                    cache_indices_opt,
                    initial_state_mode_opt,
                    num_accepted_tokens_opt,
                    activation_mode,
                    pad_slot_id,
                    run_mode,
                    /*split_qkv=*/true,
                    at::Tensor(), // y is unused when split_qkv=true
                    y_q,
                    y_k,
                    y_v
                );

Comment on lines +48 to +51
gert::Shape singleShape = *xShape;
singleShape.SetDimNum(2);
int64_t last = xShape->GetDim(1);
singleShape.SetDim(1, last / 3);

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.

high

The shape inference logic assumes that the input tensor x is always exactly 2D. If x is 3D or 4D (which is common in other configurations or future extensions), singleShape.SetDimNum(2) will incorrectly truncate the dimensions, and xShape->GetDim(1) will retrieve the sequence/token dimension instead of the channel dimension, leading to incorrect shape inference. Additionally, if x has fewer than 2 dimensions, GetDim(1) will cause an out-of-bounds access.

We should make the shape inference robust and dimension-agnostic by querying the actual number of dimensions and modifying the last dimension (the channel dimension).

Suggested change
gert::Shape singleShape = *xShape;
singleShape.SetDimNum(2);
int64_t last = xShape->GetDim(1);
singleShape.SetDim(1, last / 3);
gert::Shape singleShape = *xShape;
size_t dimNum = xShape->GetDimNum();
if (dimNum > 0) {
int64_t last = xShape->GetDim(dimNum - 1);
singleShape.SetDim(dimNum - 1, last / 3);
}

Keep fused_qkv's merged [N, 3*C] output all the way to the conv instead
of splitting it just to re-concatenate inside the op. Register a
vllm-ascend-local vllm::kda_attention_qkv custom op with the merged-qkv
signature (same opaque-boundary mechanics as the upstream
vllm::kda_attention: resolve the layer from no_compile_layers and call
its _forward eagerly) instead of changing the shared upstream op
signature — the upstream op, the reference path on other platforms, and
stock vLLM compatibility stay untouched.

The front 3 Slice + 1 ConcatD collapse to a single trim Slice; the
result is bit-identical (qkv == cat(split(qkv))). The op is appended to
compilation_config.splitting_ops at layer init so piecewise cudagraphs
still split the compiled graph at the attention boundary (model
construction runs before compilation; FULL cudagraph and eager modes
don't consult the list).
@Levi-JQ Levi-JQ changed the title [Feat][Ops] eliminate redundant slice/concat around fused CausalConv1d in Kimi K3 KDA [Kernel] Eliminate redundant slice/concat around the fused CausalConv1d in Kimi K3 KDA Sep 1, 2026
@Levi-JQ Levi-JQ changed the title [Kernel] Eliminate redundant slice/concat around the fused CausalConv1d in Kimi K3 KDA [Kernel] Feed merged qkv into Kimi K3 KDA attention via a vllm-ascend-local kda_attention_qkv op Sep 3, 2026
@Levi-JQ Levi-JQ closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants