Handle empty initializers gracefully in optimizer passes (#27976)
## Overview
Hardens several graph optimizer passes against zero-element (empty) and
other unexpected-shape constant initializers by validating initializer
element counts before dereferencing `data<T>()`. Prevents out-of-bounds
reads / crashes during optimization when a pass that assumes a scalar
input encounters a 0-element or multi-element tensor.
## Changes
### Source
| File | Description |
| ---- | ----------- |
| `onnxruntime/core/optimizer/div_mul_fusion.cc` | Require `size() == 1`
for the div constant. |
| `onnxruntime/core/optimizer/layer_norm_fusion.cc` | Require `epsilon`
initializer `size() == 1`; fall back to the default epsilon otherwise
(both `LayerNormFusion` and `SimplifiedLayerNormFusion`). |
| `onnxruntime/core/optimizer/dropout_elimination.cc` | Require
`ratio.size() == 1` before reading it as a scalar. |
| `onnxruntime/core/optimizer/double_qdq_pairs_remover.cc` | Require
`size() == 1` for zero-point / scale in `FindNewZeroPointAndScale`;
`ApplyNewInputValue` now uses `ORT_ENFORCE` to make the pre-validated
invariant loud. |
| `onnxruntime/core/optimizer/utils.cc` | `GetClipConstantMinMax` now
requires `size() == 1` for Clip min/max. |
| `onnxruntime/core/optimizer/noop_elimination.cc` | A zero-element
initializer is no longer treated as a proven identity value; the node is
preserved. |
Rationale: these operator inputs are defined by ONNX as scalar /
1-element tensors. Previously the passes only checked for `size() > 0`
(or not at all) and would silently read `data<T>()[0]` of a
multi-element initializer, changing semantics. Requiring exactly one
element makes the optimizer skip fusion / elimination for malformed or
unexpected shapes instead.
### Tests
- New regression tests covering zero-element initializers:
- `GraphTransformationTests.DivMulFusion_ZeroElementInitializer` /
`DivMulFusion_MultiElementInitializer`
- `GraphTransformationTests.NoopElimination_ZeroElementInitializer` /
`NoopElimination_ZeroElementInitializer_InternalNode`
- `GraphTransformationTests.LayerNormFusion_ZeroElementEpsilon` /
`SimplifiedLayerNormFusion_ZeroElementEpsilon`
- The LayerNorm / SimplifiedLayerNorm tests assert that fusion fired and
that the fused node carries `epsilon == DEFAULT_LAYERNORM_EPSILON
(1e-5f)`.
- The DivMulFusion test asserts that both `Div` and `Mul` are preserved
after the pass when fusion is correctly skipped.
- Tests use `ASSERT_STATUS_OK(Model::Load(...))` so load failures are
reported, not silently skipped.
- Zero-element initializers are intentionally not declared as graph
inputs, so they remain constant initializers and the guarded code paths
are actually exercised (ORT treats initializers appearing in graph
inputs as overrideable).
### CI infrastructure (test filter)
- `onnxruntime/test/testdata/onnx_backend_test_series_filters.jsonc`
adds `^test_reduce_max_empty_set_cuda` and
`^test_reduce_min_empty_set_cuda` to the CUDA exclusion list.
This is **unrelated to the optimizer hardening** but is required to
unblock the Windows GPU CUDA CI Pipeline on this PR. Every sibling
empty-set Reduce test
(`reduce_l1/l2/log_sum/log_sum_exp/prod/sum/sum_square_empty_set_cuda`)
is already excluded; ReduceMax/ReduceMin were missed. The CUDA
`PrepareForReduce` (`reduction_ops.cc:304`) rejects shapes with a
zero-sized reduction axis when `keepdims=false` instead of producing the
spec-defined identity (`-inf` / `+inf`); the proper fix is a larger
change in the CUDA reduction op and can be split into a follow-up.
## Validation
- Built `onnxruntime_test_all` (RelWithDebInfo, Windows).
- 11 PR-related tests pass (7 new optimizer tests + 4 existing
`DoubleQDQ*` regressions).
- No regressions: 278 `GraphTransformationTests` and 134
`QDQTransformerTests` all pass.
---------
Co-authored-by: Gopalakrishnan Nallasamy <gopalakrishnan.nallasamy@microsoft.com>