Skip to content

Commit 71a80d7

Browse files
authored
Add sym_int prim op for symbolic integer casting (pytorch#20679)
torch.export with dynamic shapes (Dim.AUTO) can emit sym_int nodes when symbolic float expressions need integer conversion. Without a registered prim op and C++ kernel, these models fail at export or crash at runtime. Adds Python op registration, mapping entry, and C++ kernel handling Int (passthrough), Double (truncate toward zero), and Bool (0/1).
1 parent 7f74376 commit 71a80d7

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

exir/emit/test/test_emit.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,6 +2452,44 @@ def forward(self, x):
24522452
self.assertTrue(expected.shape == et_result.shape)
24532453
self.assertTrue(torch.allclose(expected, et_result))
24542454

2455+
def test_emit_sym_int(self) -> None:
2456+
class SymIntModel(nn.Module):
2457+
def forward(self, x):
2458+
n = x.shape[0]
2459+
f = torch.sym_float(n)
2460+
i = torch.sym_int(f)
2461+
return torch.zeros(i, dtype=x.dtype, device=x.device)
2462+
2463+
model = SymIntModel()
2464+
model.eval()
2465+
test_inputs = [
2466+
torch.randn(3, 4),
2467+
torch.randn(8, 4),
2468+
]
2469+
reference_outputs = []
2470+
with torch.no_grad():
2471+
for inp in test_inputs:
2472+
reference_outputs.append(model(inp))
2473+
2474+
batch_dim = Dim("batch", min=1, max=20)
2475+
dynamic_shapes = {"x": {0: batch_dim}}
2476+
exported_program = torch.export.export(
2477+
model, (test_inputs[0],), dynamic_shapes=dynamic_shapes
2478+
)
2479+
2480+
edge_program = to_edge(
2481+
exported_program,
2482+
compile_config=exir.EdgeCompileConfig(_check_ir_validity=False),
2483+
)
2484+
et_program = edge_program.to_executorch()
2485+
program_buffer = et_program.buffer
2486+
et_module = _load_for_executorch_from_buffer(program_buffer)
2487+
for inp, expected in zip(test_inputs, reference_outputs):
2488+
et_output = et_module.forward([inp])
2489+
et_result = et_output[0]
2490+
self.assertTrue(expected.shape == et_result.shape)
2491+
self.assertTrue(torch.allclose(expected, et_result))
2492+
24552493
def test_emit_channels_last_constant(self) -> None:
24562494
"""Test that channels-last constant tensors are emitted correctly.
24572495

exir/passes/executorch_prim_ops_registry.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def sym_float(a: _SymScalar) -> _SymScalar:
5858
return float(a) # pyre-ignore
5959

6060

61+
@bind_pattern_to_op(executorch_prims_lib, "sym_int.Scalar(Scalar a) -> Scalar")
62+
def sym_int(a: _SymScalar) -> _SymScalar:
63+
return int(a) # pyre-ignore
64+
65+
6166
# TODO: ideally we should return SymBool in the schema, but it seems
6267
# the schema parser does not recognize SymBool yet: P629748075
6368
@bind_pattern_to_op(executorch_prims_lib, "gt.Scalar(Scalar a, Scalar b) -> bool")
@@ -146,6 +151,7 @@ def sym_not(a: _SymScalar) -> bool:
146151
operator.mod: ops.backend.executorch_prim.mod.Scalar,
147152
operator.neg: ops.backend.executorch_prim.neg.Scalar,
148153
torch.sym_float: ops.backend.executorch_prim.sym_float.Scalar,
154+
torch.sym_int: ops.backend.executorch_prim.sym_int.Scalar,
149155
torch.sym_max: ops.backend.executorch_prim.sym_max.Scalar,
150156
torch.sym_min: ops.backend.executorch_prim.sym_min.Scalar,
151157
torch.sym_not: ops.backend.executorch_prim.sym_not.Scalar,

kernels/prim_ops/register_prim_ops.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,40 @@ static Kernel prim_ops[] = {
400400
}),
401401
#endif
402402

403+
#if !defined(EXECUTORCH_ENABLE_PRIM_OPS_SELECTIVE_BUILD) || \
404+
defined(INCLUDE_EXECUTORCH_PRIM_SYM_INT_SCALAR)
405+
// executorch_prim::sym_int.Scalar(Scalar) -> Scalar
406+
Kernel(
407+
"executorch_prim::sym_int.Scalar",
408+
[](KernelRuntimeContext& context, Span<EValue*> stack) {
409+
ET_KERNEL_CHECK_MSG(
410+
context,
411+
stack.size() == 2,
412+
InvalidProgram,
413+
/* void */,
414+
"Expected %zu args, got %zu",
415+
(size_t)2,
416+
stack.size());
417+
EValue& a = *stack[0];
418+
EValue& out = *stack[1];
419+
if (a.isInt()) {
420+
out = EValue(a.toInt());
421+
} else if (a.isDouble()) {
422+
out = EValue(static_cast<int64_t>(a.toDouble()));
423+
} else if (a.isBool()) {
424+
out = EValue(static_cast<int64_t>(a.toBool()));
425+
} else {
426+
ET_KERNEL_CHECK_MSG(
427+
context,
428+
false,
429+
InvalidType,
430+
/* void */,
431+
"sym_int only supports int, double, or bool inputs, got %zu",
432+
(size_t)a.tag);
433+
}
434+
}),
435+
#endif
436+
403437
#if !defined(EXECUTORCH_ENABLE_PRIM_OPS_SELECTIVE_BUILD) || \
404438
defined(INCLUDE_EXECUTORCH_PRIM_EQ_SCALAR)
405439
// executorch_prim::eq.Scalar(Scalar, Scalar) -> bool

kernels/prim_ops/test/prim_ops_test.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ TEST_F(RegisterPrimOpsTest, OpRegistered) {
4141
EXPECT_TRUE(hasOpsFn("executorch_prim::sym_max.Scalar"));
4242
EXPECT_TRUE(hasOpsFn("executorch_prim::sym_min.Scalar"));
4343
EXPECT_TRUE(hasOpsFn("executorch_prim::sym_not.Scalar"));
44+
EXPECT_TRUE(hasOpsFn("executorch_prim::sym_int.Scalar"));
4445
}
4546

4647
TEST_F(RegisterPrimOpsTest, SymSizeReturnsCorrectValue) {
@@ -204,6 +205,42 @@ TEST_F(RegisterPrimOpsTest, SymFloatHandlesBoolInput) {
204205
EXPECT_FLOAT_EQ(stack[1]->toDouble(), 0.0);
205206
}
206207

208+
TEST_F(RegisterPrimOpsTest, SymIntReturnsCorrectValue) {
209+
EValue values[2];
210+
EValue* stack[2];
211+
for (size_t i = 0; i < 2; i++) {
212+
stack[i] = &values[i];
213+
}
214+
215+
// Int passthrough
216+
values[0] = EValue((int64_t)7);
217+
values[1] = EValue((int64_t)0);
218+
getOpsFn("executorch_prim::sym_int.Scalar")(context_, Span<EValue*>(stack));
219+
EXPECT_EQ(stack[1]->toInt(), 7);
220+
221+
// Double truncates toward zero
222+
values[0] = EValue(3.7);
223+
values[1] = EValue((int64_t)0);
224+
getOpsFn("executorch_prim::sym_int.Scalar")(context_, Span<EValue*>(stack));
225+
EXPECT_EQ(stack[1]->toInt(), 3);
226+
227+
values[0] = EValue(-2.9);
228+
values[1] = EValue((int64_t)0);
229+
getOpsFn("executorch_prim::sym_int.Scalar")(context_, Span<EValue*>(stack));
230+
EXPECT_EQ(stack[1]->toInt(), -2);
231+
232+
// Bool converts to 0/1
233+
values[0] = EValue(true);
234+
values[1] = EValue((int64_t)0);
235+
getOpsFn("executorch_prim::sym_int.Scalar")(context_, Span<EValue*>(stack));
236+
EXPECT_EQ(stack[1]->toInt(), 1);
237+
238+
values[0] = EValue(false);
239+
values[1] = EValue((int64_t)0);
240+
getOpsFn("executorch_prim::sym_int.Scalar")(context_, Span<EValue*>(stack));
241+
EXPECT_EQ(stack[1]->toInt(), 0);
242+
}
243+
207244
TEST_F(RegisterPrimOpsTest, TestAlgebraOps) {
208245
EValue values[3];
209246
int64_t a = 3;

0 commit comments

Comments
 (0)