Comfy-Org / Comfy-Org/ComfyUI

Supporting Block-Wise FP8 Scaled quantization models - Kohya using this better quality than Tensor-Wise Scaling

Open
#10,491 7 comments 2 reactions 0 assignees View on GitHub
Potential Bug
Dominant language
Python
Stars
133k
Forks
15.7k
Avg merge
1d 6h
Merged PRs (30d)
155

Description

# Feature Request: Support Block-wise FP8 Scaled Quantization - Currently not working throwing error

## Summary

Add support for block-wise FP8 quantization in addition to the current per-tensor FP8 implementation. Block-wise quantization provides ~2-3% better quality compared to per-tensor quantization while maintaining the same memory footprint.

## Current Situation

ComfyUI currently supports FP8 scaled models with **per-tensor quantization** (scalar `scale_weight`). This works well, but block-wise quantization offers superior quality for minimal additional complexity.

### Current Implementation
- Uses scalar `scale_weight` tensors: `torch.Size([])`
- Calls `scale_weight.squeeze()` to get scalar value
- Uses `torch._scaled_mm()` for efficient FP8 matrix multiplication

### Proposed Enhancement
- Support multi-dimensional `scale_weight` tensors: `torch.Size([out_features, num_blocks, 1])`
- Automatically detect quantization mode based on scale shape
- Fall back to dequantization when block-wise scales are present

## Why This Matters

**Quality Improvement:**
- Block-wise quantization: ~2-3% better quality than per-tensor
- Better preservation of outliers and dynamic range
- Industry standard for high-quality FP8 conversion

**Compatibility:**
- Works with models from Musubi Tuner and other FP8 conversion tools
- Backward compatible (existing per-tensor models continue to work)
- No changes needed to model files or workflow

## Technical Details

### Modified Forward Pass

The change is in `fp8_optimization.py`, function `fp8_linear_forward()`. The patch adds a check for `scale_weight.ndim`:

```python
def fp8_linear_forward(cls, base_dtype, input):
weight_dtype = cls.weight.dtype
if weight_dtype in [torch.float8_e4m3fn, torch.float8_e5m2]:
scale_weight = getattr(cls, 'scale_weight', None)
if scale_weight is None:
scale_weight = torch.ones((), device=input.device, dtype=torch.float32)

# Check if block-wise quantization (3D scale: [out, num_blocks, 1])
if scale_weight.ndim == 3:
# Block-wise quantization path
import torch.nn.functional as F
original_dtype = scale_weight.dtype
out_features, num_blocks, _ = scale_weight.shape

# Dequantize weight: reshape and multiply by scale
dequantized_weight = cls.weight.to(original_dtype).contiguous().view(out_features, num_blocks, -1)
dequantized_weight = dequantized_weight * scale_weight
dequantized_weight = dequantized_weight.view(cls.weight.shape)

# Perform linear transformation
if cls.bias is not None:
output = F.linear(input, dequantized_weight, cls.bias)
else:
output = F.linear(input, dequantized_weight)

return output

else:
# Original per-tensor path (scalar scale)
if len(input.shape) == 3:
input_shape = input.shape
scale_weight = scale_weight.to(input.device).squeeze()
scale_input = torch.ones((), device=input.device, dtype=torch.float32)

input = torch.clamp(input, min=-448, max=448, out=input)
inn = input.reshape(-1, input_shape[2]).to(torch.float8_e4m3fn).contiguous()

bias = cls.bias.to(base_dtype) if cls.bias is not None else None
o = torch._scaled_mm(inn, cls.weight.t(), out_dtype=base_dtype, bias=bias, scale_a=scale_input, scale_b=scale_weight)
return o.reshape((-1, input_shape[1], cls.weight.shape[0]))
else:
return cls.original_forward(input.to(base_dtype))
else:
return cls.original_forward(input)
```

### Key Changes
1. **Line 9-26**: New block-wise quantization path
2. **Line 28-42**: Existing per-tensor path (unchanged)
3. **Automatic detection**: Uses `scale_weight.ndim` to select the right path

## Benefits

- ✅ **Backward compatible**: Existing per-tensor models continue to work
- ✅ **Better quality**: ~2-3% improvement with block-wise quantization
- ✅ **Minimal code change**: Only modifies one function
- ✅ **Industry standard**: Aligns with best practices from Musubi Tuner, ComfyUI forks
- ✅ **No performance penalty**: Block-wise uses the same memory, similar speed

## Use Cases

- Users converting models with FP8 optimization tools
- Users wanting higher quality FP8 models
- Compatibility with Musubi Tuner and other training frameworks
- Professional workflows requiring maximum quality

## Example Model Comparison

**Per-tensor (current):**
```
scale_weight: dtype=torch.bfloat16, shape=torch.Size([]) # scalar
```

**Block-wise (proposed):**
```
scale_weight: dtype=torch.bfloat16, shape=torch.Size([3072, 48, 1]) # per-block
```

Both use the same FP8 weights (`torch.float8_e4m3fn`), only the scale granularity differs.

## Testing

I can provide test models in both formats to verify the implementation if needed.

---

Thank you for considering this enhancement! Block-wise FP8 quantization would significantly improve quality for users working with FP8 models.

Contributor guide

Open the contributing guide

Research direction

Start by reading fp8_optimization.py and the fp8_linear_forward() entry point, then compare the existing scalar scale_weight path with the proposed multi-dimensional scale handling. Done means block-wise FP8 models from Musubi Tuner and existing per-tensor models both work without the reported error, while preserving the current behavior for scalar scales.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
machine-learning, performance
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.