Skip to content

Basic convrot int4 support.#65

Merged
comfyanonymous merged 9 commits into
mainfrom
dev/int4
Jul 9, 2026
Merged

Basic convrot int4 support.#65
comfyanonymous merged 9 commits into
mainfrom
dev/int4

Conversation

@comfyanonymous

Copy link
Copy Markdown
Member

Should work on 30 and 40 series. 50 series doesn't have int4 ops so it will use int8 ops. 20 series needs special code for int4 and should fallback to int8 at the moment but is untested.

Should work on 30 and 40 series. 50 series doesn't have int4 ops so it will
use int8 ops. 20 series needs special code for int4 and should fallback to
int8 at the moment but is untested.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds ConvRot W4A4 int4 quantization, dequantization, and linear support across eager PyTorch, CUDA, and tensor-layout backends. It also extends public exports, backend bindings, CUTLASS strided dequant routing, and validation coverage.

Changes

ConvRot W4A4 Feature

Layer / File(s) Summary
Eager ConvRot W4A4 core
comfy_kitchen/backends/eager/convrot_w4a4.py, comfy_kitchen/backends/eager/__init__.py
Adds Hadamard rotation helpers, INT4 quantization and packing, ConvRot W4A4 weight quantize/dequantize routines, eager linear execution, and backend exports plus constraints.
Tensor layout and dispatch
comfy_kitchen/tensor/convrot_w4a4.py, comfy_kitchen/tensor/__init__.py, comfy_kitchen/__init__.py
Adds TensorCoreConvRotW4A4Layout, registry-backed wrappers, aten.t/linear/mm/addmm handlers, and top-level package exports and registration.
CUDA kernel bindings and Python API
comfy_kitchen/backends/cuda/CMakeLists.txt, comfy_kitchen/backends/cuda/dlpack_bindings.cpp, comfy_kitchen/backends/cuda/__init__.py, comfy_kitchen/backends/cuda/ops/cutlass_gemm_int8.cu, comfy_kitchen/backends/cuda/ops/int8_linear.cu
Adds CUDA source wiring, INT4 kernel declarations, nanobind wrappers, Python-level INT4 and ConvRot W4A4 APIs, fallback routing, CUTLASS strided dequant updates, and 64-bit INT8 quantization indexing.
ConvRot W4A4 validation
tests/test_convrot_w4a4.py
Adds eager and CUDA tests for quantization, dequantization, linear correctness, layout equivalence, stochastic rounding, shape validation, large-K reconstruction, overflow-scale clamping, and seeded determinism.
🚥 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
  • Commit unit tests in branch dev/int4
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch dev/int4

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 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/eager/convrot_w4a4.py`:
- Around line 156-171: The int4 accumulation in int4_linear currently happens in
the low-precision out_dtype because x_qdata and weight are cast before the
matmul, which can lose precision for large K and diverge from the CUDA path.
Update int4_linear so the unpacked operands are accumulated in fp32 during x_int
@ w_int.t(), then apply x_scale, weight_scale, and bias, and only cast the final
tensor to out_dtype at the end; use int4_linear and convrot_w4a4_linear as the
main points to locate the change.
- Around line 24-25: The power-of-4 validation in the regular Hadamard size
check relies on math.log in the convrot_w4a4 module, which can fail for large
valid powers due to float precision. Update the size validation logic in the
Regular Hadamard check to use an integer-only test instead of math.log(size, 4)
% 1, ideally with a bit-length/parity-based power-of-4 check, while keeping the
existing ValueError path and message in the same validation block.
🪄 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: 1834f6a8-172e-46f7-b57c-dc05638ce144

📥 Commits

Reviewing files that changed from the base of the PR and between 14e3a43 and d2a742f.

📒 Files selected for processing (10)
  • comfy_kitchen/__init__.py
  • comfy_kitchen/backends/cuda/CMakeLists.txt
  • comfy_kitchen/backends/cuda/__init__.py
  • comfy_kitchen/backends/cuda/dlpack_bindings.cpp
  • comfy_kitchen/backends/cuda/ops/convrot_w4a4.cu
  • comfy_kitchen/backends/cuda/ops/cutlass_gemm_int8.cu
  • comfy_kitchen/backends/eager/__init__.py
  • comfy_kitchen/backends/eager/convrot_w4a4.py
  • comfy_kitchen/tensor/__init__.py
  • comfy_kitchen/tensor/convrot_w4a4.py

Comment on lines +24 to +25
if size < 4 or (size & (size - 1)) != 0 or math.log(size, 4) % 1 != 0:
raise ValueError(f"Regular Hadamard size must be a power of 4, got {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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Demonstrate the float fragility across powers of 4 and confirm bit_length parity is exact.
python - <<'PY'
import math
for e in range(2, 40, 2):        # even exponents -> powers of 4
    size = 1 << e
    print(size,
          "log%1=", math.log(size, 4) % 1,
          "bitlen_ok=", (size.bit_length() - 1) % 2 == 0)
PY

Repository: Comfy-Org/comfy-kitchen

Length of output: 818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding implementation and any size constraints.
git ls-files comfy_kitchen/backends/eager/convrot_w4a4.py
wc -l comfy_kitchen/backends/eager/convrot_w4a4.py
sed -n '1,120p' comfy_kitchen/backends/eager/convrot_w4a4.py

# Search for callers/constraints on the Hadamard size.
rg -n "Regular Hadamard size|convrot_w4a4|Hadamard|power of 4|size < 4" comfy_kitchen -S

Repository: Comfy-Org/comfy-kitchen

Length of output: 11072


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the other implementation that uses the same guard and the nearby constraint docs.
wc -l comfy_kitchen/tensor/int8_utils.py comfy_kitchen/backends/eager/__init__.py comfy_kitchen/backends/cuda/__init__.py
sed -n '1,80p' comfy_kitchen/tensor/int8_utils.py
sed -n '210,260p' comfy_kitchen/backends/eager/__init__.py
sed -n '2230,2275p' comfy_kitchen/backends/cuda/__init__.py

# Find where the group size is documented as a constraint on the public APIs.
rg -n "convrot_groupsize|group size|power of 4|Regular orthogonal Hadamard" comfy_kitchen/tensor comfy_kitchen/backends -S

Repository: Comfy-Org/comfy-kitchen

Length of output: 19922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Compare the current float-based guard against an exact integer criterion
# over a wide range of powers of 4 and nearby non-powers.
python3 - <<'PY'
import math

def float_guard(size: int) -> bool:
    return size < 4 or (size & (size - 1)) != 0 or math.log(size, 4) % 1 != 0

def exact_guard(size: int) -> bool:
    return size < 4 or (size & (size - 1)) != 0 or ((size.bit_length() - 1) % 2 != 0)

mismatches = []
for e in range(2, 4000):  # plenty of powers of 4 and very large ints
    size = 1 << e
    if float_guard(size) != exact_guard(size):
        mismatches.append(("pow2", e, size, math.log(size, 4) % 1, float_guard(size), exact_guard(size)))
        break
    if e % 2 == 0 and e > 2:
        # test immediate neighbors too
        for delta in (-1, 1):
            n = size + delta
            if float_guard(n) != exact_guard(n):
                mismatches.append(("neighbor", e, n, math.log(n, 4) % 1 if n > 0 else None, float_guard(n), exact_guard(n)))
                break
        if mismatches:
            break

print("mismatches:", mismatches[:3] if mismatches else "none")
PY

Repository: Comfy-Org/comfy-kitchen

Length of output: 244


Use an integer-only power-of-4 check here. math.log(size, 4) % 1 can reject valid large powers of 4 once float precision slips; for example, 2^58 = 4^29 trips the test. A bit-length parity check is exact and keeps the Hadamard power on the rails, no log-jam needed.

🤖 Prompt for 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.

In `@comfy_kitchen/backends/eager/convrot_w4a4.py` around lines 24 - 25, The
power-of-4 validation in the regular Hadamard size check relies on math.log in
the convrot_w4a4 module, which can fail for large valid powers due to float
precision. Update the size validation logic in the Regular Hadamard check to use
an integer-only test instead of math.log(size, 4) % 1, ideally with a
bit-length/parity-based power-of-4 check, while keeping the existing ValueError
path and message in the same validation block.

Comment on lines +156 to +171
def int4_linear(
x_qdata: torch.Tensor,
weight: torch.Tensor,
x_scale: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
out_dtype: torch.dtype,
) -> torch.Tensor:
x_int = _unpack_int4_row_major(x_qdata).to(dtype=out_dtype)
w_int = _unpack_int4_row_major(weight).to(dtype=out_dtype)
out = x_int @ w_int.t()
out = out * x_scale.reshape(-1, 1).to(device=out.device, dtype=out_dtype)
out = out * weight_scale.reshape(1, -1).to(device=out.device, dtype=out_dtype)
if bias is not None:
out = out + bias.to(device=out.device, dtype=out_dtype).reshape(1, -1)
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

INT4 GEMM accumulates in the low‑precision output dtype — expect precision to depart in a hurry.

out_dtype is passed as x.dtype from convrot_w4a4_linear (Line 195), which for real models is typically bfloat16/float16. Casting the unpacked int4 operands to that dtype before x_int @ w_int.t() means the accumulation runs in bf16/fp16. bf16 represents integers exactly only up to 256, so for any K >= 256 (and convrot_groupsize defaults to 256) the summed products silently lose bits, producing wrong reference results that won't match the int32-accumulating CUDA int4 MMA.

Accumulate in fp32 and cast only the final result:

🐛 Proposed fix: fp32 accumulation, cast at the end
-    x_int = _unpack_int4_row_major(x_qdata).to(dtype=out_dtype)
-    w_int = _unpack_int4_row_major(weight).to(dtype=out_dtype)
-    out = x_int @ w_int.t()
-    out = out * x_scale.reshape(-1, 1).to(device=out.device, dtype=out_dtype)
-    out = out * weight_scale.reshape(1, -1).to(device=out.device, dtype=out_dtype)
-    if bias is not None:
-        out = out + bias.to(device=out.device, dtype=out_dtype).reshape(1, -1)
-    return out
+    x_int = _unpack_int4_row_major(x_qdata).to(torch.float32)
+    w_int = _unpack_int4_row_major(weight).to(torch.float32)
+    out = x_int @ w_int.t()
+    out = out * x_scale.reshape(-1, 1).to(device=out.device, dtype=torch.float32)
+    out = out * weight_scale.reshape(1, -1).to(device=out.device, dtype=torch.float32)
+    if bias is not None:
+        out = out + bias.to(device=out.device, dtype=torch.float32).reshape(1, -1)
+    return out.to(out_dtype)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def int4_linear(
x_qdata: torch.Tensor,
weight: torch.Tensor,
x_scale: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
out_dtype: torch.dtype,
) -> torch.Tensor:
x_int = _unpack_int4_row_major(x_qdata).to(dtype=out_dtype)
w_int = _unpack_int4_row_major(weight).to(dtype=out_dtype)
out = x_int @ w_int.t()
out = out * x_scale.reshape(-1, 1).to(device=out.device, dtype=out_dtype)
out = out * weight_scale.reshape(1, -1).to(device=out.device, dtype=out_dtype)
if bias is not None:
out = out + bias.to(device=out.device, dtype=out_dtype).reshape(1, -1)
return out
def int4_linear(
x_qdata: torch.Tensor,
weight: torch.Tensor,
x_scale: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
out_dtype: torch.dtype,
) -> torch.Tensor:
x_int = _unpack_int4_row_major(x_qdata).to(torch.float32)
w_int = _unpack_int4_row_major(weight).to(torch.float32)
out = x_int @ w_int.t()
out = out * x_scale.reshape(-1, 1).to(device=out.device, dtype=torch.float32)
out = out * weight_scale.reshape(1, -1).to(device=out.device, dtype=torch.float32)
if bias is not None:
out = out + bias.to(device=out.device, dtype=torch.float32).reshape(1, -1)
return out.to(out_dtype)
🤖 Prompt for 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.

In `@comfy_kitchen/backends/eager/convrot_w4a4.py` around lines 156 - 171, The
int4 accumulation in int4_linear currently happens in the low-precision
out_dtype because x_qdata and weight are cast before the matmul, which can lose
precision for large K and diverge from the CUDA path. Update int4_linear so the
unpacked operands are accumulated in fp32 during x_int @ w_int.t(), then apply
x_scale, weight_scale, and bias, and only cast the final tensor to out_dtype at
the end; use int4_linear and convrot_w4a4_linear as the main points to locate
the change.

@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: 1

🤖 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/__init__.py`:
- Around line 640-645: The helper _int4_int8_weight_chunk_cols has a redundant m
<= 128 branch because both paths return the same chunk width. Simplify the logic
by collapsing that branch in _int4_int8_weight_chunk_cols so it only keeps the n
<= 2560 special case and then falls back to the shared min(n,
_INT4_INT8_WEIGHT_CHUNK_N) result, unless you intend to introduce a distinct
m-dependent width later.
🪄 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: 51443c89-87a1-4c2d-aa77-60d2826d7501

📥 Commits

Reviewing files that changed from the base of the PR and between 4e2518f and ac3d717.

📒 Files selected for processing (8)
  • comfy_kitchen/backends/cuda/__init__.py
  • comfy_kitchen/backends/cuda/dlpack_bindings.cpp
  • comfy_kitchen/backends/cuda/ops/convrot_w4a4.cu
  • comfy_kitchen/backends/cuda/ops/cutlass_gemm_int8.cu
  • comfy_kitchen/backends/eager/__init__.py
  • comfy_kitchen/backends/eager/convrot_w4a4.py
  • comfy_kitchen/tensor/convrot_w4a4.py
  • tests/test_convrot_w4a4.py

Comment on lines +640 to +645
def _int4_int8_weight_chunk_cols(m: int, n: int) -> int:
if n <= 2560:
return n
if m <= 128:
return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
return min(n, _INT4_INT8_WEIGHT_CHUNK_N)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no other callers rely on m-based branching and check for an intended large-M chunk constant.
rg -nP '_int4_int8_weight_chunk_cols|_INT4_INT8_WEIGHT_CHUNK' comfy_kitchen -C2

Repository: Comfy-Org/comfy-kitchen

Length of output: 1766


🏁 Script executed:

#!/bin/bash
sed -n '630,705p' comfy_kitchen/backends/cuda/__init__.py
printf '\n----\n'
sed -n '780,1010p' comfy_kitchen/backends/cuda/__init__.py

Repository: Comfy-Org/comfy-kitchen

Length of output: 13655


Collapse the redundant m <= 128 branch

_int4_int8_weight_chunk_cols returns min(n, _INT4_INT8_WEIGHT_CHUNK_N) in both m paths, and there’s no separate large-M chunk width defined. Collapse the branch, or wire in a real m-dependent width if that was the plan — this split is all sizzle, no steak.

♻️ Collapse the redundant branch
 def _int4_int8_weight_chunk_cols(m: int, n: int) -> int:
     if n <= 2560:
         return n
-    if m <= 128:
-        return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
     return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _int4_int8_weight_chunk_cols(m: int, n: int) -> int:
if n <= 2560:
return n
if m <= 128:
return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
def _int4_int8_weight_chunk_cols(m: int, n: int) -> int:
if n <= 2560:
return n
return min(n, _INT4_INT8_WEIGHT_CHUNK_N)
🤖 Prompt for 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.

In `@comfy_kitchen/backends/cuda/__init__.py` around lines 640 - 645, The helper
_int4_int8_weight_chunk_cols has a redundant m <= 128 branch because both paths
return the same chunk width. Simplify the logic by collapsing that branch in
_int4_int8_weight_chunk_cols so it only keeps the n <= 2560 special case and
then falls back to the shared min(n, _INT4_INT8_WEIGHT_CHUNK_N) result, unless
you intend to introduce a distinct m-dependent width later.

@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: 1

🤖 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 `@tests/test_convrot_w4a4.py`:
- Around line 184-185: Add test coverage in the convrot W4A4 tests for automatic
INT4→INT8 fallback routing, since the current parametrization only verifies
explicit INT8 execution via linear_dtype in test_convrot_w4a4. Introduce a case
that requests INT4 while mocking native INT4 support as unavailable, then assert
the result matches the INT8 fallback path; make sure this applies to the
relevant parametrized sections around convrot_groupsize and the other referenced
test blocks so the fallback behavior is exercised for the targeted GPU routing.
🪄 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: a19e42f2-576f-4843-be03-ed1d6c84c397

📥 Commits

Reviewing files that changed from the base of the PR and between 8cd18b9 and 314f4bc.

📒 Files selected for processing (2)
  • comfy_kitchen/backends/cuda/__init__.py
  • tests/test_convrot_w4a4.py

Comment on lines +184 to +185
@pytest.mark.parametrize("linear_dtype", ["int4", "int8"])
@pytest.mark.parametrize("convrot_groupsize", [16, 64, 256])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for automatic INT4→INT8 fallback routing.

Selecting linear_dtype="int8" only tests explicit INT8 execution. Add a test that requests INT4 while mocking native-INT4 support as unavailable, then verifies the INT8 fallback result. Otherwise, the architecture fallback is four-bit folklore rather than tested fact.

As per PR objectives, 20- and 50-series GPUs must route INT4 requests through INT8 fallback.

Also applies to: 215-216, 245-246

🤖 Prompt for 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.

In `@tests/test_convrot_w4a4.py` around lines 184 - 185, Add test coverage in the
convrot W4A4 tests for automatic INT4→INT8 fallback routing, since the current
parametrization only verifies explicit INT8 execution via linear_dtype in
test_convrot_w4a4. Introduce a case that requests INT4 while mocking native INT4
support as unavailable, then assert the result matches the INT8 fallback path;
make sure this applies to the relevant parametrized sections around
convrot_groupsize and the other referenced test blocks so the fallback behavior
is exercised for the targeted GPU routing.

@comfyanonymous comfyanonymous merged commit c765355 into main Jul 9, 2026
14 checks passed
@comfyanonymous comfyanonymous deleted the dev/int4 branch July 9, 2026 21:28
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant