Skip to content

[Relax][ONNX] Guard integer Div with dynamic zero divisor#19897

Closed
Aharrypotter wants to merge 1 commit into
apache:mainfrom
Aharrypotter:onnx-integer-div-guard
Closed

[Relax][ONNX] Guard integer Div with dynamic zero divisor#19897
Aharrypotter wants to merge 1 commit into
apache:mainfrom
Aharrypotter:onnx-integer-div-guard

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes Relax ONNX frontend handling for integer Div when the divisor
is dynamic and may contain zero.

Issue #19541 reports a SIGFPE crash when an ONNX Div model has integer
inputs 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. After
legalization, LLVM could still see integer sdiv / udiv with 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_v7 now emits a
guarded Relax subgraph:

rhs_nonzero = relax.op.not_equal(rhs, relax.const(0, rhs_dtype))
safe_rhs = relax.op.where(rhs_nonzero, rhs, relax.const(1, rhs_dtype))
quotient = relax.op.divide(lhs, safe_rhs)
result = relax.op.where(rhs_nonzero, quotient, relax.const(0, quotient_dtype))

The divisor passed to relax.divide is therefore never zero. The final
where restores the deterministic zero result for the originally-zero divisor
lanes.

The importer normalizes the intermediate expressions whose inferred type is
needed by later where construction. It uses scalar constants for the zero and
one 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 Div still uses
the normal binary frontend implementation.

For integer Div with a constant RHS:

  • constants containing zero still raise ValueError at import time
  • constants without zero still use the normal binary frontend implementation

This 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 inf behavior.

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

Case Previous behavior New behavior
integer Div with dynamic integer RHS imports to bare relax.divide; LLVM can see integer division by zero at runtime imports to a guarded Relax subgraph; non-zero divisor lanes keep exact integer division, zero-divisor lanes return zero
integer Div with constant integer RHS containing zero import-time ValueError from PR #19566 unchanged
integer Div with constant integer RHS without zero normal binary lowering unchanged
non-integer Div normal binary lowering unchanged

Safety Checks

  • Constant integer divisors containing zero raise
    ValueError("ONNX Div with integer inputs encountered divisor value 0.").
  • Dynamic integer divisors are replaced with scalar 1 only for the
    division operand when the divisor lane is zero.
  • Zero-divisor lanes are overwritten with scalar 0 after the
    division.
  • The guarded graph is asserted with structural Relax IR tests, so the frontend
    cannot silently regress to a bare relax.divide or wire the guard operands
    incorrectly.

Out of Scope / Non-Goals

  • This PR does not define global Relax, TIR, or LLVM integer division-by-zero
    semantics.
  • This PR does not use a float-cast fallback for integer division.
  • This PR does not change ONNX Div floating-point behavior.
  • This PR does not change other frontends or other ONNX operators.

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 Coverage
test_div_integer_constant_zero_divisor_raises_valueerror keeps PR #19566 constant-zero import-time error
test_div_integer_dynamic_zero_divisor_runtime runs one dynamic INT32 ONNX Div model through DecomposeOpsForInference, LegalizeOps, LLVM compile, and VM execution; asserts dtype, shape, exact non-zero divisor results, and deterministic zero results for zero-divisor lanes
test_div_integer_dynamic_zero_divisor_ir_guard uses tvm.ir.assert_structural_equal to lock the exact guarded Relax subgraph before legalization
test_div_integer_dynamic_zero_divisor_broadcast_ir_guard uses tvm.ir.assert_structural_equal to lock the guarded Relax subgraph when RHS broadcasting is required

Local 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.py

Result:

test_div_integer_dynamic_zero_divisor_runtime: passed
test_frontend_onnx.py -k "div": 7 passed
ruff check: All checks passed

References

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +552 to +557
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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@Aharrypotter Aharrypotter force-pushed the onnx-integer-div-guard branch from 178ad02 to ea84457 Compare June 28, 2026 02:09
@tqchen

tqchen commented Jul 1, 2026

Copy link
Copy Markdown
Member

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

@Aharrypotter

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Relax ONNX integer Div by zero crashes process with SIGFPE

2 participants