Skip to content

Commit dc0c41c

Browse files
authored
Fix torch.randint generating values equal to the upper bound (#2568) (#2714)
* Fix torch.randint producing values equal to the upper bound (#2568) torch.randint is documented as returning integers in [low, high), but the lowering used mb.random_uniform whose upper bound is inclusive — and cast-to-int truncates toward zero, so a sample exactly at high stays at high after casting. The reporter saw torch.randint(0, 100, ...) produce values up to 100. Clamp the cast result to high - 1 with mb.minimum so the converted graph matches torch's exclusive upper bound. Adds a regression test that asserts the lowered MIL graph contains the clamp op. * Check randint upper bound via prediction instead of MIL op-type walk
1 parent 82c89a8 commit dc0c41c

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

coremltools/converters/mil/frontend/torch/ops.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5654,7 +5654,14 @@ def _parse_positional_args(context, node) -> Tuple[Var]:
56545654

56555655
low, high, shape = _parse_positional_args(context, node)
56565656
rand_uniform = mb.random_uniform(shape=shape, low=low, high=high)
5657-
rand_int = mb.cast(x=rand_uniform, dtype="int32", name=node.name)
5657+
# torch.randint yields integers in [low, high) — the upper bound is exclusive.
5658+
# mb.random_uniform's upper bound is inclusive, so cast-to-int can yield
5659+
# exactly `high`. Clamp the result to `high - 1` to match torch's semantics
5660+
# (issue #2568).
5661+
rand_int_unclipped = mb.cast(x=rand_uniform, dtype="int32")
5662+
high_int = mb.cast(x=high, dtype="int32")
5663+
high_minus_one = mb.sub(x=high_int, y=1)
5664+
rand_int = mb.minimum(x=rand_int_unclipped, y=high_minus_one, name=node.name)
56585665
context.add(rand_int)
56595666

56605667
@register_torch_op

coremltools/converters/mil/frontend/torch/test/test_torch_ops.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4748,6 +4748,41 @@ def forward(self, x):
47484748
inputs = [ct.TensorType(shape=x.shape)] if frontend == TorchFrontend.TORCHSCRIPT else None
47494749
ct.convert(torch_model, inputs=inputs)
47504750

4751+
@pytest.mark.parametrize("frontend", frontends)
4752+
def test_upper_bound_is_exclusive(self, frontend):
4753+
# Regression test for issue #2568. torch.randint produces integers in
4754+
# [low, high) — the upper bound is exclusive. random_uniform's upper
4755+
# bound is inclusive, so before the clamp cast-to-int could emit exactly
4756+
# `high`. The converted model must never produce a value >= `high`.
4757+
if frontend == TorchFrontend.EXECUTORCH:
4758+
pytest.skip("torch._ops.aten.randint.low is not Aten Canonical")
4759+
4760+
class TestModel(nn.Module):
4761+
def forward(self, x):
4762+
return torch.randint(0, 100, (1000, 100), dtype=torch.int64) + x
4763+
4764+
model = TestModel().eval()
4765+
x = torch.zeros((1000, 100), dtype=torch.int64)
4766+
torch_model = export_torch_model_to_frontend(model, (x,), frontend)
4767+
# TorchScript needs explicit input types; exported programs carry them.
4768+
inputs = (
4769+
[ct.TensorType(shape=x.shape, dtype=np.int32)]
4770+
if frontend == TorchFrontend.TORCHSCRIPT
4771+
else None
4772+
)
4773+
mlmodel = ct.convert(torch_model, inputs=inputs)
4774+
4775+
# Over a 100k-element draw the prediction must stay within [0, high).
4776+
# Before the clamp, the cast could emit exactly `high` (== 100).
4777+
input_name = mlmodel.get_spec().description.input[0].name
4778+
prediction = list(
4779+
mlmodel.predict({input_name: x.numpy().astype(np.int32)}).values()
4780+
)[0]
4781+
assert (
4782+
prediction.max() < 100
4783+
), f"randint emitted {prediction.max()}, must be < high (100)"
4784+
assert prediction.min() >= 0
4785+
47514786

47524787
class TestRand(TorchBaseTest):
47534788
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)