The fbgemm kernels require reduce_range to be True, to prevent potential overflow.

Yeah this makes sense after looking at qconv.cpp, which suggest that vpmaddsubsw instructions are used in fbgemm. Actually these saturate not overflow, but they saturate to 16 bit intermediates rather than 32 bit. So is reduce_range meant to prevent this?
I also modified the script a little bit and found that in torch fx, the fbgemm option doesn’t quite reduce the range by 1 bit, so overflow/saturation is still observed:
import torch
from torch import nn
from torchvision import transforms
import torchvision.models as tvm
from torchvision.datasets.folder import default_loader
from torch.nn.quantized.modules.utils import _quantize_weight

def _fuse_fx(graph_module, fuse_custom_config_dict):
    return graph_module

import torch.quantization.quantize_fx
torch.quantization.quantize_fx._fuse_fx = _fuse_fx

backend_str = 'fbgemm'

x_filename = 'ILSVRC2012_val_00000293.JPEG'
imsize = 224
normalize = transforms.Normalize(
    mean=[0.485, 0.456, 0.406],
    std=[0.229, 0.224, 0.225]
)
val_transforms = transforms.Compose([
    transforms.Resize(imsize + 24),
    transforms.CenterCrop(imsize),
    transforms.ToTensor(),
    normalize
])

x_raw = default_loader(x_filename)
x_encoding = val_transforms(x_raw).unsqueeze(0)
torch.random.manual_seed(0)
x_test = x_encoding*2.0

model = tvm.resnet50(pretrained=True)
weight = model.conv1.weight.detach().clone()
import torch.quantization.quantize_fx as quantize_fx
qconfig_dict = {"": torch.quantization.get_default_qconfig(backend_str)}
model.eval()
model = quantize_fx.prepare_fx(model, qconfig_dict)
model(x_encoding)
model = quantize_fx.convert_fx(model)

qmod = model.conv1

X_test = torch.quantize_per_tensor(x_test, model.conv1_input_scale_0, model.conv1_input_zero_point_0, model.conv1_input_dtype_0)

y_native = qmod(X_test).dequantize()
y_simulated = torch.quantize_per_tensor(
    torch.nn.functional.conv2d(
        X_test.dequantize(),
        qmod.weight().dequantize(),
        None,
        qmod.stride, qmod.padding, qmod.dilation, qmod.groups
    ),
    qmod.scale, qmod.zero_point, torch.quint8
).dequantize()

if backend_str == 'fbgemm':
    print(qmod.weight().q_per_channel_scales()/((weight.max(dim=1).values.max(dim=1).values.max(dim=1).values - weight.min(dim=1).values.min(dim=1).values.min(dim=1).values)/255))
else:
    print(qmod.weight().q_scale()/((weight.max()-weight.min())/255))

# Bad
print((y_native[0,33,18-3:18+3,23-3:23+3]-y_simulated[0,33,18-3:18+3,23-3:23+3]).abs())

# Good
print((y_native[0,32,18-3:18+3,23-3:23+3]-y_simulated[0,32,18-3:18+3,23-3:23+3]).abs())

# Op error
print('Max absolute difference', (y_native-y_simulated).abs().max())

print(len(torch.where((y_native-y_simulated).abs() > 0.0005)[0]))

My output shows that all channels have a scale that is less than 2x the expected scale but greater than 1x the expected scale, which seems to indicate that the range is reduced by less than one bit. There are also a handful of overflow/saturation instances. On the other hand, if I manually set an observer with reduce_range=True, the scale is exactly 2x the expected scale (reduced by exactly one bit) and no overflow/saturation issues. So my question is, is the default behavior not to reduce range in torch fx?