[Relax][ONNX] Guard integer Div with dynamic zero divisor#19897
[Relax][ONNX] Guard integer Div with dynamic zero divisor#19897Aharrypotter wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the ONNX Div importer in TVM Relax to handle dynamic integer division by zero by replacing zero divisors with ones during division and masking the output to zero where the divisor was zero. It also adds corresponding unit tests to verify the runtime behavior and the generated IR. The reviewer suggested using scalar constants (relax.const) instead of zeros_like and ones_like to prevent unnecessary memory allocations and simplify the Relax IR.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| rhs_nonzero = bb.normalize(relax.op.not_equal(inputs[1], relax.op.zeros_like(inputs[1]))) | ||
| safe_rhs = bb.normalize( | ||
| relax.op.where(rhs_nonzero, inputs[1], relax.op.ones_like(inputs[1])) | ||
| ) | ||
| quotient = bb.normalize(relax.op.divide(inputs[0], safe_rhs)) | ||
| return relax.op.where(rhs_nonzero, quotient, relax.op.zeros_like(quotient)) |
There was a problem hiding this comment.
Efficiency & Cleanliness Improvement
Instead of allocating and filling intermediate tensors of the same shape as inputs[1] or quotient using zeros_like and ones_like, we can use scalar constants relax.const(0, dtype) and relax.const(1, dtype). Since Relax operators support broadcasting with scalar constants, this avoids unnecessary memory allocations and kernel launches for filling these intermediate tensors, which is especially beneficial for large or dynamic tensors.
This also simplifies the generated Relax IR by removing three intermediate variables (zeros_like and ones_like bindings).
Note that if you apply this suggestion, you will need to update the expected IR in test_div_integer_dynamic_zero_divisor_ir_guard and test_div_integer_dynamic_zero_divisor_broadcast_ir_guard to use R.const(0, "int32") and R.const(1, "int32") instead of R.zeros_like and R.ones_like.
| rhs_nonzero = bb.normalize(relax.op.not_equal(inputs[1], relax.op.zeros_like(inputs[1]))) | |
| safe_rhs = bb.normalize( | |
| relax.op.where(rhs_nonzero, inputs[1], relax.op.ones_like(inputs[1])) | |
| ) | |
| quotient = bb.normalize(relax.op.divide(inputs[0], safe_rhs)) | |
| return relax.op.where(rhs_nonzero, quotient, relax.op.zeros_like(quotient)) | |
| dtype = inputs[1].ty.dtype | |
| rhs_nonzero = bb.normalize(relax.op.not_equal(inputs[1], relax.const(0, dtype))) | |
| safe_rhs = bb.normalize( | |
| relax.op.where(rhs_nonzero, inputs[1], relax.const(1, dtype)) | |
| ) | |
| quotient = bb.normalize(relax.op.divide(inputs[0], safe_rhs)) | |
| return relax.op.where(rhs_nonzero, quotient, relax.const(0, dtype)) |
178ad02 to
ea84457
Compare
|
unfortunately the extra op will slow down compute, normally for zero divisor we should just leave it as it is and let hw handle error |
Thanks @tqchen, that makes sense. I'll close this PR. |
Summary
This PR fixes Relax ONNX frontend handling for integer
Divwhen the divisoris dynamic and may contain zero.
Issue #19541 reports a
SIGFPEcrash when an ONNXDivmodel has integerinputs and the divisor is supplied as a graph input containing zero. PR #19566
already added an import-time error for constant integer divisors containing
zero, but dynamic divisors still imported to a bare
relax.divide. Afterlegalization, LLVM could still see integer
sdiv/udivwith a zero divisor.ONNX integer division by zero is undefined. This PR keeps exact integer
division for every non-zero divisor lane, and returns zero for lanes where the
runtime divisor is zero. The result is deterministic and avoids exposing
integer division by zero to LLVM.
Design
Guarded Dynamic Integer Div
For integer operands with a non-constant RHS,
Div._impl_v7now emits aguarded Relax subgraph:
The divisor passed to
relax.divideis therefore never zero. The finalwhererestores the deterministic zero result for the originally-zero divisorlanes.
The importer normalizes the intermediate expressions whose inferred type is
needed by later
whereconstruction. It uses scalar constants for the zero andone guard values so the imported Relax graph does not allocate separate
filled tensors for the guard.
Constant and Non-Integer Paths
The existing non-integer behavior is unchanged: non-integer
Divstill usesthe normal binary frontend implementation.
For integer
Divwith a constant RHS:ValueErrorat import timeThis keeps the PR #19566 behavior for constants while only adding runtime
guarding where import-time zero detection is impossible.
No Float Fallback
The implementation intentionally avoids casting integer tensors through float
types. The guarded Relax graph preserves integer division semantics on all
non-zero lanes and avoids precision loss or float-to-int
infbehavior.Frontend-Local Runtime Guard
This is intentionally a frontend-local runtime guard rather than a global
Relax, TIR, or LLVM integer-division policy. The check is not an import-time
Python decision for dynamic inputs; it is expressed as Relax IR and executes
with the model at runtime.
Handling this in a generic Relax/TIR pass would affect all producers of integer
division and would need a project-wide policy for undefined integer
division-by-zero behavior: whether to trap, raise a controlled runtime error,
or return a deterministic value. This PR only addresses the ONNX frontend bug
reported in #19541 and keeps the deterministic zero result local to imported
ONNX integer
Div.Updated Converter Behavior
Divwith dynamic integer RHSrelax.divide; LLVM can see integer division by zero at runtimeDivwith constant integer RHS containing zeroValueErrorfrom PR #19566Divwith constant integer RHS without zeroDivSafety Checks
ValueError("ONNX Div with integer inputs encountered divisor value 0.").1only for thedivision operand when the divisor lane is zero.
0after thedivision.
cannot silently regress to a bare
relax.divideor wire the guard operandsincorrectly.
Out of Scope / Non-Goals
semantics.
Divfloating-point behavior.Tests
The tests primarily lock the imported Relax graph shape, with one minimal VM
runtime smoke test for the original execution-time crash path.
test_div_integer_constant_zero_divisor_raises_valueerrortest_div_integer_dynamic_zero_divisor_runtimeDivmodel throughDecomposeOpsForInference,LegalizeOps, LLVM compile, and VM execution; asserts dtype, shape, exact non-zero divisor results, and deterministic zero results for zero-divisor lanestest_div_integer_dynamic_zero_divisor_ir_guardtvm.ir.assert_structural_equalto lock the exact guarded Relax subgraph before legalizationtest_div_integer_dynamic_zero_divisor_broadcast_ir_guardtvm.ir.assert_structural_equalto lock the guarded Relax subgraph when RHS broadcasting is requiredLocal validation:
python -m pytest \ tests/python/relax/test_frontend_onnx.py::test_div_integer_dynamic_zero_divisor_runtime \ -xvs python -m pytest tests/python/relax/test_frontend_onnx.py -k "div" -xvs python -m ruff check \ python/tvm/relax/frontend/onnx/onnx_frontend.py \ tests/python/relax/test_frontend_onnx.pyResult:
References
Divwith a dynamic zero divisor can crash at runtime.Divdivide-by-zero crashes #19566: previous constant-zero divisor import-time error.