Skip to content

Optimize quantize_int8_rowwise_convrot64 launch config (up to 4.9x on the generic path) - #80

Open
TheLegendOfKitty wants to merge 6 commits into
Comfy-Org:mainfrom
TheLegendOfKitty:optimize-convrot64-quantize-launch
Open

Optimize quantize_int8_rowwise_convrot64 launch config (up to 4.9x on the generic path)#80
TheLegendOfKitty wants to merge 6 commits into
Comfy-Org:mainfrom
TheLegendOfKitty:optimize-convrot64-quantize-launch

Conversation

@TheLegendOfKitty

@TheLegendOfKitty TheLegendOfKitty commented Jul 20, 2026

Copy link
Copy Markdown

The generic launch path of quantize_int8_rowwise_convrot64 used a fixed 1024-thread block and a full-row shared-memory buffer for every K, reaching only 9–35% of peak bandwidth on Ampere (75% of lanes idle at K=1024). This PR right-sizes the block to the actual group count for K≤2048 and adds a two-pass chunked kernel with bounded shared memory for 2048<K≤4096, while K>4096 keeps the existing path unchanged (chunking measured ~8% slower at K=8192 — the second pass's re-read loses L2 residency at high row counts — so it is gated off there). Outputs are bitwise-identical to the current kernel, verified against the shipped v0.2.22 wheel across K∈{1024, 2048, 2304, 3072, 3584, 4096, 8192}, fp32/fp16/bf16, and adversarial inputs (all-zero rows, near-dtype-max NaN cascades — preserving the overflow-clamp semantics from #66), and the kernel gains direct test coverage it previously lacked. Measured on an RTX 3090: 4.9x at K=1024, 2.5x at K=2048, 1.12x at K=4096 standalone; end-to-end on an int8 ConvRot checkpoint (Anima) at 1536x1536 this is 0.842→0.777 s/it eager and 0.797→0.729 s/it with torch.compile.

TheLegendOfKitty and others added 2 commits July 20, 2026 03:00
quantize_int8_rowwise_convrot64's generic launch branch (K not in
{256, 2560, 6144}, M>1) always used a fixed 1024-thread block
regardless of K. At K=1024 that leaves 768 of 1024 threads idle every
iteration (only (K/256)*64 = 256 of them ever have `active` true),
and the full-row shared-memory buffer (row_buf, K floats) both grows
with K and forces roughly one resident block per SM at larger K.
Device-track profiling on an RTX 3090 (SM86) showed the kernel
achieving only 9-35% of the GPU's peak memory bandwidth depending on
K, despite being clearly memory-bound (~7 FLOP/byte vs the card's
~38 FLOP/byte ridge point).

Three changes, all confined to the launcher's generic branch (the
M==1 path and the K in {256, 2560, 6144} special cases are untouched):

1. K <= 2048: right-size the launch to
   threads = min(1024, (K/256)*64), reusing the existing single-pass
   kernel body unchanged -- only the launcher's block-size selection
   changes (new template instantiations at 128/192/256/320/384/448/
   512 threads, dispatched via a switch on K/256, same pattern the
   existing 256/640/768-thread special cases already use).

2. 2048 < K <= 4096: a new two-pass "chunked" kernel
   (quantize_int8_rowwise_convrot64_chunked_kernel) that never
   buffers more than one 2048-element chunk's rotated values at a
   time (16KB shared memory total at 512 threads, independent of K).
   Pass 1 rotates each chunk and folds its local max into the row's
   abs_max (discarding the rotated values); pass 2 re-reads each
   chunk (hopefully L2-resident) and re-rotates it -- deterministic
   fp32 math, so this reproduces pass 1's values exactly -- fusing
   the quantize step into the last butterfly stage so pass 2 needs no
   buffer either.

3. K > 4096: kept on the ORIGINAL, unmodified single-pass kernel at a
   fixed 1024 threads -- verified byte-identical dispatch to stock.
   The chunked kernel from (2) was initially applied to all K > 2048,
   but measured a reproducible ~8% regression at K=8192 (M=9216):
   with 9216 blocks in flight, pass 2's re-read no longer reliably
   hits L2 (the resident-rows working set exceeds the RTX 3090's 6MB
   L2 at that row count), so the recompute becomes a genuine second
   DRAM read instead of an L2 hit, and the occupancy gain isn't
   enough to pay for that. Gated to K<=4096, where the smaller
   per-row footprint keeps the working set L2-resident and the trade
   stays net-positive. See the PR description for the full numbers;
   occupancy/L2-aware chunk sizing that adapts to M is noted there as
   possible future work.

Verified bitwise-identical (qdata and scale, torch.equal) against the
unmodified kernel across all real and boundary shapes -- K in
{1024, 2048, 3072, 3584, 4096, 8192} (3072/3584 specifically exercise
the chunked kernel's partial-last-chunk / inactive-lane path, since
3072/256=12 and 3584/256=14 groups are not multiples of the chunked
kernel's 8-groups-per-chunk width), M in {1, 9216, 9217}, fp16/fp32/
bf16, and an adversarial suite (all-zero rows, near-dtype-max rows
that overflow to NaN mid-butterfly, denormal-heavy rows, single-huge-
outlier rows) -- 64/64 cases, zero mismatches. Full pytest suite
(with the CUTLASS submodule initialized) passes the same 23
pre-existing failures / 101 skipped as the unmodified tree (fp8/
nvfp4/mxfp8 and torch.compile-integration issues on this box,
unrelated to this file), plus all new tests in the companion commit.

Preserves PR Comfy-Org#66's finite-max clamp (finite_absmax_for_int8_scale)
unchanged. PR Comfy-Org#65 (ConvRot int4/W4A4 support) also touched this file,
but only an unrelated int64-overflow fix in the plain (non-ConvRot)
quantize_int8_rowwise_kernel -- no overlap with the convrot64 kernel
or launcher this commit changes.

Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
This kernel previously had no direct tests (only indirect coverage
through int8_linear / TensorWiseINT8Layout convrot weight-quantize
tests). Added alongside the launch-config fix in the previous commit
to pin down the kernel's contract independent of that change:

- shape/dtype matrix across the launcher's special cases, the
  right-sized single-pass branch, the chunked branch, and the K>4096
  branch (K in 256/512/1024/1536/2048/2560/3072/3584/4096/6144/8192,
  M in 1/4/63/9216, bf16/fp16/fp32) -- K=3072 and K=3584 specifically
  target the chunked kernel's partial-last-chunk path (12 and 14
  groups respectively, not multiples of its 8-groups-per-chunk width,
  so some lanes are masked `active=false` in the final chunk
  iteration, unlike K=4096's exact 2x8-group split)
- determinism (two calls on the same input are bitwise identical),
  including across the resident/chunked and chunked/K>4096 boundaries
- all-zero-row scale floor (1e-30), including a degenerate row mixed
  in among normal rows
- near-dtype-max rows stay finite (exercises PR Comfy-Org#66's clamp on the
  shapes/launch configs this change touches)
- CUDA vs the eager backend's reference (dense Hadamard matmul
  rotation) within a documented tolerance -- not bitwise, since the
  eager path's dense-matmul rotation sums the same terms in a
  different order than the CUDA kernel's explicit 4-stage radix-4
  butterfly, so floating-point non-associativity puts a small,
  bounded fraction of int8 codes on the other side of a rounding
  boundary (measured ~2-4% of codes, max +/-2, stable across K)

154 tests, all passing; ruff clean.

Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

✅ All contributors have signed the CLA. Thank you! This PR is ready to be merged.
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@TheLegendOfKitty, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 79460086-39cd-4080-bdd0-f311769a9260

📥 Commits

Reviewing files that changed from the base of the PR and between 38469fe and c957c76.

📒 Files selected for processing (2)
  • comfy_kitchen/backends/cuda/ops/int8_linear.cu
  • tests/test_convrot64_quantize.py
📝 Walkthrough

Walkthrough

Changes

The CUDA ConvRot64 INT8 quantization path adds a two-pass chunked kernel for 2048 < K <= 4096, size-aware launch dispatch, and CUDA tests covering numerical, determinism, boundary, and validation behavior.

ConvRot64 quantization

Layer / File(s) Summary
Chunked rotation and quantization kernel
comfy_kitchen/backends/cuda/ops/int8_linear.cu
Adds fused Hadamard-stage quantization helpers and a two-pass chunked kernel that reduces row scales before reloading and quantizing input chunks.
Size-aware kernel dispatch
comfy_kitchen/backends/cuda/ops/int8_linear.cu
Adds chunked shared-memory sizing, right-sized launches through K=2048, chunked launches through K=4096, and preserves the larger-K path.
CUDA behavior and numerical validation
tests/test_convrot64_quantize.py
Adds CUDA-gated tests for shapes, dtypes, divisibility, deterministic output, zero-row scale flooring, finite clamping, and eager-reference agreement.

Sequence Diagram(s)

sequenceDiagram
  participant Launcher
  participant ConvRotKernel
  participant Output
  Launcher->>ConvRotKernel: select kernel and thread count from K
  ConvRotKernel->>ConvRotKernel: rotate chunks and reduce row scales
  ConvRotKernel->>Output: quantize and store int8 values with scales
Loading

Suggested reviewers: comfyanonymous

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from comfyanonymous July 20, 2026 10:07

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@comfy_kitchen/backends/cuda/ops/int8_linear.cu`:
- Around line 1110-1183: Extract the duplicated load, four-value rotation,
synchronization, and FHT stages from the two passes into a shared device helper
near the relevant kernel code, such as convrot_load_rotate_group. Update both
pass 1 and pass 2 in the enclosing kernel to call it and continue consuming the
resulting values in buf1 for absmax accumulation and quantization respectively.
Preserve the existing active-group handling, buffer usage, synchronization
points, and numerical operation order.

In `@tests/test_convrot64_quantize.py`:
- Around line 80-97: Update the k parameter list in
TestConvrot64QuantizeDeterminism.test_two_calls_bitwise_equal to include 2304,
ensuring the advertised resident/chunked boundary coverage is actually exercised
while preserving the existing test cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b75d2917-5ea3-4a95-bbd3-df8758aae95f

📥 Commits

Reviewing files that changed from the base of the PR and between 05db654 and b41a280.

📒 Files selected for processing (2)
  • comfy_kitchen/backends/cuda/ops/int8_linear.cu
  • tests/test_convrot64_quantize.py

Comment thread comfy_kitchen/backends/cuda/ops/int8_linear.cu
Comment thread tests/test_convrot64_quantize.py
@TheLegendOfKitty

Copy link
Copy Markdown
Author

I have read and agree to the Contributor License Agreement

comfy-legal added a commit to Comfy-Org/comfy-cla that referenced this pull request Jul 20, 2026
TheLegendOfKitty and others added 4 commits July 20, 2026 03:22
Addresses CodeRabbit review on PR Comfy-Org#80 (2 comments):

1. quantize_int8_rowwise_convrot64_chunked_kernel's pass 1 and pass 2
   duplicated the exact same load + first-three-FHT-stages sequence
   verbatim. Extracted into a new device helper,
   convrot_load_rotate_group (templated on InputType, called
   identically by both passes, same active-group masking, same buf0/
   buf1 usage, same __syncthreads() placement, same operand order) so
   the two passes literally cannot drift apart -- pass 2's bitwise
   reproduction of pass 1's rotated values, which the whole two-pass
   design depends on, now holds by construction rather than by two
   hand-kept-in-sync copies. Each pass still applies its own final
   S=64 stage afterward (store_absmax vs the fused quantize), since
   that step differs between them.

2. The determinism test class's docstring advertised coverage "across
   the resident/chunked boundary at K=2048/2304" but 2304 was never
   actually in test_two_calls_bitwise_equal's parametrize list. Added
   2304 there and to the shape/dtype matrix (2304/256=9 groups -> an
   8+1 chunk split, the most extreme partial-last-chunk case in the
   suite: only 1 of the chunked kernel's 8 "sub" lanes active in the
   second chunk iteration, vs 3072's 8+4 and 3584's 8+6).

Verified bitwise-identical against the shipped-kernel references:
re-ran all previously-verified shapes (50 + the K=3072/3584 set of
14 = 64/64, confirming the refactor didn't change any kernel's
output) plus 7 new K=2304 cases (bf16/fp16/fp32, M=9216/M=1, random +
all-zero-mixed) -- 71/71 total, zero mismatches. Full test file: 167
tests (up from 154), all passing, ruff clean. Upstream suite:
unchanged 23 pre-existing failures / 975 passed / 101 skipped.
Standalone perf spot-check at K=4096 (the refactored kernel):
377.45 us/call, matching the pre-refactor measurement (377.5 us/call)
within noise -- the __forceinline__ extraction didn't change codegen
performance.

Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
Comment-only change (proven below), no code-token differences.

int8_linear.cu: several comment blocks added across this branch ran
15-41 lines -- narrative design rationale, roofline numbers, and
historical "was previously X" framing that belongs in the PR
description and commit messages, not the code. Stock's own comment
density in this file (and in adaln.cu/apply_rope.cu/etc.) is 1-3 lines
per block, with an occasional ~17-22 line header for a genuinely novel
kernel (e.g. this file's own pre-existing ConvRot rotation comment at
line 201). Trimmed every block we added to that density: the chunked
kernel's header is down from 41 to 9 lines (comparable to stock's own
longest precedent, not longer), the launcher's three branch comments
from 18/18/8 lines to 2-4 lines each. Kept exactly the load-bearing
one-liners a maintainer would actually want at the call site: the
NaN-fold fmaxf semantics (now next to the fold itself, not buried in a
kernel-header essay), and the one-line reason the chunked kernel is
gated to K<=4096.

tests/test_convrot64_quantize.py: existing test files in this repo
(test_int8.py, test_rms_rope.py, test_adaln.py) use exactly one-line
docstrings throughout, no exceptions found. This file's docstrings had
grown to 6-15 lines each (shape-boundary enumerations, tolerance
derivations, K=2304/3072/3584 partial-chunk explanations). Cut every
docstring to one line, matching the rest of the suite; the detail
that's genuinely useful for a reader is now in the PR description.

Proof of comment/docstring-only change:
- .cu: a comment-stripping diff (// line comments and /* */ blocks
  removed via a string/char-literal-aware scanner, blank lines
  dropped) against the pre-trim version shows zero code-token
  differences. Sanity-checked against a known-different pair (this
  file vs the original v0.2.22 base) to confirm the stripper actually
  detects real changes, not a no-op.
- tests/*.py: an AST diff with every docstring's string value blanked
  out (docstrings are string literals, not comment tokens, so a plain
  '#'-comment strip does not catch them) shows the two ASTs are
  identical once docstring text is ignored. Also sanity-checked
  against a deliberately mutated copy to confirm it flags real changes.

Given both proofs show code-identity, the previously-verified bitwise
results (71/71 across all shapes including K=2304/3072/3584, full
pytest suite unchanged) still hold without re-running them. Did
rebuild and run tests/test_convrot64_quantize.py once (167 passed) as
a sanity check.

Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
Also adds a seeded-stochastic-rounding test class spanning the resident and
chunked kernels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>
Chunked kernel's dynamic smem is always under the default 48 KiB limit, so
cudaFuncSetAttribute in launch_chunked was dead. The __syncthreads after
storing scales[row] was also redundant: block_reduce_max_t's own final
barrier already orders the block_smem read before any thread proceeds.
Rewords comments that referenced "stock", benchmarked regressions, or PR
history into present-tense facts, and makes the <=2048 switch's K=2048 case
explicit rather than falling through a "default" that doubled as a comment
explaining why it wasn't reachable for other K.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUhW5zPPgvPHxPJYcb11ri
Signed-off-by: Parsa Adl-Tabatabai <61601745+TheLegendOfKitty@users.noreply.github.com>
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.

1 participant