Skip to content

Commit 40312b5

Browse files
[converter] Fix negative axis in quantize/dequantize lowering (apple#24)
* [converter] Fix negative axis in quantize/dequantize lowering The quantize/dequantize lowering normalized a negative axis as axis + rank - 1, off by one from the eager op, which resolves it as axis + rank. A per-channel axis=-1 was applied one dimension early, so the converted model used the wrong channel; when the channel and a neighbor dim share a size this is silent, with no shape error. Normalize a negative axis as axis + rank, matching the eager op. Add per-channel negative-axis numerical tests for both ops. They use a (2, 4, 4) shape with equal middle and channel dims so a wrong axis surfaces as a numerical mismatch rather than a reshape error. * Parametrize negative-axis tests over equal and distinct dims --------- Co-authored-by: Henry Tao <55294647+jakesabathia2@users.noreply.github.com>
1 parent a43cc84 commit 40312b5

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

coreai_torch/_custom_to_core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,11 @@ def _replace_quantize_or_dequantize(
295295
quant_elem_type = input_type.element_type # dequantize: quant→float
296296
float_elem_type = result_elem_type
297297

298-
# Extract axis; normalize negative axis the same way the C++ lowering does.
298+
# Extract axis; normalize a negative axis the same way the eager op does.
299299
axis_val = _get_optional_int_arg(node, axis_idx, default=0)
300300
input_rank = len(input_type.shape)
301301
if axis_val < 0:
302-
axis_val = axis_val + input_rank - 1
302+
axis_val = axis_val + input_rank
303303

304304
axis = coreai.constant(np.array(axis_val, dtype=np.int32), loc=loc)
305305

tests/ops/test_custom_ops.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,30 @@ def forward(self, x: Tensor) -> Tensor:
411411
prepare_program=inject_subbyte_tensors,
412412
)
413413

414+
# (2, 4, 4): equal dims keep a wrong axis silent (a value mismatch).
415+
# (2, 3, 4): distinct dims show -1 is the last dim (a wrong axis is a reshape error).
416+
@pytest.mark.parametrize("x", [torch.randn(2, 4, 4), torch.randn(2, 3, 4)])
417+
async def test_per_channel_negative_axis_numerical(self, x: Tensor) -> None:
418+
"""quantize with a per-channel scale on a negative axis matches eager."""
419+
420+
class Model(nn.Module):
421+
def __init__(self) -> None:
422+
super().__init__()
423+
self.register_buffer(
424+
"scale", torch.tensor([0.1, 0.2, 0.3, 0.4], dtype=torch.float32)
425+
)
426+
self.register_buffer("zero_point", torch.zeros(4, dtype=torch.int8))
427+
428+
def forward(self, x: Tensor) -> Tensor:
429+
return torch.ops.coreai.quantize(
430+
x, self.scale, torch.int8, zero_point=self.zero_point, axis=-1
431+
)
432+
433+
model = Model()
434+
await validate_numerical_output(
435+
model=model, x=x, prepare_program=inject_subbyte_tensors
436+
)
437+
414438

415439
# ---------------------------------------------------------------------------
416440
# dequantize → coreai.dequantize
@@ -540,6 +564,36 @@ def forward(self, x: Tensor) -> Tensor:
540564
prepare_program=inject_subbyte_tensors,
541565
)
542566

567+
# (2, 4, 4): equal dims keep a wrong axis silent (a value mismatch).
568+
# (2, 3, 4): distinct dims show -1 is the last dim (a wrong axis is a reshape error).
569+
@pytest.mark.parametrize(
570+
"x",
571+
[
572+
torch.randint(-128, 127, (2, 4, 4), dtype=torch.int8),
573+
torch.randint(-128, 127, (2, 3, 4), dtype=torch.int8),
574+
],
575+
)
576+
async def test_per_channel_negative_axis_numerical(self, x: Tensor) -> None:
577+
"""dequantize with a per-channel scale on a negative axis matches eager."""
578+
579+
class Model(nn.Module):
580+
def __init__(self) -> None:
581+
super().__init__()
582+
self.register_buffer(
583+
"scale", torch.tensor([0.1, 0.2, 0.3, 0.4], dtype=torch.float32)
584+
)
585+
self.register_buffer("zero_point", torch.zeros(4, dtype=torch.int8))
586+
587+
def forward(self, x: Tensor) -> Tensor:
588+
return torch.ops.coreai.dequantize(
589+
x, self.scale, zero_point=self.zero_point, axis=-1
590+
)
591+
592+
model = Model()
593+
await validate_numerical_output(
594+
model=model, x=x, prepare_program=inject_subbyte_tensors
595+
)
596+
543597

544598
# ---------------------------------------------------------------------------
545599
# sparse_to_dense → coreai.build_sparse_with_bitmask + coreai.sparse_with_bitmask_to_dense

0 commit comments

Comments
 (0)