Skip to content

[CUDA] GroupQueryAttention: opt-in tensor-core split-KV decode kernel - #33759

Open
Yuto Ashida (y-chan) wants to merge 5 commits into
microsoft:mainfrom
y-chan:perf/gqa-split-decode-main
Open

Yuto Ashida (y-chan) wants to merge 5 commits into
microsoft:mainfrom
y-chan:perf/gqa-split-decode-main

Conversation

@y-chan

@y-chan Yuto Ashida (y-chan) commented Oct 30, 2026 •

Copy link
Copy Markdown

Description

Adds an opt-in kernel for single-token decode in GroupQueryAttention, enabled with
ORT_GQA_SPLIT_DECODE=1 (off by default, so the default backend selection is unchanged).

It applies to fp16 decode with 8 query heads per KV head and head size 128, a shared BNSH KV cache,
no softcap / smooth softmax / head sink / local window, and either RoPE over the full head
(non-interleaved) or packed QKV. When enabled and Flash can run, these shapes take the new kernel in
place of XQA, cuDNN SDPA and the Flash fast-decode path. Two kernels in
group_query_attention_split_decode.{h,cu}:

  • Prep: one warp per head applies QK-Norm (fp32 statistics, separate q/k epsilons), half-split RoPE,
    and appends K/V to the cache; this replaces UnpackRoPEAppend for these shapes.
  • Attention: one CTA per (batch, kv head, split). Each warp streams 8-key steps of K and V through a
    cp.async ring in shared memory (whole 256-byte rows, XOR-swizzled; 4 stages, or 3 where the opt-in
    shared memory is 99 KB) into tensor-core mma fragments (QK m16n8k16, PV m16n8k8), with the 8 query
    heads of the KV head as the rows of one tile, so K and V are read once per group. The warps of a CTA
    are combined in shared memory and the last CTA of a (batch, kv head) merges the split partials, so
    there is no combine kernel and no memset; with a single split the CTA writes the output directly.

CUDA graphs: the launched split count depends only on batch size, KV head count, KV buffer length
and SM count (as many splits as fill one wave of one CTA per SM, at most 64), so the grid is the same
in every replay. The number of splits actually used follows the context length on the device (128
keys per split at least); the remaining CTAs exit at once. The partials and counters are scratch
buffers of the run, allocated on its compute stream; the prep kernel zeroes the counters.

Motivation and Context

For one decode token the attention itself is short, and much of the time goes to fixed costs around
it. With QK-Norm (Qwen3-family models) the default XQA path runs UnpackRoPEAppend, a memset and the
XQA kernel; the Flash path runs UnpackRoPEAppend, two memsets, the split-KV kernel and a combine
kernel. On A100 at past 2048, per layer in a CUDA graph (nsys): XQA kernel 15.6 us +
UnpackRoPEAppend 5.1 + memset 2.1 + sequence lengths 1.9, vs split decode 14.7 + prep 2.7 + sequence
lengths 1.8. At longer contexts the split count scales with the SM count and the gap grows.

Results

Current numbers (batch 1 to 64 on A100, L40 and RTX PRO 6000, after the cp.async ring change) are
in this comment. The measurements below are from the first version of the kernel.

Earlier measurements (first version of the kernel)

A100 80GB SXM4, CUDA 12.9, fp16, batch 1, 32 query / 4 KV heads, head size 128, QK-Norm, RoPE.

48 GQA nodes with independent KV caches in one CUDA graph, GPU time per replay / 48 (us per layer):

past default (XQA) ORT_GQA_SPLIT_DECODE=1 Flash (ORT_ENABLE_XQA=0)
512 22.8 15.5 30.0
2048 26.9 18.8 40.9
8192 36.9 25.7 81.6

Without a CUDA graph (same model): 26.3 / 28.6 / 39.0 (default) vs 20.3 / 21.3 / 30.5 us.
Single op with the existing profile_gqa.py (kernel time per call, nsys; the KV cache stays in L2):
default 21.5 / 23.8 / 33.2 us + 2.3 us memset vs 17.1 / 19.5 / 24.0 us and no memset.

Without QK-Norm the default path is XQA (and Flash fast decode with ORT_ENABLE_XQA=0); in a CUDA
graph with packed QKV the opt-in kernel was 15.0 / 19.4 / 24.6 us vs XQA 23.7 / 31.0 / 36.0 us.

End to end, on a non-public decoder with the Qwen3-30B-A3B architecture (48 layers, 32/4 heads,
QK-Norm, CUDA graph, batch 1, 300 tokens): 5.84 -> 5.00 ms/token (171.3 -> 200.1 tokens/s) with
ORT_GQA_SPLIT_DECODE=1, generated text unchanged (only timestamps in the transcript moved).

Other GPUs, same CUDA-graph benchmark (us per layer, past 512 / 2048 / 8192). On SM >= 9 the
cuDNN SDPA column is what ORT_ENABLE_XQA=0 selects:

GPU default (XQA) ORT_GQA_SPLIT_DECODE=1 cuDNN SDPA Flash
L40 (SM 8.9) 13.4 / 18.4 / 39.3 8.0 / 14.9 / 34.1 - 19.7 / 33.4 / 78.8
RTX PRO 6000 Blackwell Max-Q (SM 12.0) 15.2 / 16.4 / 26.0 8.7 / 11.4 / 21.2 11.0 / 12.9 / 22.2 18.5 / 28.8 / 56.9

Without a CUDA graph: L40 14.7 / 19.3 / 40.6 (default) vs 12.0 / 16.9 / 36.2 us; RTX PRO 6000
22.9 / 23.7 / 28.0 (default) vs 17.2 / 20.0 / 26.7 us (cuDNN SDPA 40.0 / 31.3 / 32.6).

Batch size (CUDA graph, same shapes; us per layer at past 512 / 2048 / 8192; below 100% means the
opt-in kernel is faster):

A100:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 15.5 / 16.9 / 26.7 24.3 / 26.4 / 38.0 64% / 64% / 70%
4 17.4 / 30.4 / 68.9 24.6 / 32.3 / 61.4 71% / 94% / 112%
8 25.9 / 43.5 / 112.3 27.5 / 41.4 / 96.1 94% / 105% / 117%
16 31.0 / 66.4 / 209.2 29.5 / 59.7 / 173.5 105% / 111% / 121%
32 45.4 / 116.3 / 407.4 45.4 / 104.5 / 347.2 100% / 111% / 117%
64 70.0 / 190.7 / 687.0 68.4 / 176.2 / 613.5 102% / 108% / 112%

L40:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 8.0 / 14.9 / 34.1 13.4 / 18.4 / 39.2 60% / 81% / 87%
4 14.7 / 34.0 / 107.6 18.4 / 37.1 / 110.9 80% / 92% / 97%
8 21.9 / 56.9 / 193.4 24.9 / 61.9 / 214.2 88% / 92% / 90%
16 35.5 / 103.6 / 371.8 40.1 / 113.9 / 416.7 89% / 91% / 89%
32 62.4 / 199.3 / 735.4 66.7 / 219.4 / 818.6 94% / 91% / 90%
64 115.6 / 386.2 / 1454.7 124.5 / 428.7 / 1627.0 93% / 90% / 89%

RTX PRO 6000:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 8.9 / 11.3 / 21.2 14.4 / 16.3 / 25.9 62% / 69% / 82%
4 10.4 / 20.1 / 55.5 15.9 / 22.9 / 54.1 65% / 88% / 103%
8 13.4 / 32.8 / 102.1 18.3 / 32.9 / 95.2 73% / 100% / 107%
16 20.8 / 55.4 / 193.8 22.7 / 53.7 / 176.9 92% / 103% / 110%
32 31.2 / 92.8 / 338.7 32.4 / 94.2 / 339.4 96% / 99% / 100%
64 52.3 / 176.6 / 668.3 53.4 / 178.2 / 668.2 98% / 99% / 100%

The gain is largest for batch 1, where fixed costs dominate. With more batch x context the work is
bound by reading the KV cache, and there XQA reads it faster on A100 (batch 64 / past 8192: about
1.75 TB/s vs 1.56 TB/s for the opt-in kernel), so the opt-in kernel is up to 1.21x slower on A100 and
up to 1.10x on RTX PRO 6000 (where cuDNN SDPA is the fastest or tied at large batch). On L40 both
saturate its lower bandwidth and the opt-in kernel stays slightly ahead.

End to end on the other GPUs (same decoder architecture, 300 tokens, CUDA graph):

GPU default ORT_GQA_SPLIT_DECODE=1
L40 6.15 ms/token (162.5 tokens/s) 5.71 ms/token (175.3 tokens/s)
RTX PRO 6000 (*) 4.45 ms/token (224.8 tokens/s) 4.09 ms/token (244.7 tokens/s)

(*) A different export of the same architecture (block-wise QMoE scales, 4096-token KV buffer). On L40
the generated text was unchanged; on RTX PRO 6000 the output showed small numerical differences
(a few timestamps and punctuation marks), as expected from a different accumulation order.

Tests

  • New TestGQASplitDecode (test_gqa.py), 19 parity cases: past lengths 1 to 8191 including totals
    on the 64 / 128 / 256 boundaries, full caches, a KV buffer 8000 rows longer than the context, batch
    1 to 40, 16/2, 32/4 and 64/8 heads, with and without QK-Norm, rotary, packed QKV and explicit
    position_ids. Each case runs with ORT_GQA_SPLIT_DECODE=1 and with the default backends against
    the PyTorch reference, and checks the debug info (SplitDecode=1 only with the flag). The existing
    GQA tests use other group sizes.
  • test_gqa_split_decode_cuda_graph_replay: one captured graph replayed while the context lengths
    change on the device only (across split counts and split edges), compared with the default
    backends at every step.
  • test_gqa_split_decode_concurrent_runs: one session run from four threads, each output compared
    with that thread's serial result.
  • L40 (sm89, CUDA 12.9, Release, reduced build): TestGQASplitDecode 21 passed (also on RTX PRO 6000,
    sm120). test_gqa.py on L40: 975
    passed, 4 failed, 1 skipped; all four also fail without this change: the two A100 failures listed
    below, and two INT8 KV-cache cases ("No available GroupQueryAttention kernel supports the quantized
    KV cache").
  • First version, A100 (sm80, CUDA 12.9, Release): onnxruntime_provider_test --gtest_filter=*GroupQueryAttention* 113 passed, onnxruntime_test_all 31 passed. test_gqa.py:
    751 passed, 194 failed, 3 skipped;
    the failures are unrelated and also occur without this change: 192 FP8 KV-cache XQA cases expect
    XQA, which is not available on SM 8.0; TestFlashGQA::test_gqa_decode_bidirectional is rejected by
    the input check; TestGQAAttentionBias::test_windowed_cache_attention_bias_is_rejected hits the
    KV-capacity check before the attention-bias check, so the expected message differs.

How to reproduce

cd onnxruntime/test/python/transformers
for s in 0 1; do
  ORT_GQA_SPLIT_DECODE=$s nsys profile -t cuda,nvtx -o gqa_$s --export=sqlite \
    python profile_gqa.py --mode fp16 --batch-size 1 --num-heads 32 --kv-num-heads 4 --head-size 128 \
      --qk-norm --past-sequence-length 2048 --max-sequence-length 2112
  python parse_nsys.py gqa_$s.sqlite --nvtx-range benchmark_fp16_qknorm --pattern '%'
done

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 30, 2026 03:18
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@y-chan Yuto Ashida (y-chan) changed the title [CUDA] GroupQueryAttention: opt-in single-token split decode kernel [CUDA] GroupQueryAttention: opt-in tensor-core split-KV decode kernel Oct 30, 2026
@y-chan

Copy link
Copy Markdown
Author

At larger batch × context the opt-in kernel is currently slower than XQA on A100 (up to 1.21x) and RTX PRO 6000 (up to 1.10x), as shown in the batch table. In that regime it reads the KV cache at a lower rate than XQA (about 1.56 vs 1.75 TB/s on A100). I'm looking into a faster KV-streaming path and will update this PR with the results.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Opt-in (default off) fp16 split-KV decode kernel for GQA (group 8, head 128). The eligibility gate is tight (fp16, seq 1, shared BNSH cache, no softcap/sink/window/smooth-softmax, RoPE non-interleaved or packed), the kernel numerics are right in my A100 run (max error 6e-5 vs an fp64 reference, tails included), and the kernel source compiles for sm_75 through sm_120.

Blocking: the partials/counters are persistent members of the kernel object (group_query_attention.h L64-72) and the mutex only covers allocation, not use. Concurrent Session::Run calls on one session share the node instance but run on different streams, so two overlapping runs share one scratch area and one last-CTA counter array (inline comment). I reproduced wrong/NaN output and leftover non-zero counters at kernel level; per-run scratch gave 0 failures. Fix: per-run stream-tagged GetScratchBuffer and zero the counters on the run's stream (capturable).

Suggestion: TestGQASplitDecode cannot detect a missing dispatch, because the debug info prints FLASH_ATTENTION for the split path too (inline). Also uncovered: position_ids, exact 64/128 length boundaries / full cache, concurrent runs and graph replay. The slowdown vs XQA at large batch x context on A100 is acknowledged by the author (acceptable for an opt-in switch).

Validation: CI: only license/cla at the head (fork PR); no CUDA/Windows/plugin pipeline ran. Local: A100 (sm_80) standalone harness built from the PR's .cu (sequential correctness; overlap on shared vs private scratch) plus a compile-only check for sm_75/80/86/89/90/100/120 (CUDA 13.0, Linux). Not run: full ORT build, test_gqa.py, end-to-end concurrent Run, CUDA-graph replay, runtime on sm_89/90/100/120, Windows.

@y-chan

Copy link
Copy Markdown
Author

Updated the PR:

  • Review fixes (per-run scratch, dispatch check and the added tests); see the inline replies.
  • The attention kernel now streams K and V through a per-warp cp.async ring in shared memory (whole 256-byte rows,
    XOR-swizzled, 128-byte aligned), and the split count keeps all CTAs in one wave. With that, the opt-in kernel is
    no longer slower than XQA at large batch x context: opt-in / XQA is 0.58 to 1.00 over all 54 points below.

Same CUDA-graph benchmark as in the description (32 query / 4 KV heads, head size 128, QK-Norm, RoPE, fp16; us per
layer at past 512 / 2048 / 8192; below 100% means the opt-in kernel is faster). A100 is the mean of two runs, the
others one run.

A100 80GB SXM4:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 14.7 / 18.0 / 24.6 24.1 / 25.1 / 36.9 61% / 71% / 66%
4 16.7 / 23.7 / 53.5 23.8 / 32.3 / 60.6 70% / 73% / 88%
8 20.2 / 34.8 / 89.2 26.2 / 41.0 / 96.0 77% / 85% / 93%
16 21.5 / 51.3 / 164.7 29.5 / 60.2 / 173.8 73% / 85% / 95%
32 41.4 / 95.9 / 311.1 46.6 / 104.8 / 345.0 89% / 92% / 90%
64 57.5 / 165.6 / 600.5 68.3 / 176.2 / 613.2 84% / 94% / 98%

L40:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 8.1 / 14.8 / 33.3 13.5 / 18.7 / 39.2 60% / 79% / 85%
4 14.6 / 32.2 / 99.4 18.5 / 36.9 / 110.9 79% / 87% / 90%
8 21.1 / 55.4 / 190.0 25.1 / 62.0 / 213.9 84% / 89% / 89%
16 34.3 / 101.9 / 370.3 40.3 / 114.4 / 416.5 85% / 89% / 89%
32 58.1 / 193.2 / 726.6 66.7 / 219.5 / 819.2 87% / 88% / 89%
64 110.4 / 380.4 / 1447.4 125.0 / 429.2 / 1627.0 88% / 89% / 89%

RTX PRO 6000 Blackwell Max-Q:

batch ORT_GQA_SPLIT_DECODE=1 default (XQA) opt-in / XQA
1 8.5 / 11.9 / 20.9 14.7 / 16.4 / 26.1 58% / 73% / 80%
4 10.9 / 18.9 / 50.2 16.0 / 23.0 / 54.1 68% / 82% / 93%
8 13.8 / 29.0 / 91.3 18.3 / 32.9 / 94.9 75% / 88% / 96%
16 18.9 / 50.1 / 173.4 22.7 / 53.7 / 176.8 83% / 93% / 98%
32 28.1 / 89.8 / 335.4 32.4 / 94.2 / 338.9 87% / 95% / 99%
64 49.6 / 173.5 / 665.4 53.4 / 178.0 / 667.7 93% / 97% / 100%

The end-to-end numbers in the description were measured with the first version and have not been re-run.

Yuto Ashida (y-chan) and others added 4 commits November 5, 2026 15:13
For one-token decode with 8 query heads per KV head and head size 128
(fp16, shared BNSH cache, no softcap / sink / local window, RoPE over the
full head or packed QKV), ORT_GQA_SPLIT_DECODE=1 runs a dedicated kernel
in place of XQA, cuDNN SDPA and the Flash kernels. Off by default.

- Prep kernel: one warp per head applies QK-Norm (separate q/k epsilons),
  half-split RoPE and appends K/V to the cache, replacing
  UnpackRoPEAppend.
- Attention kernel: one CTA per (batch, kv head, split). Each warp streams
  8-key steps of K and V from global memory into tensor-core mma fragments
  with the 8 query heads as the rows of one tile, so K/V are read once per
  group. Warps are combined in shared memory and the last CTA of a
  (batch, kv head) merges the split partials, so there is no combine
  kernel and no memset.
- The launched split count depends only on batch, KV heads, buffer length
  and SM count, so the grid is fixed across CUDA graph replays; the splits
  actually used follow the context length on the device.

A100, 32/4 heads, QK-Norm, 48 layers in one CUDA graph, per layer:
XQA 22.8 / 26.9 / 36.9 us vs 15.5 / 18.8 / 25.7 us at past 512 / 2048 /
8192.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add TestGQASplitDecode: one-token decode with past lengths 1 to 8191,
a KV buffer 8000 rows longer than the context, batch 1-4, 8 query heads
per KV head, head size 128, with and without QK-Norm, rotary and packed
QKV. Each case runs with ORT_GQA_SPLIT_DECODE=1 (and checks that the
Flash backend, which hosts split decode, was selected) and with the
default backends, against the PyTorch reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c ring

At large batch x context the opt-in split decode kernel streamed KV at ~1.56 TB/s on A100
(XQA ~1.75 TB/s). Two causes:

* Fragment loads straight from global memory touch only 64 bytes of each 128-byte line of K per
  request, which caps an SM at ~28 GB/s regardless of prefetch depth. K and V now go through a
  per-warp cp.async ring in shared memory (4 steps on sm_80/sm_90, 3 where the opt-in shared
  memory is 99 KB), copied as whole 256-byte rows and read back with an XOR swizzle. The ring is
  128-byte aligned: at the default offset after the static shared variables every row straddles two
  lines, which cost ~13% of the streaming rate.
* The split count rounded the CTA count up past the SM count (e.g. batch 8: 4 splits x 32 pairs =
  128 CTAs on 108 SMs), adding a short second wave. It now fills one wave without exceeding it, and
  with more pairs than SMs uses one split unless the last wave would be short. A context that is not
  split is written straight to the output, without partials, counter or merge.

A100, CUDA graph, 32 query / 4 KV heads, head 128, fp16, QK-Norm + RoPE, us per layer (new / XQA):
batch 64 / past 8192: ~600 / ~613; batch 32 / past 8192: 311 / 345; batch 16 / past 8192: 165 / 174;
batch 1 / past 8192: 24.6 / 37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eck in tests

Allocate the split partials and counters per run on the run's compute stream instead of keeping them on the
kernel object: concurrent Run() calls of one session share the kernel but run on their own streams, and the
shared buffers let one run's last-CTA election and merge see another run's partials. The prep kernel, which
runs first on the same stream, now zeroes the counters, so no memset is added and the kernel no longer has to
leave them at zero.

Report the path in the attention kernel debug info (SplitDecode=1, NumSplits = launched splits) and assert it
in TestGQASplitDecode, which could not tell split decode from the regular Flash path. Add cases for totals on
the 64/128-key boundaries, a full cache and explicit position_ids, a CUDA graph replay with context lengths
that change on the device only, and concurrent runs on one session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@tianleiwu Tianlei Wu (tianleiwu) 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.

The original per-run ownership race and dispatch-observability concerns are addressed: scratch is now per invocation and stream-tagged, prep resets the counters, and the tests distinguish split decode with SplitDecode=1 while covering additional boundary/replay cases.

Two localized follow-ups remain below: avoid allocating regular-Flash workspace that the split-decode early return never uses, and make worker exceptions fail the concurrent-run regression.

Fresh validation: an AST-based probe using the actual worker function reproduced four raised worker runs while the test's final failures == [] condition still passed. The Python file parses and the diff check passes. The current head reports only CLA; no fresh native ORT/CUDA build, concurrent Session.Run execution, or CUDA Graph replay was performed. No approval submitted.


auto cuda_stream = Stream(context);
if (softmax_lse_accum_bytes > 0) {
if (softmax_lse_accum_bytes > 0 && !data.use_split_decode) {

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.

[P2] Skip the regular-Flash allocations, not just their memsets, on split decode

When data.use_split_decode is true, the three GetScratchBuffer calls immediately above still reserve the normal Flash LSE and accumulator buffers in addition to the new partials/counters. FlashAttention() then returns from LaunchGqaSplitDecode before any regular-Flash consumer can use these buffers. This leaves unnecessary allocator work and peak device memory in the new performance path (including potentially sizeable split accumulators). Please guard the regular-Flash workspace sizing/allocation block with !data.use_split_decode, retaining only values actually required by the split path.

if not torch.equal(out, expected):
failures.append((past_t.tolist(), (out.float() - expected.float()).abs().max().item()))

pool = [threading.Thread(target=loop, args=w) for w in work]

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.

[P2] Propagate exceptions from the concurrent workers into the test result

threading.Thread.join() does not re-raise worker exceptions. If runner.step() raises a CUDA/ORT error, the worker exits before appending anything to failures, so the final assertEqual(failures, []) can pass even when every concurrent run failed. This file also supports direct execution through unittest.main(), which does not turn those thread exceptions into test errors. I reproduced this with the exact worker function: 4/4 injected run failures left failures empty. Please use futures and call result() for each worker, or collect worker exceptions and fail on them after joining.

…il the concurrent test on worker errors

Split decode returns before the regular Flash kernels run, so do not size or
allocate the softmax LSE and split accumulator buffers for it. In the
concurrent-run regression, run the workers through a ThreadPoolExecutor and
collect each future's result, so an exception raised by Session.Run in a worker
fails the test instead of leaving the failure list empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@y-chan

Copy link
Copy Markdown
Author

Thanks for the review. Both points are fixed in 1fe927b: the split decode path no longer sizes or allocates the regular Flash LSE/accumulator buffers, and the concurrent-run test now runs the workers through a ThreadPoolExecutor and collects each result(), so a worker exception fails the test. Could a maintainer approve the workflow runs for this head? That would also give the CUDA build/test coverage you noted is still missing.

This branch has not been deployed

No deployments
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.

3 participants