diff --git a/.github/workflows/musa-ci.yml b/.github/workflows/musa-ci.yml
new file mode 100644
index 0000000..cda46e5
--- /dev/null
+++ b/.github/workflows/musa-ci.yml
@@ -0,0 +1,233 @@
+name: MUSA CI
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ actions: read
+
+concurrency:
+ group: musa-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Unit Tests
+ runs-on: [self-hosted, musa, s5000]
+ timeout-minutes: 90
+ env:
+ CI_ASSETS: /data/ci-assets
+ PYTHONUNBUFFERED: "1"
+
+ steps:
+ - name: Checkout PR head
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 1
+ submodules: false
+
+ - name: Configure MUSA toolkit environment
+ run: |
+ set -euo pipefail
+ echo "/usr/local/musa/bin" >> "${GITHUB_PATH}"
+ {
+ echo "MUSA_HOME=/usr/local/musa"
+ echo "LD_LIBRARY_PATH=/usr/local/musa/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
+ echo "CPLUS_INCLUDE_PATH=/usr/include/c++/11:/usr/include/x86_64-linux-gnu/c++/11:/usr/include${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
+ } >> "${GITHUB_ENV}"
+
+ echo "MUSA_HOME=/usr/local/musa"
+ echo "PATH includes /usr/local/musa/bin in subsequent steps"
+ echo "LD_LIBRARY_PATH=/usr/local/musa/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
+ echo "CPLUS_INCLUDE_PATH=/usr/include/c++/11:/usr/include/x86_64-linux-gnu/c++/11:/usr/include${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
+
+ - name: Prepare ONNX Runtime headers
+ run: |
+ set -euo pipefail
+ test -d "${CI_ASSETS}/onnxruntime"
+ rm -rf third_party/onnxruntime
+ cp -a "${CI_ASSETS}/onnxruntime" third_party/onnxruntime
+
+ - name: Build plugin and wheel
+ run: |
+ set -euo pipefail
+ echo "Testing commit: $(git rev-parse HEAD)"
+ bash build.sh
+
+ - name: Install wheel
+ run: |
+ set -euo pipefail
+ python -m pip install --no-deps "onnxruntime==1.26.0"
+
+ shopt -s nullglob
+ wheels=(dist/onnxruntime_ep_musa-*.whl)
+ if (( ${#wheels[@]} != 1 )); then
+ echo "Expected exactly one MUSA wheel, found ${#wheels[@]}" >&2
+ printf ' %s\n' "${wheels[@]}" >&2
+ exit 1
+ fi
+ python -m pip install --no-deps --force-reinstall "${wheels[0]}"
+
+ - name: Run provider tests
+ run: bash test/run_all.sh
+
+ - name: Package build artifacts
+ run: |
+ set -euo pipefail
+ mkdir -p "${RUNNER_TEMP}/musa-build"
+ tar -C build -czf "${RUNNER_TEMP}/musa-build/musa-build.tar.gz" \
+ Release/libonnxruntime_providers_musa_plugin.so
+ tar -tzf "${RUNNER_TEMP}/musa-build/musa-build.tar.gz" >/dev/null
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: musa-build-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/musa-build/musa-build.tar.gz
+ if-no-files-found: error
+ retention-days: 7
+
+ model-zoo-cpu-ep-diff:
+ name: Model Zoo CPU EP Diff
+ needs: test
+ runs-on: [self-hosted, musa, s5000]
+ timeout-minutes: 720
+ env:
+ CI_ASSETS: /data/ci-assets
+ MODEL_ZOO_WORK: /data/ci-work/onnx-model-zoo-cpu-ep-diff-${{ github.run_id }}-${{ github.run_attempt }}
+ RESULTS_DIR: /data/ci-work/onnx-model-zoo-cpu-ep-diff-${{ github.run_id }}-${{ github.run_attempt }}/results
+ MUSA_VISIBLE_DEVICES: "0"
+ PYTHONUNBUFFERED: "1"
+
+ steps:
+ - name: Checkout PR head
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 1
+ submodules: false
+
+ - name: Configure MUSA toolkit environment
+ run: |
+ set -euo pipefail
+ echo "/usr/local/musa/bin" >> "${GITHUB_PATH}"
+ {
+ echo "MUSA_HOME=/usr/local/musa"
+ echo "LD_LIBRARY_PATH=/usr/local/musa/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
+ echo "CPLUS_INCLUDE_PATH=/usr/include/c++/11:/usr/include/x86_64-linux-gnu/c++/11:/usr/include${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
+ } >> "${GITHUB_ENV}"
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: musa-build-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/musa-build
+
+ - name: Extract build artifacts
+ run: |
+ set -euo pipefail
+ mkdir -p build
+ tar -C build -xzf "${RUNNER_TEMP}/musa-build/musa-build.tar.gz"
+
+ plugin="${GITHUB_WORKSPACE}/build/Release/libonnxruntime_providers_musa_plugin.so"
+ test -f "${plugin}"
+ echo "Head commit: $(git rev-parse HEAD)"
+ sha256sum "${plugin}"
+
+ - name: Prepare model zoo
+ run: |
+ set -euo pipefail
+ test -d "${CI_ASSETS}/onnx-model-zoo"
+ rm -rf "${MODEL_ZOO_WORK}"
+ mkdir -p "${MODEL_ZOO_WORK}"
+ cp -al "${CI_ASSETS}/onnx-model-zoo/." "${MODEL_ZOO_WORK}/"
+ rm -rf "${MODEL_ZOO_WORK}/cpp_runners"
+ cp -a "${CI_ASSETS}/onnx-model-zoo/cpp_runners" "${MODEL_ZOO_WORK}/cpp_runners"
+ mkdir -p "${RESULTS_DIR}"
+ test -f "${MODEL_ZOO_WORK}/run_all_onnx_cpu_ep_diff.sh"
+ test -f "${MODEL_ZOO_WORK}/cpp_runners/build_onnx_runner.sh"
+ test -f "/data/ci-assets/ort-sdk/onnxruntime-linux-x64-1.26.0/include/onnxruntime_c_api.h"
+
+ - name: Build model zoo CPU EP diff runner
+ run: |
+ set -euo pipefail
+ cd "${MODEL_ZOO_WORK}"
+ ORT_ROOT=/data/ci-assets/ort-sdk/onnxruntime-linux-x64-1.26.0/ \
+ bash cpp_runners/build_onnx_runner.sh
+ test -x cpp_runners/onnx_cpu_ep_diff
+
+ - name: Run MUSA-vs-CPU diff
+ run: |
+ set -euo pipefail
+ plugin="${GITHUB_WORKSPACE}/build/Release/libonnxruntime_providers_musa_plugin.so"
+ result_log="${RESULTS_DIR}/musa-cpu-ep-diff.log"
+ test -f "${plugin}"
+
+ cd "${MODEL_ZOO_WORK}"
+ set +e
+ MUSA_EP_LIBRARY="${plugin}" \
+ bash run_all_onnx_cpu_ep_diff.sh --ep musa -s 4 -q 16 2>&1 | tee "${result_log}"
+ pipeline_status=("${PIPESTATUS[@]}")
+ run_status=${pipeline_status[0]}
+ tee_status=${pipeline_status[1]}
+ set -e
+
+ if [[ -d "${MODEL_ZOO_WORK}/musa_cpu_diff_logs" ]]; then
+ rm -rf "${RESULTS_DIR}/musa_cpu_diff_logs"
+ cp -a "${MODEL_ZOO_WORK}/musa_cpu_diff_logs" "${RESULTS_DIR}/musa_cpu_diff_logs"
+ fi
+
+ summary="$(grep -E '^pass=[0-9]+ fail=[0-9]+ skip=[0-9]+ logs=' "${result_log}" | tail -n 1 || true)"
+ summary_pattern='^pass=([0-9]+) fail=([0-9]+) skip=([0-9]+) logs='
+ if [[ ! "${summary}" =~ ${summary_pattern} ]]; then
+ echo "Could not find the final CPU EP diff summary in ${result_log}" >&2
+ echo "Diff runner exit code: ${run_status}" >&2
+ exit 1
+ fi
+
+ pass_count="${BASH_REMATCH[1]}"
+ fail_count="${BASH_REMATCH[2]}"
+ skip_count="${BASH_REMATCH[3]}"
+
+ {
+ echo "## Model Zoo CPU EP Diff"
+ echo '```text'
+ echo "${summary}"
+ echo '```'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ echo "CPU EP diff result: pass=${pass_count} fail=${fail_count} skip=${skip_count}"
+ if (( tee_status != 0 )); then
+ echo "Failed to write the CPU EP diff log (tee exit ${tee_status})" >&2
+ exit 1
+ fi
+ if (( fail_count != 0 )); then
+ echo "CPU EP diff gate failed: fail=${fail_count}" >&2
+ exit 1
+ fi
+ if (( run_status != 0 )); then
+ echo "CPU EP diff runner exited with code ${run_status} despite fail=0" >&2
+ exit "${run_status}"
+ fi
+ echo "CPU EP diff gate passed: fail=0 (PASS and SKIPPED are both accepted)"
+
+ - name: Upload CPU EP diff results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: model-zoo-cpu-ep-diff-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ env.RESULTS_DIR }}/
+ if-no-files-found: ignore
+ retention-days: 7
+
+ - name: Cleanup model zoo work directory
+ if: always()
+ run: |
+ set -euo pipefail
+ case "${MODEL_ZOO_WORK}" in
+ /data/ci-work/onnx-model-zoo-cpu-ep-diff-*) rm -rf "${MODEL_ZOO_WORK}" ;;
+ *) echo "Refusing to remove unexpected path: ${MODEL_ZOO_WORK}" >&2; exit 1 ;;
+ esac
diff --git a/.github/workflows/musa-weekly-cpu-ep-diff.yml b/.github/workflows/musa-weekly-cpu-ep-diff.yml
new file mode 100644
index 0000000..a229422
--- /dev/null
+++ b/.github/workflows/musa-weekly-cpu-ep-diff.yml
@@ -0,0 +1,177 @@
+name: MUSA Weekly CPU EP Diff
+
+on:
+ schedule:
+ # Monday 06:00 Asia/Shanghai (UTC+8) = Sunday 22:00 UTC.
+ - cron: "0 22 * * 0"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ actions: read
+
+concurrency:
+ group: musa-weekly-cpu-ep-diff
+ cancel-in-progress: false
+
+jobs:
+ weekly-cpu-ep-diff:
+ name: Weekly Model Zoo CPU EP Diff
+ runs-on: [self-hosted, musa, s5000]
+ timeout-minutes: 720
+ env:
+ CI_ASSETS: /data/ci-assets
+ MODEL_ZOO_WORK: /data/ci-work/onnx-model-zoo-weekly-cpu-ep-diff-${{ github.run_id }}-${{ github.run_attempt }}
+ RESULTS_DIR: /data/ci-work/onnx-model-zoo-weekly-cpu-ep-diff-${{ github.run_id }}-${{ github.run_attempt }}/results
+ MUSA_VISIBLE_DEVICES: "0"
+ PYTHONUNBUFFERED: "1"
+
+ steps:
+ - name: Checkout master
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ fetch-depth: 1
+ submodules: false
+
+ - name: Configure MUSA toolkit environment
+ run: |
+ set -euo pipefail
+ echo "/usr/local/musa/bin" >> "${GITHUB_PATH}"
+ {
+ echo "MUSA_HOME=/usr/local/musa"
+ echo "LD_LIBRARY_PATH=/usr/local/musa/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
+ echo "CPLUS_INCLUDE_PATH=/usr/include/c++/11:/usr/include/x86_64-linux-gnu/c++/11:/usr/include${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
+ } >> "${GITHUB_ENV}"
+
+ - name: Prepare ONNX Runtime headers
+ run: |
+ set -euo pipefail
+ test -d "${CI_ASSETS}/onnxruntime"
+ rm -rf third_party/onnxruntime
+ cp -a "${CI_ASSETS}/onnxruntime" third_party/onnxruntime
+
+ - name: Build master plugin
+ run: |
+ set -euo pipefail
+ echo "Master commit: $(git rev-parse HEAD)"
+ bash build.sh --clean --no-wheel
+ test -f build/Release/libonnxruntime_providers_musa_plugin.so
+ sha256sum build/Release/libonnxruntime_providers_musa_plugin.so
+
+ - name: Prepare model zoo
+ run: |
+ set -euo pipefail
+ test -d "${CI_ASSETS}/onnx-model-zoo"
+ rm -rf "${MODEL_ZOO_WORK}"
+ mkdir -p "${MODEL_ZOO_WORK}"
+ cp -al "${CI_ASSETS}/onnx-model-zoo/." "${MODEL_ZOO_WORK}/"
+ rm -rf "${MODEL_ZOO_WORK}/cpp_runners"
+ cp -a "${CI_ASSETS}/onnx-model-zoo/cpp_runners" "${MODEL_ZOO_WORK}/cpp_runners"
+ mkdir -p "${RESULTS_DIR}"
+ test -f "${MODEL_ZOO_WORK}/run_all_onnx_cpu_ep_diff.sh"
+ test -f "${MODEL_ZOO_WORK}/cpp_runners/build_onnx_runner.sh"
+ test -f "/data/ci-assets/ort-sdk/onnxruntime-linux-x64-1.26.0/include/onnxruntime_c_api.h"
+
+ - name: Build model zoo CPU EP diff runner
+ run: |
+ set -euo pipefail
+ cd "${MODEL_ZOO_WORK}"
+ ORT_ROOT=/data/ci-assets/ort-sdk/onnxruntime-linux-x64-1.26.0/ \
+ bash cpp_runners/build_onnx_runner.sh
+ test -x cpp_runners/onnx_cpu_ep_diff
+
+ - name: Run weekly MUSA-vs-CPU diff
+ run: |
+ set -euo pipefail
+ plugin="${GITHUB_WORKSPACE}/build/Release/libonnxruntime_providers_musa_plugin.so"
+ raw_log="${RESULTS_DIR}/musa-cpu-ep-diff.log"
+ report="${RESULTS_DIR}/musa-weekly-cpu-ep-diff-report.md"
+ test -f "${plugin}"
+
+ commit="$(git rev-parse HEAD)"
+ plugin_sha="$(sha256sum "${plugin}" | awk '{print $1}')"
+
+ cd "${MODEL_ZOO_WORK}"
+ set +e
+ MUSA_EP_LIBRARY="${plugin}" \
+ bash run_all_onnx_cpu_ep_diff.sh --ep musa -s 4 -q 1024 2>&1 | tee "${raw_log}"
+ pipeline_status=("${PIPESTATUS[@]}")
+ run_status=${pipeline_status[0]}
+ tee_status=${pipeline_status[1]}
+ set -e
+
+ summary="$(grep -E '^pass=[0-9]+ fail=[0-9]+ skip=[0-9]+ logs=' "${raw_log}" | tail -n 1 || true)"
+ summary_pattern='^pass=([0-9]+) fail=([0-9]+) skip=([0-9]+) logs='
+ summary_found=0
+ fail_count=""
+ if [[ "${summary}" =~ ${summary_pattern} ]]; then
+ summary_found=1
+ fail_count="${BASH_REMATCH[2]}"
+ fi
+
+ gate_status="FAIL"
+ if [[ "${summary_found}" -eq 1 && "${tee_status}" -eq 0 && "${fail_count}" -eq 0 ]]; then
+ gate_status="PASS"
+ fi
+
+ {
+ echo "# Weekly Model Zoo CPU EP Diff"
+ echo
+ echo "| Field | Value |"
+ echo "| --- | --- |"
+ echo "| Result | **${gate_status}** |"
+ echo '| Branch | `master` |'
+ echo "| Commit | \`${commit}\` |"
+ echo '| Command | `run_all_onnx_cpu_ep_diff.sh --ep musa -s 4 -q 1024` |'
+ echo "| Plugin SHA256 | \`${plugin_sha}\` |"
+ echo "| Summary | \`${summary:-not found}\` |"
+ echo "| Runner exit | \`${run_status}\` |"
+ echo
+ echo "## Diff Output"
+ echo
+ echo '```text'
+ cat "${raw_log}"
+ echo '```'
+ } > "${report}"
+
+ {
+ echo "## Weekly Model Zoo CPU EP Diff"
+ echo
+ echo "- Result: **${gate_status}**"
+ echo "- Commit: \`${commit}\`"
+ echo "- Summary: \`${summary:-not found}\`"
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [[ "${summary_found}" -eq 0 ]]; then
+ echo "Could not find the final CPU EP diff summary in ${raw_log}" >&2
+ exit 1
+ fi
+ echo "Weekly CPU EP diff result: ${summary}"
+ if (( tee_status != 0 )); then
+ echo "Failed to write the weekly report log (tee exit ${tee_status})" >&2
+ exit 1
+ fi
+ if (( fail_count != 0 )); then
+ echo "Weekly CPU EP diff gate failed: fail=${fail_count}" >&2
+ exit 1
+ fi
+ echo "Weekly CPU EP diff passed: fail=0 (PASS and SKIPPED are accepted)"
+
+ - name: Upload weekly report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: musa-weekly-cpu-ep-diff-report-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ env.RESULTS_DIR }}/musa-weekly-cpu-ep-diff-report.md
+ if-no-files-found: warn
+ retention-days: 30
+
+ - name: Cleanup model zoo work directory
+ if: always()
+ run: |
+ set -euo pipefail
+ case "${MODEL_ZOO_WORK}" in
+ /data/ci-work/onnx-model-zoo-weekly-cpu-ep-diff-*) rm -rf "${MODEL_ZOO_WORK}" ;;
+ *) echo "Refusing to remove unexpected path: ${MODEL_ZOO_WORK}" >&2; exit 1 ;;
+ esac
diff --git a/musa/docs/fusion/silu.md b/musa/docs/fusion/silu.md
index 403aaa0..29801d3 100644
--- a/musa/docs/fusion/silu.md
+++ b/musa/docs/fusion/silu.md
@@ -13,7 +13,7 @@ This file is generated from the current C++ fusion source and matching fusion te
- Runtime compute: `SiluFusionCompute`
- Runtime implementation: `musa/ep/src/fusion/silu_fusion.cc`
- `drop_constant_initializers`: `false`
-- Before-graph topology source: `musa/ep/src/fusion/silu_fusion_matcher.cc` finder source
+- Before-graph topology source: `test/fusion/test_silu_fusion.py::test_silu_fusion_float16_accepts_cpu_produced_input`
## Extracted ONNX Ops
@@ -24,7 +24,12 @@ This file is generated from the current C++ fusion source and matching fusion te
```mermaid
flowchart LR
subgraph Before[Before fusion]
- B0["Mul / Sigmoid"]
+ B0["Sin"]
+ B1["Sigmoid"]
+ B2["Mul"]
+ B0 --> B1
+ B0 --> B2
+ B1 --> B2
end
subgraph After[After fusion]
A0["MUSA fused node"]
diff --git a/musa/docs/fusion/split_concat.md b/musa/docs/fusion/split_concat.md
index 3c05028..07e78e7 100644
--- a/musa/docs/fusion/split_concat.md
+++ b/musa/docs/fusion/split_concat.md
@@ -13,7 +13,7 @@ This file is generated from the current C++ fusion source and matching fusion te
- Runtime compute: `SplitConcatFusionCompute`
- Runtime implementation: `musa/ep/src/fusion/split_concat_fusion.cc`
- `drop_constant_initializers`: `false`
-- Before-graph topology source: `musa/ep/src/fusion/split_concat_fusion_matcher.cc` finder source
+- Before-graph topology source: `test/fusion/test_split_unsqueeze_concat_fusion.py::_build_model`
## Extracted ONNX Ops
@@ -24,12 +24,13 @@ This file is generated from the current C++ fusion source and matching fusion te
```mermaid
flowchart LR
subgraph Before[Before fusion]
- B0["Split"]
- B1["Reshape"]
- B0 --> B1
+ B0["Shape"]
+ B1["Slice"]
B2["Concat"]
+ B3["Reshape"]
+ B4["Transpose"]
+ B0 --> B1
B1 --> B2
- B3["Transpose"]
B2 --> B3
end
subgraph After[After fusion]
diff --git a/musa/docs/fusion/split_unsqueeze_concat.md b/musa/docs/fusion/split_unsqueeze_concat.md
index 657010e..a78e4cc 100644
--- a/musa/docs/fusion/split_unsqueeze_concat.md
+++ b/musa/docs/fusion/split_unsqueeze_concat.md
@@ -13,7 +13,7 @@ This file is generated from the current C++ fusion source and matching fusion te
- Runtime compute: `SplitUnsqueezeConcatFusionCompute`
- Runtime implementation: `musa/ep/src/fusion/split_unsqueeze_concat_fusion.cc`
- `drop_constant_initializers`: `false`
-- Before-graph topology source: `musa/ep/src/fusion/split_unsqueeze_concat_fusion_matcher.cc` finder source
+- Before-graph topology source: `test/fusion/test_split_unsqueeze_concat_fusion.py::_build_model`
## Extracted ONNX Ops
@@ -24,15 +24,14 @@ This file is generated from the current C++ fusion source and matching fusion te
```mermaid
flowchart LR
subgraph Before[Before fusion]
- B0["Split"]
- B1["Reshape"]
+ B0["Shape"]
+ B1["Slice"]
+ B2["Concat"]
+ B3["Reshape"]
+ B4["Transpose"]
B0 --> B1
- B2["Unsqueeze"]
B1 --> B2
- B3["Concat"]
B2 --> B3
- B4["Transpose"]
- B3 --> B4
end
subgraph After[After fusion]
A0["MUSA fused node"]
diff --git a/musa/docs/supported_ops.md b/musa/docs/supported_ops.md
index 88446e4..26db28e 100644
--- a/musa/docs/supported_ops.md
+++ b/musa/docs/supported_ops.md
@@ -1,99 +1,107 @@
# Supported Ops
-- Total supported ops: **89**
-- Total registered kernels: **121**
+- Total supported ops: **97**
+- Total registered kernels: **193**
Rows are grouped by operator/domain. Multiple opset ranges in a row preserve separate ORT kernel registrations.
| Op | Domain | Opset ranges and type constraints | Source |
| --- | --- | --- | --- |
-| `Abs` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `activation/abs.cc` |
-| `Add` | `ai.onnx` | 13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/add.cc` |
+| `Abs` | `ai.onnx` | 6–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `activation/abs.cc` |
+| `Add` | `ai.onnx` | 7–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/add.cc` |
| `And` | `ai.onnx` | 7–19: `T`: bool | `logical/and.cc` |
+| `AveragePool` | `ai.onnx` | 7–9: `T`: float16, float32, float64
10–10: `T`: float16, float32, float64
11–19: `T`: float16, float32, float64 | `nn/pool.cc` |
| `BatchNormalization` | `ai.onnx` | 9–13: `T`: float16, float32, float64
14–14: `T`: float16, float32, float64
`U`: float16, float32, float64
15–19: `T`: float16, float32, float64
`T1`: float16, float32, float64
`T2`: float16, float32, float64 | `nn/batch_normalization.cc` |
| `BitwiseAnd` | `ai.onnx` | 18–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64 | `logical/bitwise_and.cc` |
-| `Cast` | `ai.onnx` | 13–19: `T1`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`T2`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/cast.cc` |
+| `Cast` | `ai.onnx` | 6–8: `T1`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`T2`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
9–12: `T1`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`T2`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `T1`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`T2`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/cast.cc` |
| `Ceil` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/ceil.cc` |
-| `Clip` | `ai.onnx` | 13–19: `T`: float16, float32, float64, int8, uint8, int16, uint16, int32, uint32, int64, uint64, bfloat16 | `math/clip.cc` |
-| `Concat` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/concat.cc` |
+| `Clip` | `ai.onnx` | 6–10: `T`: float16, float32, float64
11–11: `T`: float16, float32, float64
12–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64
13–19: `T`: float16, float32, float64, int8, uint8, int16, uint16, int32, uint32, int64, uint64, bfloat16 | `math/clip.cc` |
+| `Concat` | `ai.onnx` | 4–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/concat.cc` |
| `ConstantOfShape` | `ai.onnx` | 9–19: `T1`: int64
`T2`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool | `generator/constant_of_shape.cc` |
-| `Conv` | `ai.onnx` | 11–19: `T`: float32 | `nn/conv.cc` |
+| `Conv` | `ai.onnx` | 1–10: `T`: float32
11–19: `T`: float32 | `nn/conv.cc` |
+| `Cos` | `ai.onnx` | 7–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/cos.cc` |
| `CumSum` | `ai.onnx` | 11–13: `T`: int32, int64, uint32, uint64, float32, float64
`T2`: int32, int64
14–19: `T`: ?
`T2`: int32, int64 | `math/cumsum.cc` |
-| `Div` | `ai.onnx` | 13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/div.cc` |
+| `Div` | `ai.onnx` | 7–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/div.cc` |
| `Einsum` | `ai.onnx` | 12–19: `T`: float32 | `math/einsum.cc` |
-| `Equal` | `ai.onnx` | 13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16, bool
19–19: `T`: string | `logical/equal.cc` |
-| `Erf` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/erf.cc` |
-| `Exp` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/exp.cc` |
-| `Expand` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/expand.cc` |
+| `Equal` | `ai.onnx` | 7–10: `T`: int32, int64, bool
11–12: `T`: uint32, uint64, int32, int64, float16, float32, float64, bool
13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16, bool
19–19: `T`: string | `logical/equal.cc` |
+| `Erf` | `ai.onnx` | 9–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `activation/erf.cc` |
+| `Exp` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/exp.cc` |
+| `Expand` | `ai.onnx` | 8–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/expand.cc` |
| `Flatten` | `ai.onnx` | 1–8: `T`: float16, float32, float64
9–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/flatten.cc` |
-| `Floor` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/floor.cc` |
-| `Gather` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/gather.cc` |
+| `Floor` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/floor.cc` |
+| `Gather` | `ai.onnx` | 1–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`Tind`: int32, int64
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`Tind`: int32, int64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/gather.cc` |
| `GatherElements` | `ai.onnx` | 11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/gather_elements.cc` |
| `GatherND` | `ai.onnx` | 11–12: `T`: float32, float64, float16, int64, bfloat16, bool
`indices`: int64
13–19: `T`: float32, float64, float16, int64, bfloat16, bool
`indices`: int64 | `tensor/gather_nd.cc` |
-| `Gemm` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/gemm.cc` |
+| `Gelu` | `ai.onnx` | 20–21: `T`: float16, float32, float64, bfloat16 | `activation/gelu.cc` |
+| `Gemm` | `ai.onnx` | 7–8: `T`: float16, float32, float64
9–10: `T`: float16, float32, float64
11–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/gemm.cc` |
| `GlobalAveragePool` | `ai.onnx` | 1–19: `T`: float16, float32, float64 | `nn/pool.cc` |
| `GlobalMaxPool` | `ai.onnx` | 1–19: `T`: float16, float32, float64 | `nn/pool.cc` |
-| `Greater` | `ai.onnx` | 13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/greater.cc` |
+| `Greater` | `ai.onnx` | 7–8: `T`: float16, float32, float64
9–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/greater.cc` |
| `GreaterOrEqual` | `ai.onnx` | 13–15: `T`: uint32, uint64, int32, int64, float16, float32, float64
16–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/greater_or_equal.cc` |
| `Identity` | `ai.onnx` | 1–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–13: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
14–18: `V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
19–19: `V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/identity_op.cc` |
| `If` | `ai.onnx` | 13–15: `B`: bool
`V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
16–19: `B`: bool
`V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `controlflow/if.cc` |
| `IsNaN` | `ai.onnx` | 13–19: `T1`: float16, float32, float64, bfloat16
`T2`: bool | `logical/is_nan.cc` |
-| `LayerNormalization` | `ai.onnx` | 17–19: `T`: float32 | `nn/layer_norm.cc` |
+| `LayerNormalization` | `ai.onnx` | 17–19: `T`: float32, float16 | `nn/layer_norm.cc` |
| `LeakyRelu` | `ai.onnx` | 6–15: `T`: float16, float32, float64
16–19: `T`: float16, float32, float64, bfloat16 | `activation/leaky_relu.cc` |
-| `Less` | `ai.onnx` | 13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/less.cc` |
+| `Less` | `ai.onnx` | 7–8: `T`: float16, float32, float64
9–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/less.cc` |
| `LessOrEqual` | `ai.onnx` | 12–15: `T`: uint32, uint64, int32, int64, float16, float32, float64
16–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `logical/less_or_equal.cc` |
-| `Log` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/log.cc` |
+| `Log` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `activation/log.cc` |
| `Loop` | `ai.onnx` | 1–12: `I`: int64
`B`: bool
`V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `I`: int64
`B`: bool
`V`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool | `controlflow/loop.cc` |
-| `MatMul` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/matmul.cc` |
+| `MatMul` | `ai.onnx` | 1–8: `T`: float16, float32, float64
9–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/matmul.cc` |
| `Max` | `ai.onnx` | 13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `math/max.cc` |
| `MaxPool` | `ai.onnx` | 1–7: `T`: float16, float32, float64
8–11: `T`: float16, float32, float64
`I`: int64
12–19: `T`: float16, float32, float64, int8, uint8
`I`: int64 | `nn/pool.cc` |
+| `MemcpyFromHost` | `ai.onnx` | 1–1: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/memcpy.cc` |
+| `MemcpyToHost` | `ai.onnx` | 1–1: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/memcpy.cc` |
| `Min` | `ai.onnx` | 13–19: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16 | `math/min.cc` |
| `Mod` | `ai.onnx` | 10–12: `T`: int32, int64
13–19: `T`: int32, int64 | `math/mod.cc` |
-| `Mul` | `ai.onnx` | 13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/mul.cc` |
-| `Neg` | `ai.onnx` | 13–19: `T`: int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/neg.cc` |
-| `NonZero` | `ai.onnx` | 13–19: `T`: bool, uint8, int32, int64, float16, float32 | `tensor/nonzero.cc` |
+| `Mul` | `ai.onnx` | 7–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/mul.cc` |
+| `Neg` | `ai.onnx` | 6–12: `T`: int8, int16, int32, int64, float16, float32, float64
13–19: `T`: int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/neg.cc` |
+| `NonZero` | `ai.onnx` | 9–12: `T`: bool, uint8, int32, int64, float16, float32
13–19: `T`: bool, uint8, int32, int64, float16, float32 | `tensor/nonzero.cc` |
| `Not` | `ai.onnx` | 1–19: `T`: bool | `logical/not.cc` |
| `OneHot` | `ai.onnx` | 11–19: `T1`: int32, int64
`T2`: int32, int64
`T3`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool | `tensor/onehot.cc` |
| `Or` | `ai.onnx` | 7–19: `T`: bool | `logical/or.cc` |
| `PRelu` | `ai.onnx` | 7–15: `T`: float16, float32, float64
16–19: `T`: float16, float32, float64, bfloat16, uint32, uint64, int32, int64 | `activation/prelu.cc` |
-| `Pad` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/pad.cc` |
-| `Pow` | `ai.onnx` | 13–14: `T`: int32, int64, float16, float32, float64, bfloat16
`T1`: int32, int64, float16, float32, float64
15–19: `T`: int32, int64, float16, float32, float64, bfloat16
`T1`: int32, int64, float16, float32, float64, bfloat16 | `math/pow.cc` |
+| `Pad` | `ai.onnx` | 2–10: `T`: float16, float32, float64
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/pad.cc` |
+| `Pow` | `ai.onnx` | 7–11: `T`: float16, float32, float64
12–12: `T`: int32, int64, float16, float32, float64
`T1`: int32, int64, float16, float32, float64
13–14: `T`: int32, int64, float16, float32, float64, bfloat16
`T1`: int32, int64, float16, float32, float64
15–19: `T`: int32, int64, float16, float32, float64, bfloat16
`T1`: int32, int64, float16, float32, float64, bfloat16 | `math/pow.cc` |
| `RandomUniform` | `ai.onnx` | 1–19: `T`: float16, float32, float64 | `generator/random_uniform.cc` |
| `RandomUniformLike` | `ai.onnx` | 1–19: `T1`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`T2`: float16, float32, float64 | `generator/random_uniform.cc` |
| `Range` | `ai.onnx` | 11–19: `T`: int16, int32, int64, float32, float64 | `generator/range.cc` |
| `Reciprocal` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/reciprocal.cc` |
-| `ReduceL2` | `ai.onnx` | 13–17: `T`: float16, float32, float64, int32
18–19: `T`: float16, float32, float64, bfloat16, int32 | `reduction/reduce_l2.cc` |
-| `ReduceMax` | `ai.onnx` | 13–19: `T`: uint8, int8, float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_max.cc` |
-| `ReduceMean` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16, int32 | `reduction/reduce_mean.cc` |
-| `ReduceProd` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_prod.cc` |
-| `ReduceSum` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_sum.cc` |
-| `ReduceSumSquare` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `reduction/reduce_sum_square.cc` |
-| `Relu` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/relu.cc` |
-| `Reshape` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
19–19: `T`: string | `tensor/reshape.cc` |
+| `ReduceL2` | `ai.onnx` | 1–12: `T`: float16, float32, float64, int32
13–17: `T`: float16, float32, float64, int32
18–19: `T`: float16, float32, float64, bfloat16, int32 | `reduction/reduce_l2.cc` |
+| `ReduceMax` | `ai.onnx` | 1–12: `T`: uint8, int8, float16, float32, float64, int32, int64
13–20: `T`: uint8, int8, float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_max.cc` |
+| `ReduceMean` | `ai.onnx` | 1–12: `T`: float16, float32, float64, int32
13–19: `T`: float16, float32, float64, bfloat16, int32 | `reduction/reduce_mean.cc` |
+| `ReduceProd` | `ai.onnx` | 1–12: `T`: float16, float32, float64, int32, int64
13–19: `T`: float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_prod.cc` |
+| `ReduceSum` | `ai.onnx` | 1–12: `T`: float16, float32, float64, int32, int64
13–19: `T`: float16, float32, float64, bfloat16, int32, int64 | `reduction/reduce_sum.cc` |
+| `ReduceSumSquare` | `ai.onnx` | 1–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `reduction/reduce_sum_square.cc` |
+| `Relu` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `activation/relu.cc` |
+| `Reshape` | `ai.onnx` | 5–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–21: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
19–19: `T`: string | `tensor/reshape.cc` |
+| `Resize` | `ai.onnx` | 13–17: `T1`: float32 | `tensor/resize.cc` |
| `ReverseSequence` | `ai.onnx` | 10–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool | `tensor/reverse_sequence.cc` |
| `Round` | `ai.onnx` | 11–19: `T`: float16, float32, float64 | `math/round.cc` |
| `ScatterElements` | `ai.onnx` | 11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/scatter_elements.cc` |
| `ScatterND` | `ai.onnx` | 11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`indices`: int64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`indices`: int64 | `tensor/scatter_nd.cc` |
-| `Shape` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`T1`: int64 | `tensor/shape.cc` |
-| `Sigmoid` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/sigmoid.cc` |
+| `Shape` | `ai.onnx` | 1–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`T1`: int64
13–21: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`T1`: int64 | `tensor/shape.cc` |
+| `Sigmoid` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `activation/sigmoid.cc` |
| `Sign` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/sign.cc` |
-| `Slice` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/slice.cc` |
-| `Softmax` | `ai.onnx` | 13–17: `T`: float16, float32, float64, bfloat16 | `math/softmax.cc` |
+| `Sin` | `ai.onnx` | 7–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `math/sin.cc` |
+| `Slice` | `ai.onnx` | 1–9: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
10–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`Tind`: int32, int64
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
`Tind`: int32, int64
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool
`Tind`: int32, int64 | `tensor/slice.cc` |
+| `Softmax` | `ai.onnx` | 1–10: `T`: float16, float32, float64
11–12: `T`: float16, float32, float64
13–17: `T`: float16, float32, float64, bfloat16 | `math/softmax.cc` |
| `Softplus` | `ai.onnx` | 1–19: `T`: float16, float32, float64 | `activation/softplus.cc` |
-| `Split` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/split.cc` |
-| `Sqrt` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/sqrt.cc` |
-| `Squeeze` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/squeeze.cc` |
-| `Sub` | `ai.onnx` | 13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/sub.cc` |
+| `Split` | `ai.onnx` | 2–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/split.cc` |
+| `Sqrt` | `ai.onnx` | 6–12: `T`: float16, float32, float64
13–19: `T`: float16, float32, float64, bfloat16 | `activation/sqrt.cc` |
+| `Squeeze` | `ai.onnx` | 1–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–21: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/squeeze.cc` |
+| `Sub` | `ai.onnx` | 7–12: `T`: uint32, uint64, int32, int64, float16, float32, float64
13–13: `T`: uint32, uint64, int32, int64, float16, float32, float64, bfloat16
14–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16 | `math/sub.cc` |
| `Sum` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `math/sum.cc` |
| `Tanh` | `ai.onnx` | 13–19: `T`: float16, float32, float64, bfloat16 | `activation/tanh.cc` |
| `Tile` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/tile.cc` |
| `TopK` | `ai.onnx` | 10–10: `T`: uint8, uint16, uint32, uint64, int8, int16, float16, float32, float64, int32, int64
`I`: int64
11–19: `T`: uint8, uint16, uint32, uint64, int8, int16, float16, float32, float64, int32, int64
`I`: int64 | `math/topk.cc` |
-| `Transpose` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/transpose.cc` |
+| `Transpose` | `ai.onnx` | 1–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–21: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/transpose.cc` |
| `Unique` | `ai.onnx` | 11–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool | `tensor/unique.cc` |
-| `Unsqueeze` | `ai.onnx` | 13–19: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/unsqueeze.cc` |
+| `Unsqueeze` | `ai.onnx` | 1–10: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
11–12: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bool
13–21: `T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/unsqueeze.cc` |
| `Where` | `ai.onnx` | 9–15: `B`: bool
`T`: float16, float32, float64, int32, int64, uint8
16–19: `B`: bool
`T`: uint8, uint16, uint32, uint64, int8, int16, int32, int64, float16, float32, float64, bfloat16, bool | `tensor/where.cc` |
| `Attention` | `com.microsoft` | 1–1: `T`: float32
`M`: int32 | `llm/attention.cc` |
| `FusedGemm` | `com.microsoft` | 1–1: `T`: float16, float32, float64 | `math/fused_gemm.cc` |
| `FusedMatMul` | `com.microsoft` | 1–1: `T`: float16, float32, bfloat16 | `math/fused_matmul.cc` |
+| `Gelu` | `com.microsoft` | 1–1: `T`: float16, float32, float64, bfloat16 | `activation/gelu.cc` |
| `MultiHeadAttention` | `com.microsoft` | 1–1: `T`: float32
`QK`: float32
`M`: int32 | `llm/multihead_attention.cc` |
diff --git a/musa/ep/CMakeLists.txt b/musa/ep/CMakeLists.txt
index b19a0e5..2f6e756 100644
--- a/musa/ep/CMakeLists.txt
+++ b/musa/ep/CMakeLists.txt
@@ -13,6 +13,7 @@ set(MUSA_PLUGIN_SOURCES
src/ep_profiling.cc
src/ep_stream.cc
src/musa_arena.cc
+ src/graph/capability_predicates.cc
src/graph/cpu_preferred_nodes.cc
src/graph/graph_utils.cc
src/graph_mermaid_dump.cc
diff --git a/musa/ep/src/dtype_contracts.json b/musa/ep/src/dtype_contracts.json
index c032d0f..f53c327 100644
--- a/musa/ep/src/dtype_contracts.json
+++ b/musa/ep/src/dtype_contracts.json
@@ -32,29 +32,31 @@
{
"op": "Reshape",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "5-21",
"storage": "bfloat16,float16,float32,float64,integer,bool",
"output": "same as data input",
"compute": "alias/copy",
"accumulation": "none",
- "path": "device alias/copy",
+ "path": "device alias/copy; opset 21 only added element types",
"verified_dtypes": "float32,float16,float64,bfloat16,uint8,uint16,uint32,uint64,int8,int16,int32,int64,bool",
"tests": [
- "test/ops/test_reshape.py"
+ "test/ops/test_reshape.py",
+ "test/ops/test_opset_semantics.py"
]
},
{
"op": "Transpose",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-21",
"storage": "bfloat16,float16,float32,float64,integer,bool",
"output": "same as input",
"compute": "copy",
"accumulation": "none",
- "path": "muDNN transpose or device kernel",
+ "path": "muDNN transpose or device kernel; opset 21 only added element types",
"verified_dtypes": "float32,float16,float64,bfloat16,uint8,uint16,uint32,uint64,int8,int16,int32,int64,bool",
"tests": [
- "test/ops/test_transpose.py"
+ "test/ops/test_transpose.py",
+ "test/ops/test_opset_semantics.py"
]
},
{
@@ -148,15 +150,16 @@
{
"op": "Gemm",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "7-19",
"storage": "bfloat16,float16,float32,float64",
"output": "same as inputs",
"compute": "library tensor mode",
"accumulation": "FP32 for BF16/FP16 library path",
- "path": "muDNN MatMul+bias or muBLAS+device post kernel",
+ "path": "muDNN MatMul+bias or muBLAS+device post kernel; the pre-13 version differences are type sets and the optional C input only",
"verified_dtypes": "float32,float16,float64,bfloat16",
"tests": [
- "test/ops/test_gemm.py"
+ "test/ops/test_gemm.py",
+ "test/ops/test_opset_semantics.py"
]
},
{
@@ -190,15 +193,16 @@
{
"op": "Softmax",
"domain": "ai.onnx",
- "opset": "13-17",
+ "opset": "1-17",
"storage": "bfloat16,float16,float32,float64",
"output": "same as input",
"compute": "FP32 intermediates for BF16/FP16 device path",
"accumulation": "FP32 for BF16/FP16 device path",
- "path": "muDNN softmax or stable device kernel",
+ "path": "muDNN softmax or stable device kernel; through opset 12 the input is flattened at `axis` first (axis defaults to 1), from 13 on only `axis` is reduced",
"verified_dtypes": "float32,float16,float64,bfloat16",
"tests": [
- "test/ops/test_softmax.py"
+ "test/ops/test_softmax.py",
+ "test/ops/test_opset_semantics.py"
]
},
{
@@ -430,15 +434,16 @@
{
"op": "ReduceMax",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-20",
"storage": "uint8,int8,float16,float32,float64,bfloat16,int32,int64",
"output": "same as input",
"compute": "device reduction",
"accumulation": "FP32 for BF16/FP16; storage-compatible accumulator otherwise",
- "path": "MUSA reduction kernel",
+ "path": "MUSA reduction kernel; opset 20 only added the bool element type",
"verified_dtypes": "uint8,int8,float32,float16,float64,bfloat16,int32,int64",
"tests": [
- "test/ops/test_reduce_max.py"
+ "test/ops/test_reduce_max.py",
+ "test/ops/test_opset_semantics.py"
]
},
{
@@ -491,30 +496,32 @@
{
"op": "Slice",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-19",
"storage": "data: all fixed-size numeric and bool; starts,ends,axes,steps: int32,int64 CPU metadata",
"output": "same as data input",
"compute": "bytewise strided copy",
"accumulation": "none",
- "path": "MUSA slice kernel, device 2D copy, or device raw copy",
+ "path": "MUSA slice kernel, device 2D copy, or device raw copy; the opset 1-9 starts/ends/axes are attributes (step is fixed to 1 by those schemas)",
"verified_dtypes": "data uint8,uint16,uint32,uint64,int8,int16,int32,int64,float16,float32,float64,bfloat16,bool; metadata int32,int64",
"tests": [
- "test/ops/test_slice.py"
+ "test/ops/test_slice.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "negative or zero steps are explicitly rejected; generic device path rank is limited by kMusaMaxBroadcastRank; string and complex data are not registered"
},
{
"op": "Split",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "2-19",
"storage": "data: all fixed-size numeric and bool; split sizes: int64 CPU metadata",
"output": "each output same as data input",
"compute": "bytewise partition copy",
"accumulation": "none",
- "path": "MUSA split copy kernels; host-built device copy descriptors for irregular partitions",
+ "path": "MUSA split copy kernels; host-built device copy descriptors for irregular partitions; the opset 2-12 sizes are the `split` attribute (the opset-1 tensor-input form is not registered)",
"verified_dtypes": "data float32,float16,float64,bfloat16,int8,int16,int32,int64,uint8,uint16,uint32,uint64,bool; split sizes int64",
"tests": [
- "test/ops/test_split.py"
+ "test/ops/test_split.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "split sizes must be non-negative and sum to the selected axis extent; string and complex data are not registered"
},
@@ -536,15 +543,16 @@
{
"op": "Pad",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "2-19",
"storage": "data and constant_value: same all fixed-size numeric or bool dtype; pads,axes: int64 CPU metadata",
"output": "same as data input",
"compute": "bytewise constant fill or copy",
"accumulation": "none",
- "path": "MUSA constant-pad kernel",
+ "path": "MUSA constant-pad kernel; the opset 2-10 pads/value are attributes and the kernel converts the FLOAT value to the element type",
"verified_dtypes": "data uint8,uint16,uint32,uint64,int8,int16,int32,int64,float16,float32,float64,bfloat16,bool; pads,axes int64",
"tests": [
- "test/ops/test_pad.py"
+ "test/ops/test_pad.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "only constant mode and non-negative pads are supported; rank is limited by kMusaMaxBroadcastRank; string and complex data are not registered"
},
@@ -582,18 +590,19 @@
{
"op": "Erf",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "9-19",
"storage": "bfloat16,float16,float32,float64",
"output": "same as input",
"compute": "FP32 for BF16/FP16; FP64 for float64",
"accumulation": "none",
- "path": "muDNN unary when available; device unary kernel fallback",
+ "path": "muDNN unary when available; device unary kernel fallback. opset 9-12 reuses the same kernel class (the schema only added bfloat16 at 13), which is what the opset 10-12 Bert/GPT models need after ORT stops fusing their Gelu subgraph around the EP-claimed nodes",
"verified_dtypes": "float32,float16,float64,bfloat16",
"tests": [
"test/ops/test_erf.py",
- "test/ops/test_unary_dtype_matrix.py"
+ "test/ops/test_unary_dtype_matrix.py",
+ "test/ops/test_fusion_ops.py"
],
- "unsupported_reasons": "integer,bool,string,complex are excluded by the ONNX Erf type constraint"
+ "unsupported_reasons": "integer,bool,string,complex are excluded by the ONNX Erf type constraint; the 9-12 registration has no bfloat16 (opset 13)"
},
{
"op": "Exp",
@@ -710,16 +719,17 @@
{
"op": "Clip",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "6-19",
"storage": "bfloat16,float16,float32,float64,uint8,uint16,uint32,uint64,int8,int16,int32,int64",
"output": "same as input",
"compute": "FP32 for BF16/FP16; storage dtype otherwise",
"accumulation": "none",
- "path": "MUSA Clip device kernel; optional scalar bounds are staged to device",
+ "path": "MUSA Clip device kernel; optional scalar bounds are staged to device; the opset 6-10 bounds are FLOAT attributes the kernel converts to the element type",
"verified_dtypes": "float32,float16,float64,bfloat16,int8,int16,int32,int64,uint8,uint16,uint32,uint64",
"tests": [
"test/ops/test_clip.py",
- "test/ops/test_unary_dtype_matrix.py"
+ "test/ops/test_unary_dtype_matrix.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "bounds must be scalar and match input dtype; bool,string,complex are excluded by ONNX Clip"
},
@@ -802,16 +812,17 @@
"op": "LayerNormalization",
"domain": "ai.onnx",
"opset": "17-19",
- "storage": "float32 X/output; float32 Scale/B (B optional)",
+ "storage": "float32 or float16 X/Y; Scale/B of the same element type as X (ORT binds them to T); B optional",
"output": "same shape as X; optional float32 mean and inv_std",
"compute": "row-wise normalization over axis suffix",
"accumulation": "FP32",
- "path": "muDNN LayerNorm or MUSA FP32 device kernel",
- "verified_dtypes": "float32",
+ "path": "muDNN LayerNorm for float32, MUSA device kernel for float32/float16",
+ "verified_dtypes": "float32,float16",
"tests": [
- "test/ops/test_layer_normalization.py"
+ "test/ops/test_layer_normalization.py",
+ "test/ops/test_fusion_ops.py"
],
- "unsupported_reasons": "stash_type=1 only; Scale/B must be float and match normalized suffix; registration currently exposes float32 only"
+ "unsupported_reasons": "stash_type=1 only; Scale/B must match X in type and the normalized suffix shape; Scale/B float32 next to float16 X is rejected because ORT itself cannot load it"
},
{
"op": "Flatten",
@@ -831,45 +842,48 @@
{
"op": "Squeeze",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-21",
"storage": "all fixed-size numeric and bool; axes int64 CPU metadata",
"output": "same storage as data input",
"compute": "alias or device byte copy",
"accumulation": "none",
- "path": "MUSA device alias/copy",
+ "path": "MUSA device alias/copy; through opset 12 `axes` is an attribute; opset 21 only added element types",
"verified_dtypes": "float32,int64,uint8,bool,float16,float64,int8,int16,uint16,uint32,uint64,bfloat16",
"tests": [
- "test/ops/test_squeeze.py"
+ "test/ops/test_squeeze.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "string and complex data are not registered; axes input must be int64 and selected dimensions must be singleton"
},
{
"op": "Unsqueeze",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-21",
"storage": "all fixed-size numeric and bool; axes int64 CPU metadata",
"output": "same storage as data input",
"compute": "alias or device byte copy",
"accumulation": "none",
- "path": "MUSA device alias/copy",
+ "path": "MUSA device alias/copy; through opset 12 `axes` is an attribute; opset 21 only added element types",
"verified_dtypes": "float32,float16,int32,bool,float64,int8,int16,int64,uint8,uint16,uint32,uint64,bfloat16",
"tests": [
- "test/ops/test_unsqueeze.py"
+ "test/ops/test_unsqueeze.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "string and complex data are not registered; axes input must be int64 and axes must be unique/in range"
},
{
"op": "Shape",
"domain": "ai.onnx",
- "opset": "13-19",
+ "opset": "1-21",
"storage": "input all fixed-size numeric and bool; output int64",
"output": "int64 rank-length vector",
"compute": "host shape metadata materialization",
"accumulation": "none",
- "path": "CPU metadata output by explicit KernelDef memory contract",
+ "path": "CPU metadata output by explicit KernelDef memory contract; opset 21 only added element types",
"verified_dtypes": "input float32,float16,float64,bfloat16,uint8,uint16,uint32,uint64,int8,int16,int32,int64,bool; output int64",
"tests": [
- "test/ops/test_shape.py"
+ "test/ops/test_shape.py",
+ "test/ops/test_opset_semantics.py"
],
"unsupported_reasons": "Shape consumes tensor metadata only; string and complex inputs are not registered"
},
@@ -1340,6 +1354,81 @@
"test/ops/test_reverse_sequence.py"
],
"unsupported_reasons": "schema excludes BF16 through opset 19; string/complex unregistered; axes restricted to 0/1"
+ },
+ {
+ "op": "Sin",
+ "domain": "ai.onnx",
+ "opset": "7-19",
+ "storage": "bfloat16,float16,float32,float64",
+ "output": "same as input",
+ "compute": "FP32 for float16; FP64 for float64",
+ "accumulation": "none",
+ "path": "MUSA device unary kernel (sinf/sin); the schema is unchanged from opset 7 through 19 apart from bfloat16 at 13",
+ "verified_dtypes": "float32,float16",
+ "tests": [
+ "test/ops/test_fusion_ops.py"
+ ],
+ "unsupported_reasons": "integer,bool,string,complex are excluded by the ONNX Sin type constraint; the 7-12 registration has no bfloat16"
+ },
+ {
+ "op": "Cos",
+ "domain": "ai.onnx",
+ "opset": "7-19",
+ "storage": "bfloat16,float16,float32,float64",
+ "output": "same as input",
+ "compute": "FP32 for float16; FP64 for float64",
+ "accumulation": "none",
+ "path": "MUSA device unary kernel (cosf/cos); the schema is unchanged from opset 7 through 19 apart from bfloat16 at 13",
+ "verified_dtypes": "float32,float16",
+ "tests": [
+ "test/ops/test_fusion_ops.py"
+ ],
+ "unsupported_reasons": "integer,bool,string,complex are excluded by the ONNX Cos type constraint; the 7-12 registration has no bfloat16"
+ },
+ {
+ "op": "AveragePool",
+ "domain": "ai.onnx",
+ "opset": "7-19",
+ "storage": "float16,float32,float64",
+ "output": "same as input",
+ "compute": "window sum and divide; the divisor is the kernel size when count_include_pad is set, otherwise the number of window positions inside the input",
+ "accumulation": "FP32 for float16; FP64 for float32/float64 custom path",
+ "path": "muDNN Pooling AVGPOOL_COUNT_PAD/AVGPOOL_COUNT_WITHOUT_PAD for symmetric pads, MUSA device pooling kernel otherwise",
+ "verified_dtypes": "float32,float16",
+ "tests": [
+ "test/ops/test_fusion_ops.py"
+ ],
+ "unsupported_reasons": "ceil_mode=1 stays on the CPU EP (the capability predicate and the kernel share the rejection list in kernels/nn/average_pool_support.h); rank 3-8, concrete shapes, auto_pad NOTSET/VALID/SAME_UPPER/SAME_LOWER"
+ },
+ {
+ "op": "Gelu",
+ "domain": "ai.onnx",
+ "opset": "20-21",
+ "storage": "bfloat16,float16,float32,float64",
+ "output": "same as input",
+ "compute": "0.5*x*(1+erf(x/sqrt(2))); FP32 for float16/bfloat16",
+ "accumulation": "none",
+ "path": "MUSA device unary kernel through the shared unary dispatcher",
+ "verified_dtypes": "float32,float16",
+ "tests": [
+ "test/ops/test_fusion_ops.py"
+ ],
+ "unsupported_reasons": "approximate=tanh stays on the CPU EP (capability predicate in graph/capability_predicates.cc shares kernels/activation/gelu_support.h with the kernel)"
+ },
+ {
+ "op": "Gelu",
+ "domain": "com.microsoft",
+ "opset": "1",
+ "storage": "bfloat16,float16,float32,float64",
+ "output": "same as input",
+ "compute": "0.5*x*(1+erf(x/sqrt(2))); FP32 for float16/bfloat16",
+ "accumulation": "none",
+ "path": "MUSA device unary kernel, shared with the ONNX Gelu",
+ "verified_dtypes": "float32,float16",
+ "tests": [
+ "test/ops/test_fusion_ops.py"
+ ],
+ "unsupported_reasons": "an int approximate=1 (tanh) stays on the CPU EP; no fusion in this ORT version produces these nodes for the plugin EP yet"
}
]
}
diff --git a/musa/ep/src/ep.cc b/musa/ep/src/ep.cc
index 5b39379..582de4c 100644
--- a/musa/ep/src/ep.cc
+++ b/musa/ep/src/ep.cc
@@ -28,6 +28,7 @@
#include "ep_stream.h"
#include "fusion/fusion_matcher.h"
#include "fusion/fusion_node_compute.h"
+#include "graph/capability_predicates.h"
#include "graph/cpu_preferred_nodes.h"
#include "graph_mermaid_dump.h"
#include "plugin_ep_utils.h"
@@ -60,6 +61,15 @@ bool DisableShapeReshapeFusion() {
return disabled;
}
+// Same-submit A/B switch: disabling only stops the matcher from claiming
+// Tile+Concat nodes, so a latency difference under one binary can be attributed
+// to the fusion itself instead of to a different build.
+bool DisableTileConcatFusion() {
+ static const bool disabled =
+ EnvFlagEnabled("ORT_MUSA_DISABLE_TILE_CONCAT_FUSION");
+ return disabled;
+}
+
OrtStatus* RegisterFusionMatches(
const OrtEpApi& ep_api, OrtEpGraphSupportInfo* graph_support_info,
const std::vector& fusion_matches) {
@@ -237,7 +247,11 @@ OrtStatus* CollectSupportedNodeCandidates(
const OrtKernelDef* kernel_def = nullptr;
RETURN_IF_ERROR(ep_api.EpGraphSupportInfo_LookUpKernel(graph_support_info,
node, &kernel_def));
- if (kernel_def != nullptr) {
+ // The registry only matches op/domain/opset/dtype, so ask the per-operator
+ // predicate as well: a kernel that implements a subset of the ONNX
+ // configurations must not claim the nodes it cannot run, or they would
+ // fail during Compute() instead of running on the CPU EP.
+ if (kernel_def != nullptr && IsNodeConfigurationSupported(node)) {
candidates.nodes.push_back(node);
candidates.tentative_nodes.push_back(node);
}
@@ -363,8 +377,13 @@ MusaEp::GetCapabilityImpl(OrtEp* this_ptr, const OrtGraph* ort_graph,
std::unordered_set graph_output_names =
GetGraphOutputNames(graph);
std::vector fusion_matches;
+ // Filled while the whole graph is still visible: Compile() only gets the
+ // fused subgraph, which no longer holds the chain a shape value comes from.
+ FusionShapeExprHints shape_expr_hints;
+ FusionMaskEndHints mask_end_hints;
if (!DisableAllFusions()) {
- fusion_matches = FindFusionMatches(all_nodes, graph_output_names);
+ fusion_matches = FindFusionMatches(all_nodes, graph_output_names,
+ &shape_expr_hints, &mask_end_hints);
if (DisableShapeReshapeFusion()) {
fusion_matches.erase(
std::remove_if(fusion_matches.begin(), fusion_matches.end(),
@@ -374,6 +393,18 @@ MusaEp::GetCapabilityImpl(OrtEp* this_ptr, const OrtGraph* ort_graph,
}),
fusion_matches.end());
}
+ if (DisableTileConcatFusion()) {
+ fusion_matches.erase(
+ std::remove_if(fusion_matches.begin(), fusion_matches.end(),
+ [](const FusionMatch& match) {
+ return std::string(match.finder) ==
+ "FindTileConcatFusions";
+ }),
+ fusion_matches.end());
+ shape_expr_hints.clear();
+ }
+ ep->AddShapeExprHints(std::move(shape_expr_hints));
+ ep->AddMaskEndHints(std::move(mask_end_hints));
RETURN_IF_ERROR(RegisterFusionMatches(ep->ep_api_, graph_support_info,
fusion_matches));
}
diff --git a/musa/ep/src/ep.h b/musa/ep/src/ep.h
index 365d474..b679b54 100644
--- a/musa/ep/src/ep.h
+++ b/musa/ep/src/ep.h
@@ -22,6 +22,7 @@
#include
#include "ep_options.h"
+#include "fusion/fusion_matcher.h"
class FusionNodeCompute;
class MusaEpFactory;
@@ -47,6 +48,35 @@ class MusaEp : public OrtEp {
return fusion_computes_;
}
+ // Shape expressions a finder proved while it could still see the whole graph.
+ // Compile() only receives fused subgraphs, so GetCapability() stores them
+ // here.
+ const musa_ep::FusionShapeExprHints& ShapeExprHints() const {
+ return shape_expr_hints_;
+ }
+ void AddShapeExprHints(musa_ep::FusionShapeExprHints hints) {
+ for (auto& [key, values] : hints) {
+ auto& stored = shape_expr_hints_[key];
+ for (auto& [name, hint] : values) {
+ stored.emplace(name, std::move(hint));
+ }
+ }
+ }
+
+ // Mask-end provenance a finder proved while it could still see the whole
+ // graph; Compile() remaps the source names to fused input indices.
+ const musa_ep::FusionMaskEndHints& MaskEndHints() const {
+ return mask_end_hints_;
+ }
+ void AddMaskEndHints(musa_ep::FusionMaskEndHints hints) {
+ for (auto& [key, values] : hints) {
+ auto& stored = mask_end_hints_[key];
+ for (auto& [name, sources] : values) {
+ stored[name] = std::move(sources);
+ }
+ }
+ }
+
private:
static const char* ORT_API_CALL GetNameImpl(const OrtEp* this_ptr) noexcept;
@@ -83,4 +113,6 @@ class MusaEp : public OrtEp {
const OrtLogger& logger_;
std::unordered_map>
fusion_computes_;
+ musa_ep::FusionShapeExprHints shape_expr_hints_;
+ musa_ep::FusionMaskEndHints mask_end_hints_;
};
diff --git a/musa/ep/src/ep_data_transfer.cc b/musa/ep/src/ep_data_transfer.cc
index 0128516..730386f 100644
--- a/musa/ep/src/ep_data_transfer.cc
+++ b/musa/ep/src/ep_data_transfer.cc
@@ -108,21 +108,21 @@ void TraceD2hDeviceSynchronize(const OrtValue* src_tensor,
#endif
}
-OrtStatus* CopyPageableHostToDevice(MusaDataTransfer& impl,
- const void* src_data, void* dst_data,
- size_t bytes, musaStream_t stream,
- bool allow_pageable_bounce) {
- constexpr size_t kDefaultBounceThresholdBytes = 1024;
- const size_t bounce_threshold =
- EnvSizeOrDefault("ORT_MUSA_PAGEABLE_H2D_BOUNCE_THRESHOLD_BYTES",
- kDefaultBounceThresholdBytes);
- if (bytes < bounce_threshold) {
- return MusaStatus(impl.ort_api_, musaMemcpy(dst_data, src_data, bytes,
- musaMemcpyHostToDevice));
- }
-
- if (!allow_pageable_bounce || stream == nullptr ||
- impl.pinned_host_pool_ == nullptr ||
+// Pageable host -> device transfer. Every such write must be issued on the
+// caller's stream: a blocking musaMemcpy() on the null stream is ordered
+// against neither the kernels that are still running on the EP's compute stream
+// nor the consumers enqueued after it. ORT's memory pattern routinely hands the
+// destination of a CPU->device boundary copy - a few bytes such as a Shape
+// output - the block of a tensor that is already dead on the host side but
+// whose producer kernel has not finished on the device, so the copy read back
+// the previous owner's bytes and the shape chain fed garbage sizes to its
+// consumers (Add/Mul broadcast mismatch, Reshape "invalid argument").
+// Staging through the pinned pool keeps the copy ordered on the caller's stream
+// and lets the pageable host source be released immediately.
+OrtStatus* CopyStagedPageableHostToDevice(MusaDataTransfer& impl,
+ const void* src_data, void* dst_data,
+ size_t bytes, musaStream_t stream) {
+ if (stream == nullptr || impl.pinned_host_pool_ == nullptr ||
!EnvFlagEnabled("ORT_MUSA_ENABLE_PAGEABLE_H2D_BOUNCE", true)) {
return CopyMemcpy(impl.ort_api_, src_data, dst_data, bytes,
musaMemcpyHostToDevice, stream);
@@ -146,6 +146,30 @@ OrtStatus* CopyPageableHostToDevice(MusaDataTransfer& impl,
return nullptr;
}
+OrtStatus* CopyPageableHostToDevice(MusaDataTransfer& impl,
+ const void* src_data, void* dst_data,
+ size_t bytes, musaStream_t stream,
+ bool allow_pageable_bounce) {
+ constexpr size_t kDefaultBounceThresholdBytes = 1024;
+ const size_t bounce_threshold =
+ EnvSizeOrDefault("ORT_MUSA_PAGEABLE_H2D_BOUNCE_THRESHOLD_BYTES",
+ kDefaultBounceThresholdBytes);
+ if (bytes < bounce_threshold) {
+ return CopyStagedPageableHostToDevice(impl, src_data, dst_data, bytes,
+ stream);
+ }
+
+ if (!allow_pageable_bounce || stream == nullptr ||
+ impl.pinned_host_pool_ == nullptr ||
+ !EnvFlagEnabled("ORT_MUSA_ENABLE_PAGEABLE_H2D_BOUNCE", true)) {
+ return CopyMemcpy(impl.ort_api_, src_data, dst_data, bytes,
+ musaMemcpyHostToDevice, stream);
+ }
+
+ return CopyStagedPageableHostToDevice(impl, src_data, dst_data, bytes,
+ stream);
+}
+
OrtStatus* CopyImpl(MusaDataTransfer& impl, const OrtMemoryDevice* src_device,
const OrtMemoryDevice* dst_device, const void* src_data,
void* dst_data, size_t bytes, musaStream_t stream,
@@ -307,6 +331,29 @@ OrtStatus* CopyDeviceToHostBatch(MusaDataTransfer& impl,
} // namespace
+OrtStatus* MusaCopyTensorOnStream(OrtDataTransferImpl& impl_base,
+ const OrtValue* src_tensor,
+ OrtValue* dst_tensor, musaStream_t stream) {
+ auto& impl = static_cast(impl_base);
+
+ const OrtMemoryDevice* src_device =
+ impl.ep_api_.Value_GetMemoryDevice(src_tensor);
+ const OrtMemoryDevice* dst_device =
+ impl.ep_api_.Value_GetMemoryDevice(dst_tensor);
+
+ const void* src_data = nullptr;
+ void* dst_data = nullptr;
+ size_t bytes = 0;
+ RETURN_IF_ERROR(impl.ort_api_.GetTensorData(src_tensor, &src_data));
+ RETURN_IF_ERROR(impl.ort_api_.GetTensorMutableData(dst_tensor, &dst_data));
+ RETURN_IF_ERROR(impl.ort_api_.GetTensorSizeInBytes(src_tensor, &bytes));
+
+ // A single boundary transfer never batches, so pageable host memory may be
+ // bounced through the pinned pool to keep the copy on the caller's stream.
+ return CopyImpl(impl, src_device, dst_device, src_data, dst_data, bytes,
+ stream, /*allow_pageable_bounce*/ true);
+}
+
bool ORT_API_CALL MusaDataTransfer::CanCopyImpl(
const OrtDataTransferImpl* this_ptr,
const OrtMemoryDevice* src_memory_device,
diff --git a/musa/ep/src/ep_data_transfer.h b/musa/ep/src/ep_data_transfer.h
index 0a9138f..8ea1d1c 100644
--- a/musa/ep/src/ep_data_transfer.h
+++ b/musa/ep/src/ep_data_transfer.h
@@ -13,6 +13,8 @@
#pragma once
+#include
+
#include
#include
@@ -57,3 +59,12 @@ struct MusaDataTransfer : OrtDataTransferImpl {
uint32_t vendor_id_;
std::shared_ptr pinned_host_pool_;
};
+
+// Copies a single tensor across the host/device boundary using `impl`, ordering
+// the transfer on `stream` (the caller's own MUSA compute stream). Shares the
+// pageable-H2D staging and D2H ordering of MusaDataTransfer instead of issuing
+// a raw musaMemcpy inline. Used by the MemcpyFromHost / MemcpyToHost kernels
+// that ORT's MemcpyTransformer inserts for CPU/device placement boundaries.
+OrtStatus* MusaCopyTensorOnStream(OrtDataTransferImpl& impl,
+ const OrtValue* src_tensor,
+ OrtValue* dst_tensor, musaStream_t stream);
diff --git a/musa/ep/src/fusion/concat_matmul_fusion.cc b/musa/ep/src/fusion/concat_matmul_fusion.cc
index 2041b39..5192f57 100644
--- a/musa/ep/src/fusion/concat_matmul_fusion.cc
+++ b/musa/ep/src/fusion/concat_matmul_fusion.cc
@@ -178,7 +178,7 @@ std::string Name(Ort::ConstValueInfo value_info) {
int64_t ReadIntAttribute(Ort::ConstNode node, const std::string& name) {
Ort::ConstOpAttr attr;
Ort::Status status = node.GetAttributeByName(name, attr);
- if (!status.IsOK()) {
+ if (!status.IsOK() || !attr) {
throw std::runtime_error("missing required node attribute: " + name);
}
@@ -481,10 +481,54 @@ void RunMudnnConcat(const std::vector& concat_inputs,
void* concat_data, ONNXTensorElementDataType elem_type,
musaStream_t stream) {
::musa::dnn::Handle* handle = MudnnHandleOrThrow(stream);
- std::vector<::musa::dnn::Tensor> input_tensors(concat_inputs.size());
- for (size_t i = 0; i < concat_inputs.size(); ++i) {
- SetupMudnnTensor(input_tensors[i], concat_inputs[i].GetTensorRawData(),
- TensorShape(concat_inputs[i]), elem_type);
+ const size_t axis_index = static_cast(axis);
+
+ // muDNN Concat rejects tensors with a zero extent, while ONNX permits empty
+ // inputs along the concatenation axis. Drop those inputs before invoking
+ // muDNN; they contribute no elements and therefore do not change the
+ // concatenation result.
+ std::vector non_empty_inputs;
+ non_empty_inputs.reserve(concat_inputs.size());
+ for (Ort::ConstValue input : concat_inputs) {
+ const std::vector shape = TensorShape(input);
+ if (axis_index >= shape.size()) {
+ throw std::runtime_error("ConcatMatMul concat axis is out of range");
+ }
+ if (shape[axis_index] < 0) {
+ throw std::runtime_error(
+ "ConcatMatMul concat input has an unresolved dimension");
+ }
+ if (shape[axis_index] != 0) {
+ non_empty_inputs.push_back(input);
+ }
+ }
+
+ if (non_empty_inputs.empty()) {
+ throw std::runtime_error(
+ "ConcatMatMul concat has no non-empty inputs for a non-empty output");
+ }
+
+ // With only one non-empty input, avoid relying on whether muDNN accepts a
+ // one-input Concat. Empty inputs have already been removed, so the output
+ // is layout-identical to that input and a device-to-device copy is enough.
+ if (non_empty_inputs.size() == 1) {
+ const size_t bytes =
+ static_cast(NumElements(concat_shape)) * ElementSize(elem_type);
+ const musaError_t status =
+ musaMemcpyAsync(concat_data, non_empty_inputs[0].GetTensorRawData(),
+ bytes, musaMemcpyDeviceToDevice, stream);
+ if (status != musaSuccess) {
+ throw std::runtime_error(
+ std::string("ConcatMatMul concat copy failed: ") +
+ MusaErrorString(status));
+ }
+ return;
+ }
+
+ std::vector<::musa::dnn::Tensor> input_tensors(non_empty_inputs.size());
+ for (size_t i = 0; i < non_empty_inputs.size(); ++i) {
+ SetupMudnnTensor(input_tensors[i], non_empty_inputs[i].GetTensorRawData(),
+ TensorShape(non_empty_inputs[i]), elem_type);
}
::musa::dnn::Tensor output_tensor;
diff --git a/musa/ep/src/fusion/concat_reshape_fusion.cc b/musa/ep/src/fusion/concat_reshape_fusion.cc
index 2a3a734..c231dcf 100644
--- a/musa/ep/src/fusion/concat_reshape_fusion.cc
+++ b/musa/ep/src/fusion/concat_reshape_fusion.cc
@@ -452,7 +452,7 @@ int64_t ReadIntAttribute(Ort::ConstNode node, const std::string& name,
int64_t default_value) {
Ort::ConstOpAttr attr;
Ort::Status status = node.GetAttributeByName(name, attr);
- if (!status.IsOK()) {
+ if (!status.IsOK() || !attr) {
return default_value;
}
diff --git a/musa/ep/src/fusion/concat_split_fusion.cc b/musa/ep/src/fusion/concat_split_fusion.cc
index 66db100..6074485 100644
--- a/musa/ep/src/fusion/concat_split_fusion.cc
+++ b/musa/ep/src/fusion/concat_split_fusion.cc
@@ -42,7 +42,7 @@ int64_t ReadIntAttribute(Ort::ConstNode node, const std::string& name,
int64_t default_value) {
Ort::ConstOpAttr attr;
Ort::Status status = node.GetAttributeByName(name, attr);
- if (!status.IsOK()) {
+ if (!status.IsOK() || !attr) {
return default_value;
}
diff --git a/musa/ep/src/fusion/fusion_matcher.cc b/musa/ep/src/fusion/fusion_matcher.cc
index 51623eb..dccfcd3 100644
--- a/musa/ep/src/fusion/fusion_matcher.cc
+++ b/musa/ep/src/fusion/fusion_matcher.cc
@@ -148,7 +148,9 @@ void AddFusionMatch(std::vector& matches, const char* finder,
std::vector FindFusionMatches(
const std::vector& all_nodes,
- const std::unordered_set& graph_output_names) {
+ const std::unordered_set& graph_output_names,
+ FusionShapeExprHints* shape_expr_hints,
+ FusionMaskEndHints* mask_end_hints) {
std::unordered_set accepted_node_ids;
std::vector matches;
@@ -170,8 +172,8 @@ std::vector FindFusionMatches(
// local SDPA matcher so future ONNX-node-based expansions do not get
// preempted by the narrower attention chain.
auto mhta_scaled_dot_product_attention_fusions =
- FindMhtaScaledDotProductAttentionFusions(all_nodes, graph_output_names,
- accepted_node_ids);
+ FindMhtaScaledDotProductAttentionFusions(
+ all_nodes, graph_output_names, accepted_node_ids, mask_end_hints);
AddFusionMatch(matches, "FindMhtaScaledDotProductAttentionFusions", false,
std::move(mhta_scaled_dot_product_attention_fusions),
accepted_node_ids);
@@ -326,8 +328,8 @@ std::vector FindFusionMatches(
AddFusionMatch(matches, "FindShapeReshapeFusions", true,
std::move(shape_reshape_fusions), accepted_node_ids);
- auto tile_concat_fusions =
- FindTileConcatFusions(all_nodes, graph_output_names, accepted_node_ids);
+ auto tile_concat_fusions = FindTileConcatFusions(
+ all_nodes, graph_output_names, accepted_node_ids, shape_expr_hints);
AddFusionMatch(matches, "FindTileConcatFusions", false,
std::move(tile_concat_fusions), accepted_node_ids);
diff --git a/musa/ep/src/fusion/fusion_matcher.h b/musa/ep/src/fusion/fusion_matcher.h
index 6129c5d..f31e2db 100644
--- a/musa/ep/src/fusion/fusion_matcher.h
+++ b/musa/ep/src/fusion/fusion_matcher.h
@@ -18,13 +18,31 @@
#undef ORT_API_MANUAL_INIT
#include
+#include
#include
#include
#include "fusion/fusion_dtype.h"
+#include "fusion/shape_expr.h"
namespace musa_ep {
+// Shape expressions a finder proved on the whole graph, keyed first by a
+// signature of the fusion boundary and then by the outer value name they
+// compute. GetCapability() fills this in; Compile() reads it back because the
+// subgraph it receives does not contain the nodes the value is computed from.
+using FusionShapeExprHints =
+ std::unordered_map>;
+
+// Mask-end provenance a finder proved on the whole graph, keyed the same way:
+// boundary signature -> end value name -> names of the fused inputs whose
+// shapes determine that value. Compile() cannot see the end's producer chain
+// and remaps these names to fused input indices.
+using FusionMaskEndHints =
+ std::unordered_map>>;
+
struct FusionDTypeRejection {
std::vector nodes;
std::string reason;
@@ -40,7 +58,9 @@ struct FusionMatch {
std::vector FindFusionMatches(
const std::vector& all_nodes,
- const std::unordered_set& graph_output_names);
+ const std::unordered_set& graph_output_names,
+ FusionShapeExprHints* shape_expr_hints = nullptr,
+ FusionMaskEndHints* mask_end_hints = nullptr);
std::vector> FindSplitUnsqueezeConcatFusions(
const std::vector& all_nodes,
@@ -65,7 +85,8 @@ std::vector> FindSliceConcatFusions(
std::vector> FindTileConcatFusions(
const std::vector& all_nodes,
const std::unordered_set& graph_output_names,
- const std::unordered_set& accepted_node_ids);
+ const std::unordered_set& accepted_node_ids,
+ FusionShapeExprHints* shape_expr_hints = nullptr);
std::vector> FindGemmActivationFusions(
const std::vector& all_nodes,
const std::unordered_set& graph_output_names,
@@ -166,7 +187,8 @@ std::vector>
FindMhtaScaledDotProductAttentionFusions(
const std::vector& all_nodes,
const std::unordered_set& graph_output_names,
- const std::unordered_set& accepted_node_ids);
+ const std::unordered_set& accepted_node_ids,
+ FusionMaskEndHints* mask_end_hints = nullptr);
std::vector>
FindQkvAttentionOutputProjectionFusions(
const std::vector& all_nodes,
diff --git a/musa/ep/src/fusion/fusion_matcher_utils.cc b/musa/ep/src/fusion/fusion_matcher_utils.cc
index 2a3e018..2a8bea7 100644
--- a/musa/ep/src/fusion/fusion_matcher_utils.cc
+++ b/musa/ep/src/fusion/fusion_matcher_utils.cc
@@ -283,4 +283,180 @@ bool HasCastBoundary(const std::vector& fusion_nodes) {
return false;
}
+// Ops whose output shape is a pure function of their input shapes. A Shape
+// node reading their output can therefore be keyed on the shapes of the fused
+// inputs that feed them, without the fused node taking a new input.
+bool IsShapePreservingDataOp(const std::string& op) {
+ return op == "Transpose" || op == "Cast" || op == "Identity";
+}
+
+// Walk from `value_name` through shape-preserving producers to the fused
+// inputs that determine its shape. Returns false when the walk leaves the
+// fused boundary through anything whose output shape is data-dependent.
+bool CollectShapePreservingSources(
+ const std::string& value_name,
+ const std::unordered_map& producers,
+ const std::unordered_map& input_indices,
+ const std::unordered_set& initializer_names,
+ std::unordered_set* sources) {
+ // A value can also be pinned down from the other side: when another
+ // Transpose reads the *same* value and produces one of the fusion's inputs,
+ // that input's shape is this value's shape under a fixed permutation (every
+ // valid ONNX Transpose perm is invertible), so it can key the cache too.
+ std::unordered_map> consumers;
+ for (const auto& [output, producer] : producers) {
+ for (Ort::ConstValueInfo input : producer.GetInputs()) {
+ const std::string input_name = input.GetName();
+ if (!input_name.empty()) {
+ consumers[input_name].push_back(producer);
+ }
+ }
+ }
+
+ std::vector pending{value_name};
+ std::unordered_set visited;
+ while (!pending.empty()) {
+ const std::string name = std::move(pending.back());
+ pending.pop_back();
+ if (!visited.insert(name).second) {
+ continue;
+ }
+ auto index_it = input_indices.find(name);
+ if (index_it != input_indices.end()) {
+ sources->insert(index_it->second);
+ continue;
+ }
+ if (initializer_names.count(name) != 0) {
+ continue; // a constant's shape is static and needs no runtime key
+ }
+ bool keyed_from_consumer = false;
+ auto consumer_it = consumers.find(name);
+ if (consumer_it != consumers.end()) {
+ for (Ort::ConstNode consumer : consumer_it->second) {
+ if (!IsOnnxOp(consumer, "Transpose") ||
+ consumer.GetInputs().size() != 1) {
+ continue;
+ }
+ for (Ort::ConstValueInfo output : consumer.GetOutputs()) {
+ auto fused_it = input_indices.find(Name(output));
+ if (fused_it != input_indices.end()) {
+ sources->insert(fused_it->second);
+ keyed_from_consumer = true;
+ }
+ }
+ if (keyed_from_consumer) {
+ break;
+ }
+ }
+ }
+ if (keyed_from_consumer) {
+ continue;
+ }
+ auto producer_it = producers.find(name);
+ if (producer_it == producers.end()) {
+ return false;
+ }
+ Ort::ConstNode node = producer_it->second;
+ if (!IsShapePreservingDataOp(node.GetOperatorType())) {
+ return false;
+ }
+ for (Ort::ConstValueInfo input : node.GetInputs()) {
+ const std::string input_name = input.GetName();
+ if (!input_name.empty()) {
+ pending.push_back(input_name);
+ }
+ }
+ }
+ return true;
+}
+
+std::string FusionBoundaryKey(const std::vector& input_names,
+ const std::vector& output_names) {
+ auto normalize = [](std::vector names) {
+ names.erase(
+ std::remove_if(names.begin(), names.end(),
+ [](const std::string& name) { return name.empty(); }),
+ names.end());
+ std::sort(names.begin(), names.end());
+ names.erase(std::unique(names.begin(), names.end()), names.end());
+ std::string key;
+ for (const std::string& name : names) {
+ key += name;
+ key += '\x1e';
+ }
+ return key;
+ };
+ return normalize(input_names) + "|" + normalize(output_names);
+}
+
+bool ValueDependsOnlyOnInputShapes(
+ const std::string& value_name,
+ const std::unordered_map& producers,
+ const std::unordered_map& input_indices,
+ const std::unordered_set& initializer_names,
+ std::vector* source_indices) {
+ // Ops that only reshape or combine shape values. Reshape is included because
+ // its output shape is fixed by its (constant) shape input; a data dependency
+ // would surface as a non-initializer leaf and be rejected below.
+ static const std::unordered_set kShapeOnlyOps = {
+ "Gather", "Unsqueeze", "Squeeze", "Concat", "Add",
+ "Sub", "Mul", "Div", "Cast", "Slice",
+ "Identity", "Mod", "Floor", "Ceil", "Reshape",
+ "ReduceProd", "Constant", "ConstantOfShape"};
+ std::vector pending{value_name};
+ std::unordered_set visited;
+ std::unordered_set sources;
+ while (!pending.empty()) {
+ const std::string name = std::move(pending.back());
+ pending.pop_back();
+ if (!visited.insert(name).second) {
+ continue;
+ }
+ auto producer_it = producers.find(name);
+ if (producer_it == producers.end()) {
+ // A leaf must be a constant. A graph input or fused data input used as a
+ // value would make the result data-dependent.
+ if (initializer_names.count(name) == 0) {
+ return false;
+ }
+ continue;
+ }
+ Ort::ConstNode node = producer_it->second;
+ const std::string op = node.GetOperatorType();
+ std::vector node_inputs = node.GetInputs();
+ if (op == "Shape") {
+ if (node_inputs.size() != 1) {
+ return false;
+ }
+ const std::string source = node_inputs[0].GetName();
+ auto index_it = input_indices.find(source);
+ if (index_it == input_indices.end()) {
+ // Shape of a constant is constant and needs no runtime key. Otherwise
+ // follow shape-preserving producers (a Transpose kept only for its
+ // shape, for example) back to the fused inputs that determine it.
+ if (initializer_names.count(source) == 0 &&
+ !CollectShapePreservingSources(source, producers, input_indices,
+ initializer_names, &sources)) {
+ return false;
+ }
+ } else {
+ sources.insert(index_it->second);
+ }
+ continue;
+ }
+ if (kShapeOnlyOps.count(op) == 0) {
+ return false;
+ }
+ for (Ort::ConstValueInfo input : node_inputs) {
+ const std::string input_name = input.GetName();
+ if (!input_name.empty()) {
+ pending.push_back(input_name);
+ }
+ }
+ }
+ source_indices->assign(sources.begin(), sources.end());
+ std::sort(source_indices->begin(), source_indices->end());
+ return true;
+}
+
} // namespace musa_ep
diff --git a/musa/ep/src/fusion/fusion_matcher_utils.h b/musa/ep/src/fusion/fusion_matcher_utils.h
index ce97859..b811542 100644
--- a/musa/ep/src/fusion/fusion_matcher_utils.h
+++ b/musa/ep/src/fusion/fusion_matcher_utils.h
@@ -59,4 +59,28 @@ bool ValueHasExternalConsumerOrGraphOutput(
std::optional ReadScalarIntInitializer(Ort::ConstValueInfo value_info);
bool HasCastBoundary(const std::vector& fusion_nodes);
+// Sorted, de-duplicated signature of a fusion boundary (input and output value
+// names). GetCapability() and Compile() build the same key from the boundary,
+// which is all either side sees of the other.
+std::string FusionBoundaryKey(const std::vector& input_names,
+ const std::vector& output_names);
+
+// Returns true when `value_name` is produced only from shape arithmetic:
+// constants, plus Shape ops over inputs of the current fusion. The output is
+// then a pure function of the shapes of the fused inputs listed in
+// `source_indices` (empty means the value is constant). A Shape whose source
+// is not itself a fused input is accepted when the source is reached from one
+// through shape-preserving ops only (Transpose with a perm attribute, Cast,
+// Identity), or when another Transpose reading the same value produces one of
+// the fusion's inputs (both expose the value's shape up to a fixed
+// permutation). Anything that could make the value depend on data, on a tensor
+// outside the fusion by another path, or on an op that is not shape-only
+// returns false.
+bool ValueDependsOnlyOnInputShapes(
+ const std::string& value_name,
+ const std::unordered_map& producers,
+ const std::unordered_map& input_indices,
+ const std::unordered_set& initializer_names,
+ std::vector* source_indices);
+
} // namespace musa_ep
diff --git a/musa/ep/src/fusion/fusion_node_compute.cc b/musa/ep/src/fusion/fusion_node_compute.cc
index 87c46ce..766387f 100644
--- a/musa/ep/src/fusion/fusion_node_compute.cc
+++ b/musa/ep/src/fusion/fusion_node_compute.cc
@@ -331,7 +331,8 @@ OrtStatus* ORT_API_CALL MusaEp::CompileImpl(
finder_name = "FindMultiKqvMhaOutputProjectionFusions";
} else if (IsMhtaScaledDotProductAttentionFusionGraph(graph)) {
fusion_compute =
- CreateMhtaScaledDotProductAttentionFusion(graph, fused_node);
+ CreateMhtaScaledDotProductAttentionFusion(graph, fused_node,
+ &ep->MaskEndHints());
finder_name = "FindMhtaScaledDotProductAttentionFusions";
} else if (IsQkvAttentionOutputProjectionFusionGraph(graph)) {
fusion_compute =
@@ -420,7 +421,8 @@ OrtStatus* ORT_API_CALL MusaEp::CompileImpl(
fusion_compute = CreateShapeReshapeFusion(graph, fused_node);
finder_name = "FindShapeReshapeFusions";
} else if (IsTileConcatFusionGraph(graph)) {
- fusion_compute = CreateTileConcatFusion(graph, fused_node);
+ fusion_compute =
+ CreateTileConcatFusion(graph, fused_node, &ep->ShapeExprHints());
finder_name = "FindTileConcatFusions";
} else if (IsSliceConcatFusionGraph(graph)) {
fusion_compute = CreateSliceConcatFusion(graph, fused_node);
diff --git a/musa/ep/src/fusion/linear_fusion.cc b/musa/ep/src/fusion/linear_fusion.cc
index a93b0a5..3fe0a1d 100644
--- a/musa/ep/src/fusion/linear_fusion.cc
+++ b/musa/ep/src/fusion/linear_fusion.cc
@@ -108,7 +108,7 @@ int64_t ReadIntAttribute(Ort::ConstNode node, const std::string& name,
int64_t default_value) {
Ort::ConstOpAttr attr;
Ort::Status status = node.GetAttributeByName(name, attr);
- if (!status.IsOK()) {
+ if (!status.IsOK() || !attr) {
return default_value;
}
@@ -121,7 +121,7 @@ float ReadFloatAttribute(Ort::ConstNode node, const std::string& name,
float default_value) {
Ort::ConstOpAttr attr;
Ort::Status status = node.GetAttributeByName(name, attr);
- if (!status.IsOK()) {
+ if (!status.IsOK() || !attr) {
return default_value;
}
diff --git a/musa/ep/src/fusion/log_bucketize_gather_fusion.cc b/musa/ep/src/fusion/log_bucketize_gather_fusion.cc
index 613d0f7..f038189 100644
--- a/musa/ep/src/fusion/log_bucketize_gather_fusion.cc
+++ b/musa/ep/src/fusion/log_bucketize_gather_fusion.cc
@@ -165,7 +165,7 @@ Ort::ConstValue ReadInitializerValue(Ort::ConstValueInfo value_info,
Ort::Value ReadConstantNodeValue(Ort::ConstNode node, const char* name) {
Ort::ConstOpAttr attr;
Ort::Status attr_status = node.GetAttributeByName("value", attr);
- if (!attr_status.IsOK()) {
+ if (!attr_status.IsOK() || !attr) {
throw std::runtime_error(std::string("LogBucketizeGather ") + name +
" requires Constant value");
}
diff --git a/musa/ep/src/fusion/log_bucketize_gather_fusion_matcher.cc b/musa/ep/src/fusion/log_bucketize_gather_fusion_matcher.cc
index d10fe17..1e7ce67 100644
--- a/musa/ep/src/fusion/log_bucketize_gather_fusion_matcher.cc
+++ b/musa/ep/src/fusion/log_bucketize_gather_fusion_matcher.cc
@@ -130,7 +130,7 @@ std::optional> ReadSmallIntConstantOrInitializer(
}
Ort::ConstOpAttr attr;
Ort::Status attr_status = producer.GetAttributeByName("value", attr);
- if (!attr_status.IsOK()) {
+ if (!attr_status.IsOK() || !attr) {
return std::nullopt;
}
Ort::Value value{nullptr};
diff --git a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.cc b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.cc
index 81dce87..f9d2f99 100644
--- a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.cc
+++ b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.cc
@@ -15,14 +15,18 @@
#include
+#include
#include
#include
+#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
@@ -275,7 +279,9 @@ class MhtaScaledDotProductAttentionFusionCompute final
float scale, float mask_scale, MhtaSdpaLayout layout,
bool boolean_mask = false, bool boolean_mask_int32 = false,
size_t mask_end_index = kNoInputIndex, int64_t static_mask_end = -1,
- bool lseq_last_key_mask = false, bool no_external_mask = false)
+ bool lseq_last_key_mask = false, bool no_external_mask = false,
+ bool mask_end_cacheable = false,
+ const std::vector& mask_end_source_indices = {})
: q_index_(q_index),
k_index_(k_index),
v_index_(v_index),
@@ -288,7 +294,9 @@ class MhtaScaledDotProductAttentionFusionCompute final
mask_end_index_(mask_end_index),
static_mask_end_(static_mask_end),
lseq_last_key_mask_(lseq_last_key_mask),
- no_external_mask_(no_external_mask) {}
+ no_external_mask_(no_external_mask),
+ mask_end_cacheable_(mask_end_cacheable),
+ mask_end_source_indices_(mask_end_source_indices) {}
OrtStatus* Compute(OrtKernelContext* kernel_context) const override;
@@ -306,6 +314,15 @@ class MhtaScaledDotProductAttentionFusionCompute final
int64_t static_mask_end_;
bool lseq_last_key_mask_;
bool no_external_mask_;
+ bool mask_end_cacheable_;
+ std::vector mask_end_source_indices_;
+ // Reading the Slice end from device memory synchronizes the compute stream,
+ // so cache it — but only when the matcher proved the value is a pure
+ // function of the listed input shapes (see ValueDependsOnlyOnInputShapes).
+ mutable std::mutex mask_end_mutex_;
+ mutable bool mask_end_cached_ = false;
+ mutable int64_t mask_end_cached_value_ = -1;
+ mutable std::vector mask_end_cached_shape_key_;
};
bool IsMhtaSdpaTensorType(ONNXTensorElementDataType elem_type) {
@@ -563,6 +580,34 @@ bool SetupMhtaSdpaMaskParams(const std::vector& mask_shape,
return true;
}
+OrtStatus* ReadMaskEndValue(Ort::ConstValue mask_end, musaStream_t stream,
+ int64_t* value) {
+ if (IsGpuMemory(mask_end.GetTensorMemoryInfo())) {
+ // The EP registers MemcpyFromHost (PR #6), so a CPU-produced scalar that
+ // the fused node consumes is copied onto the device before this node runs.
+ // Read the single value back on the compute stream; the copy node precedes
+ // us on that stream. Callers cache the result, so this path runs only when
+ // the input shapes change.
+ int64_t host_value = 0;
+ musaError_t copy_status = musaMemcpyAsync(
+ &host_value, mask_end.GetTensorData(), sizeof(int64_t),
+ musaMemcpyDeviceToHost, stream);
+ if (copy_status != musaSuccess) {
+ return Ort::GetApi().CreateStatus(ORT_EP_FAIL,
+ MusaErrorString(copy_status));
+ }
+ musaError_t sync_status = musaStreamSynchronize(stream);
+ if (sync_status != musaSuccess) {
+ return Ort::GetApi().CreateStatus(ORT_EP_FAIL,
+ MusaErrorString(sync_status));
+ }
+ *value = host_value;
+ return nullptr;
+ }
+ *value = mask_end.GetTensorData()[0];
+ return nullptr;
+}
+
OrtStatus* MhtaScaledDotProductAttentionFusionCompute::Compute(
OrtKernelContext* kernel_context) const {
try {
@@ -756,13 +801,56 @@ OrtStatus* MhtaScaledDotProductAttentionFusionCompute::Compute(
Ort::ConstValue mask_end = ctx.GetInput(mask_end_index_);
auto end_info = mask_end.GetTensorTypeAndShapeInfo();
if (end_info.GetElementType() != ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 ||
- end_info.GetElementCount() != 1 ||
- IsGpuMemory(mask_end.GetTensorMemoryInfo())) {
+ end_info.GetElementCount() != 1) {
return Ort::GetApi().CreateStatus(
ORT_NOT_IMPLEMENTED,
"MHTA raw INT32 mask requires a scalar CPU Slice end");
}
- mask_end_value = mask_end.GetTensorData()[0];
+ std::vector shape_key;
+ if (mask_end_cacheable_) {
+ for (size_t index : mask_end_source_indices_) {
+ const auto dims =
+ ctx.GetInput(index).GetTensorTypeAndShapeInfo().GetShape();
+ shape_key.insert(shape_key.end(), dims.begin(), dims.end());
+ shape_key.push_back(-1);
+ }
+ }
+ const size_t key_size = shape_key.size();
+ std::lock_guard lock(mask_end_mutex_);
+ const bool cache_hit = mask_end_cacheable_ && mask_end_cached_ &&
+ mask_end_cached_shape_key_ == shape_key;
+ bool reread = false;
+ if (cache_hit) {
+ mask_end_value = mask_end_cached_value_;
+ } else {
+ RETURN_IF_ERROR(ReadMaskEndValue(mask_end, stream, &mask_end_value));
+ if (mask_end_cacheable_) {
+ mask_end_cached_ = true;
+ mask_end_cached_value_ = mask_end_value;
+ mask_end_cached_shape_key_ = std::move(shape_key);
+ }
+ }
+ if (mask_end_value != seqlen_k && cache_hit) {
+ // Not shape-derived after all: re-read once and refresh the cache.
+ RETURN_IF_ERROR(ReadMaskEndValue(mask_end, stream, &mask_end_value));
+ mask_end_cached_value_ = mask_end_value;
+ reread = true;
+ }
+ if (std::getenv("ORT_MUSA_MHTA_END_TRACE") != nullptr) {
+ static std::atomic trace_left{64};
+ if (trace_left.load() > 0) {
+ trace_left.fetch_sub(1);
+ std::fprintf(
+ stderr,
+ "MUSA_MHTA_END node=%p cacheable=%d cached=%d hit=%d reread=%d "
+ "key=%zu sel=%zu value=%lld seqlen_k=%lld\n",
+ static_cast(this), mask_end_cacheable_ ? 1 : 0,
+ mask_end_cached_ ? 1 : 0, cache_hit ? 1 : 0, reread ? 1 : 0,
+ key_size, mask_end_source_indices_.size(),
+ static_cast(mask_end_value),
+ static_cast(seqlen_k));
+ }
+ }
}
if (mask_end_value != seqlen_k) {
return Ort::GetApi().CreateStatus(
@@ -1294,8 +1382,60 @@ bool IsMhtaScaledDotProductAttentionFusionGraph(Ort::ConstGraph graph) {
lseq_bf16_shd_boundary);
}
+namespace {
+
+// Compile() only holds the fused subgraph, so the matcher's proof that the
+// end is a function of fused input shapes arrives through the hint map. Both
+// sides key it by the same boundary signature.
+void MaskEndCacheHint(
+ const musa_ep::FusionMaskEndHints* hints, Ort::ConstGraph graph,
+ Ort::ConstNode fused_node,
+ const std::unordered_map& fused_indices,
+ const std::string& end_name, bool* cacheable,
+ std::vector* source_indices) {
+ *cacheable = false;
+ source_indices->clear();
+ if (hints == nullptr) {
+ return;
+ }
+ std::vector input_names;
+ for (Ort::ConstValueInfo input : fused_node.GetInputs()) {
+ input_names.push_back(Name(input));
+ }
+ for (Ort::ConstValueInfo initializer : graph.GetInitializers()) {
+ input_names.push_back(Name(initializer));
+ }
+ std::vector output_names;
+ for (Ort::ConstValueInfo output : fused_node.GetOutputs()) {
+ output_names.push_back(Name(output));
+ }
+ auto hint_it =
+ hints->find(musa_ep::FusionBoundaryKey(input_names, output_names));
+ if (hint_it == hints->end()) {
+ return;
+ }
+ auto end_it = hint_it->second.find(end_name);
+ if (end_it == hint_it->second.end() || end_it->second.empty()) {
+ return;
+ }
+ std::vector indices;
+ indices.reserve(end_it->second.size());
+ for (const std::string& name : end_it->second) {
+ auto index_it = fused_indices.find(name);
+ if (index_it == fused_indices.end()) {
+ return;
+ }
+ indices.push_back(index_it->second);
+ }
+ *cacheable = true;
+ *source_indices = std::move(indices);
+}
+
+} // namespace
+
std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
- Ort::ConstGraph graph, Ort::ConstNode fused_node) {
+ Ort::ConstGraph graph, Ort::ConstNode fused_node,
+ const musa_ep::FusionMaskEndHints* mask_end_hints) {
Ort::ConstNode value_matmul{nullptr};
Ort::ConstNode score_matmul{nullptr};
Ort::ConstNode mul_node{nullptr};
@@ -1336,7 +1476,6 @@ std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
}
auto fused_indices = FusedInputIndices(fused_node);
-
if (where_nodes.size() == 2) {
Ort::ConstNode post_where{nullptr};
for (Ort::ConstNode node : graph.GetNodes()) {
@@ -1387,6 +1526,8 @@ std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
bool boolean_mask_int32 = false;
size_t mask_end_index = kNoInputIndex;
int64_t static_mask_end = -1;
+ bool mask_end_cacheable = false;
+ std::vector mask_end_source_indices;
auto equal_it = producers.find(Name(mask_input));
if (equal_it != producers.end() && IsOnnxOp(equal_it->second, "Equal")) {
const auto equal_inputs = equal_it->second.GetInputs();
@@ -1415,8 +1556,18 @@ std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
end.has_value()) {
static_mask_end = *end;
} else {
- mask_end_index =
- InputIndex(fused_indices, slice_it->second.GetInputs()[2]);
+ Ort::ConstValueInfo end_value = slice_it->second.GetInputs()[2];
+ const std::string end_name = Name(end_value);
+ mask_end_index = InputIndex(fused_indices, end_value);
+ MaskEndCacheHint(mask_end_hints, graph, fused_node, fused_indices,
+ end_name, &mask_end_cacheable,
+ &mask_end_source_indices);
+ // One line per fused raw-INT32-mask attention node at session
+ // creation: 0 means the end is read from the device every run.
+ std::fprintf(stderr,
+ "MUSA_MHTA_SLICE_END cacheable=%d sources=%zu\n",
+ mask_end_cacheable ? 1 : 0,
+ mask_end_source_indices.size());
}
boolean_mask_int32 = true;
}
@@ -1430,7 +1581,8 @@ std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
InputIndex(fused_indices, boundary->v.GetInputs()[0]),
InputIndex(fused_indices, mask_input), scale, 0.0f,
MhtaSdpaLayout::kBshdStridedFlash, true, boolean_mask_int32,
- mask_end_index, static_mask_end);
+ mask_end_index, static_mask_end, false, false,
+ mask_end_cacheable, mask_end_source_indices);
}
return std::make_unique(
InputIndex(fused_indices, score_inputs[0]),
@@ -1438,7 +1590,8 @@ std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
InputIndex(fused_indices, value_inputs[1]),
InputIndex(fused_indices, mask_input), scale, 0.0f,
MhtaSdpaLayout::kBhsd, true, boolean_mask_int32, mask_end_index,
- static_mask_end);
+ static_mask_end, false, false, mask_end_cacheable,
+ mask_end_source_indices);
}
if (where_nodes.size() == 1 && !add_node && !div_node && !einsum_node &&
diff --git a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.h b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.h
index 94c2a1d..94efc47 100644
--- a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.h
+++ b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion.h
@@ -19,8 +19,12 @@
#include "onnxruntime_cxx_api.h"
#undef ORT_API_MANUAL_INIT
+#include "fusion/fusion_matcher.h"
#include "fusion/fusion_node_compute.h"
bool IsMhtaScaledDotProductAttentionFusionGraph(Ort::ConstGraph graph);
+// `mask_end_hints` carries the Slice-end provenance the matcher proved on the
+// whole graph; Compile() cannot see the end's producer chain any more.
std::unique_ptr CreateMhtaScaledDotProductAttentionFusion(
- Ort::ConstGraph graph, Ort::ConstNode fused_node);
+ Ort::ConstGraph graph, Ort::ConstNode fused_node,
+ const musa_ep::FusionMaskEndHints* mask_end_hints = nullptr);
diff --git a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion_matcher.cc b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion_matcher.cc
index 342952f..3a3f94c 100644
--- a/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion_matcher.cc
+++ b/musa/ep/src/fusion/mhta_scaled_dot_product_attention_fusion_matcher.cc
@@ -590,7 +590,8 @@ bool TryAppendRawInt32MaskPreprocessing(
const std::unordered_set& graph_output_names,
const std::unordered_set& accepted_node_ids,
std::unordered_set& selected,
- std::vector& fusion_nodes) {
+ std::vector& fusion_nodes,
+ std::string* mask_end_name = nullptr) {
Ort::ConstNode equal{nullptr};
if (!GetProducer(boolean_mask, equal) || !IsOnnxOp(equal, "Equal")) {
return true;
@@ -649,6 +650,9 @@ bool TryAppendRawInt32MaskPreprocessing(
if (!starts.has_value() || *starts != std::vector{0}) return false;
if (!axes.has_value() || *axes != std::vector{3}) return false;
if (!steps.has_value() || *steps != std::vector{1}) return false;
+ if (mask_end_name != nullptr) {
+ *mask_end_name = Name(slice_inputs[2]);
+ }
for (Ort::ConstNode node : {slice, cast, equal}) {
if (!AddFusionNode(node, accepted_node_ids, selected, fusion_nodes)) {
@@ -714,7 +718,8 @@ bool CanFuseBooleanMhtaScaledDotProductAttention(
Ort::ConstNode value_matmul,
const std::unordered_set& graph_output_names,
const std::unordered_set& accepted_node_ids,
- std::vector& fusion_nodes) {
+ std::vector& fusion_nodes,
+ std::string* mask_end_name = nullptr) {
if (!IsOnnxOp(value_matmul, "MatMul") ||
accepted_node_ids.count(value_matmul.GetId()) != 0)
return false;
@@ -808,7 +813,8 @@ bool CanFuseBooleanMhtaScaledDotProductAttention(
}
if (!TryAppendRawInt32MaskPreprocessing(pre_inputs[0], pre_where, post_where,
graph_output_names, accepted_node_ids,
- selected, fusion_nodes)) {
+ selected, fusion_nodes,
+ mask_end_name)) {
return false;
}
TryAppendBshdBoundaryTransposes(score_matmul, value_matmul,
@@ -1313,24 +1319,119 @@ bool CanFuseSimRank3MhtaScaledDotProductAttention(
return true;
}
+// Prove, on the whole graph, that the raw-mask Slice end depends only on the
+// shapes of the fusion's own inputs. Compile() only receives the fused
+// subgraph, so the answer has to travel through the hint map, keyed by the
+// fusion boundary both sides can see.
+void AddMaskEndHint(
+ const std::string& end_name,
+ const std::vector& fusion_nodes,
+ const std::unordered_set& graph_output_names,
+ const std::unordered_map& producers,
+ const std::unordered_set& initializer_names,
+ musa_ep::FusionMaskEndHints* hints) {
+ if (hints == nullptr || end_name.empty() || fusion_nodes.empty()) {
+ return;
+ }
+ std::unordered_set internal;
+ std::unordered_set node_ids;
+ for (Ort::ConstNode node : fusion_nodes) {
+ node_ids.insert(node.GetId());
+ for (Ort::ConstValueInfo output : node.GetOutputs()) {
+ if (output != nullptr) {
+ internal.insert(Name(output));
+ }
+ }
+ }
+ std::vector input_names;
+ std::unordered_map input_indices;
+ for (Ort::ConstNode node : fusion_nodes) {
+ for (Ort::ConstValueInfo input : node.GetInputs()) {
+ if (input == nullptr) {
+ continue;
+ }
+ const std::string name = Name(input);
+ if (name.empty() || internal.count(name) != 0) {
+ continue;
+ }
+ if (input_indices.emplace(name, input_indices.size()).second) {
+ input_names.push_back(name);
+ }
+ }
+ }
+ std::vector output_names;
+ for (Ort::ConstNode node : fusion_nodes) {
+ for (Ort::ConstValueInfo output : node.GetOutputs()) {
+ if (output == nullptr) {
+ continue;
+ }
+ bool external = graph_output_names.count(Name(output)) != 0;
+ for (const auto& consumer : output.GetConsumers()) {
+ if (node_ids.count(consumer.node.GetId()) == 0) {
+ external = true;
+ }
+ }
+ if (external) {
+ output_names.push_back(Name(output));
+ }
+ }
+ }
+ std::vector source_indices;
+ if (!musa_ep::ValueDependsOnlyOnInputShapes(end_name, producers, input_indices,
+ initializer_names,
+ &source_indices)) {
+ return;
+ }
+ if (source_indices.empty()) {
+ return; // constant end: nothing to key on
+ }
+ std::vector source_names;
+ source_names.reserve(source_indices.size());
+ for (size_t index : source_indices) {
+ if (index >= input_names.size()) {
+ return;
+ }
+ source_names.push_back(input_names[index]);
+ }
+ (*hints)[musa_ep::FusionBoundaryKey(input_names, output_names)][end_name] =
+ std::move(source_names);
+}
+
} // namespace
std::vector>
FindMhtaScaledDotProductAttentionFusions(
const std::vector& all_nodes,
const std::unordered_set& graph_output_names,
- const std::unordered_set& accepted_node_ids) {
+ const std::unordered_set& accepted_node_ids,
+ musa_ep::FusionMaskEndHints* mask_end_hints) {
std::vector> fusions;
+ const auto producers = BuildProducerMap(all_nodes);
+ std::unordered_set initializer_names;
+ for (const Ort::ConstNode& node : all_nodes) {
+ for (Ort::ConstValueInfo input : node.GetInputs()) {
+ if (IsConstantInitializerValueInfo(input)) {
+ initializer_names.insert(Name(input));
+ }
+ }
+ }
for (Ort::ConstNode node : all_nodes) {
std::vector fusion_nodes;
- if (CanFuseBooleanMhtaScaledDotProductAttention(
- node, graph_output_names, accepted_node_ids, fusion_nodes) ||
+ std::string mask_end_name;
+ const bool boolean_fused = CanFuseBooleanMhtaScaledDotProductAttention(
+ node, graph_output_names, accepted_node_ids, fusion_nodes,
+ &mask_end_name);
+ if (boolean_fused ||
CanFuseLseqPreMaskMhtaScaledDotProductAttention(
node, graph_output_names, accepted_node_ids, fusion_nodes) ||
CanFuseMhtaScaledDotProductAttention(node, graph_output_names,
accepted_node_ids, fusion_nodes) ||
CanFuseSimRank3MhtaScaledDotProductAttention(
node, graph_output_names, accepted_node_ids, fusion_nodes)) {
+ if (boolean_fused) {
+ AddMaskEndHint(mask_end_name, fusion_nodes, graph_output_names,
+ producers, initializer_names, mask_end_hints);
+ }
fusions.push_back(std::move(fusion_nodes));
}
}
diff --git a/musa/ep/src/fusion/moe_fusion_matcher.cc b/musa/ep/src/fusion/moe_fusion_matcher.cc
index be5aed9..378b113 100644
--- a/musa/ep/src/fusion/moe_fusion_matcher.cc
+++ b/musa/ep/src/fusion/moe_fusion_matcher.cc
@@ -57,7 +57,7 @@ float ReadFloatAttribute(Ort::ConstNode node, const char* name,
float default_value) {
Ort::ConstOpAttr attr;
float value = default_value;
- return node.GetAttributeByName(name, attr).IsOK() &&
+ return node.GetAttributeByName(name, attr).IsOK() && attr &&
attr.GetValue(value).IsOK()
? value
: default_value;
@@ -88,7 +88,7 @@ std::optional> ReadIntConstant(
}
Ort::ConstOpAttr attr;
Ort::Value value{nullptr};
- if (!producer.GetAttributeByName("value", attr).IsOK() ||
+ if (!producer.GetAttributeByName("value", attr).IsOK() || !attr ||
!attr.GetTensorAttributeAsOrtValue(value).IsOK() || !value) {
return std::nullopt;
}
diff --git a/musa/ep/src/fusion/multi_kqv_mha_output_projection_fusion.cc b/musa/ep/src/fusion/multi_kqv_mha_output_projection_fusion.cc
index dad8e8d..961a1e0 100644
--- a/musa/ep/src/fusion/multi_kqv_mha_output_projection_fusion.cc
+++ b/musa/ep/src/fusion/multi_kqv_mha_output_projection_fusion.cc
@@ -376,7 +376,7 @@ size_t FusedInputIndex(const std::unordered_map& indices,
float GetFloatAttribute(Ort::ConstNode node, const char* name, float fallback) {
Ort::ConstOpAttr attr;
float value = fallback;
- return node.GetAttributeByName(name, attr).IsOK() &&
+ return node.GetAttributeByName(name, attr).IsOK() && attr &&
attr.GetValue(value).IsOK()
? value
: fallback;
diff --git a/musa/ep/src/fusion/parallel_linear_fusion_matcher.cc b/musa/ep/src/fusion/parallel_linear_fusion_matcher.cc
index b88cbaf..3df25ff 100644
--- a/musa/ep/src/fusion/parallel_linear_fusion_matcher.cc
+++ b/musa/ep/src/fusion/parallel_linear_fusion_matcher.cc
@@ -50,7 +50,7 @@ bool RequireSameParallelLinearStorageType(
int64_t ReadIntAttribute(Ort::ConstNode node, const char* name,
int64_t default_value) {
Ort::ConstOpAttr attr;
- if (!node.GetAttributeByName(name, attr).IsOK()) {
+ if (!node.GetAttributeByName(name, attr).IsOK() || !attr) {
return default_value;
}
int64_t value = default_value;
@@ -60,7 +60,7 @@ int64_t ReadIntAttribute(Ort::ConstNode node, const char* name,
float ReadFloatAttribute(Ort::ConstNode node, const char* name,
float default_value) {
Ort::ConstOpAttr attr;
- if (!node.GetAttributeByName(name, attr).IsOK()) {
+ if (!node.GetAttributeByName(name, attr).IsOK() || !attr) {
return default_value;
}
float value = default_value;
diff --git a/musa/ep/src/fusion/parallel_matmul_concat_fusion.cc b/musa/ep/src/fusion/parallel_matmul_concat_fusion.cc
index d353c40..7067745 100644
--- a/musa/ep/src/fusion/parallel_matmul_concat_fusion.cc
+++ b/musa/ep/src/fusion/parallel_matmul_concat_fusion.cc
@@ -450,7 +450,7 @@ std::unique_ptr CreateParallelMatMulConcatFusion(
int64_t concat_axis = 0;
Ort::ConstOpAttr axis_attr;
Ort::Status status = concat_node.GetAttributeByName("axis", axis_attr);
- if (!status.IsOK() || !axis_attr.GetValue(concat_axis).IsOK()) {
+ if (!status.IsOK() || !axis_attr || !axis_attr.GetValue(concat_axis).IsOK()) {
throw std::runtime_error("ParallelMatMulConcat missing Concat axis");
}
diff --git a/musa/ep/src/fusion/qkv_attention_output_projection_fusion.cc b/musa/ep/src/fusion/qkv_attention_output_projection_fusion.cc
index 57c71e1..2e33099 100644
--- a/musa/ep/src/fusion/qkv_attention_output_projection_fusion.cc
+++ b/musa/ep/src/fusion/qkv_attention_output_projection_fusion.cc
@@ -57,7 +57,8 @@ size_t Index(const std::unordered_map& m,
float FloatAttr(Ort::ConstNode n, const char* name, float d) {
Ort::ConstOpAttr a;
float x = d;
- return n.GetAttributeByName(name, a).IsOK() && a.GetValue(x).IsOK() ? x : d;
+ return n.GetAttributeByName(name, a).IsOK() && a && a.GetValue(x).IsOK() ? x
+ : d;
}
// Prefer muDNN's GEMM+bias epilogue. It removes the standalone device bias
diff --git a/musa/ep/src/fusion/qkv_attention_output_projection_fusion_matcher.cc b/musa/ep/src/fusion/qkv_attention_output_projection_fusion_matcher.cc
index 6e52761..f26f753 100644
--- a/musa/ep/src/fusion/qkv_attention_output_projection_fusion_matcher.cc
+++ b/musa/ep/src/fusion/qkv_attention_output_projection_fusion_matcher.cc
@@ -12,7 +12,7 @@ float ReadFloatAttribute(Ort::ConstNode node, const char* name,
float default_value) {
Ort::ConstOpAttr attribute;
float value = default_value;
- return node.GetAttributeByName(name, attribute).IsOK() &&
+ return node.GetAttributeByName(name, attribute).IsOK() && attribute &&
attribute.GetValue(value).IsOK()
? value
: default_value;
diff --git a/musa/ep/src/fusion/rms_norm_fusion.cc b/musa/ep/src/fusion/rms_norm_fusion.cc
index 6818767..48f8cb8 100644
--- a/musa/ep/src/fusion/rms_norm_fusion.cc
+++ b/musa/ep/src/fusion/rms_norm_fusion.cc
@@ -91,16 +91,6 @@ size_t GetMappedIndex(const std::unordered_map& indices,
return it->second;
}
-bool IsFloatGpuTensor(Ort::ConstValue value) {
- auto info = value.GetTensorTypeAndShapeInfo();
- ONNXTensorElementDataType elem_type = info.GetElementType();
- return (elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT ||
- elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16 ||
- elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 ||
- elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE) &&
- IsGpuMemory(value.GetTensorMemoryInfo());
-}
-
bool IsRmsNormStorageType(ONNXTensorElementDataType elem_type) {
return elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT ||
elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16 ||
@@ -108,6 +98,11 @@ bool IsRmsNormStorageType(ONNXTensorElementDataType elem_type) {
elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE;
}
+bool IsRmsNormFloatTensor(Ort::ConstValue value) {
+ auto info = value.GetTensorTypeAndShapeInfo();
+ return IsRmsNormStorageType(info.GetElementType());
+}
+
class DeviceTempBuffer {
public:
DeviceTempBuffer(size_t bytes, musaStream_t stream)
@@ -142,15 +137,14 @@ std::vector RmsNormInvVarShape(
return inv_var_shape;
}
-bool TryMudnnRmsNorm(Ort::ConstValue input, const void* gamma_data,
+bool TryMudnnRmsNorm(const void* input_data, const void* gamma_data,
Ort::UnownedValue output,
const std::vector& input_shape,
const std::vector& gamma_shape,
ONNXTensorElementDataType elem_type, float epsilon,
musaStream_t stream) {
- if (!IsGpuMemory(input.GetTensorMemoryInfo()) ||
- !IsGpuMemory(output.GetTensorMemoryInfo()) || gamma_data == nullptr ||
- input_shape.empty() ||
+ if (input_data == nullptr || !IsGpuMemory(output.GetTensorMemoryInfo()) ||
+ gamma_data == nullptr || input_shape.empty() ||
elem_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE) {
return false;
}
@@ -176,8 +170,7 @@ bool TryMudnnRmsNorm(Ort::ConstValue input, const void* gamma_data,
::musa::dnn::Tensor output_tensor;
::musa::dnn::Tensor gamma_tensor;
::musa::dnn::Tensor inv_var_tensor;
- if (!SetMudnnTensor(input_tensor, input.GetTensorRawData(), input_shape,
- elem_type) ||
+ if (!SetMudnnTensor(input_tensor, input_data, input_shape, elem_type) ||
!SetMudnnTensor(output_tensor, output.GetTensorMutableRawData(),
input_shape, elem_type) ||
!SetMudnnTensor(gamma_tensor, gamma_data, gamma_shape, elem_type) ||
@@ -214,9 +207,9 @@ struct RmsNormFusionCompute : FusionNodeCompute {
Ort::KernelContext ctx(kernel_context);
Ort::ConstValue input = ctx.GetInput(input_index);
Ort::ConstValue gamma = ctx.GetInput(gamma_index);
- if (!IsFloatGpuTensor(input)) {
+ if (!IsRmsNormFloatTensor(input)) {
return Ort::GetApi().CreateStatus(
- ORT_NOT_IMPLEMENTED, "RmsNorm requires a MUSA floating input");
+ ORT_NOT_IMPLEMENTED, "RmsNorm requires a floating-point input");
}
auto input_info = input.GetTensorTypeAndShapeInfo();
@@ -261,15 +254,17 @@ struct RmsNormFusionCompute : FusionNodeCompute {
}
const musaStream_t stream = GetComputeStream(ctx);
+ DeviceInputBuffer input_buffer;
DeviceInputBuffer gamma_buffer;
+ RETURN_IF_ERROR(input_buffer.Bind(input, stream));
RETURN_IF_ERROR(gamma_buffer.Bind(gamma, stream));
- if (TryMudnnRmsNorm(input, gamma_buffer.data(), output, input_shape,
- gamma_shape, input_info.GetElementType(), epsilon,
- stream)) {
+ if (TryMudnnRmsNorm(input_buffer.data(), gamma_buffer.data(), output,
+ input_shape, gamma_shape, input_info.GetElementType(),
+ epsilon, stream)) {
return nullptr;
}
return LaunchStatus(
- LaunchMusaRmsNormKernel(input.GetTensorRawData(), gamma_buffer.data(),
+ LaunchMusaRmsNormKernel(input_buffer.data(), gamma_buffer.data(),
output.GetTensorMutableRawData(), rows,
norm_size, epsilon, elem_type, stream));
} catch (const Ort::Exception& ex) {
@@ -304,11 +299,10 @@ struct CastRmsNormFusionCompute : FusionNodeCompute {
if (input_info.GetElementType() !=
ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 ||
gamma_info.GetElementType() !=
- ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 ||
- !IsGpuMemory(input.GetTensorMemoryInfo())) {
+ ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16) {
return Ort::GetApi().CreateStatus(
ORT_NOT_IMPLEMENTED,
- "CastRmsNorm requires MUSA bfloat16 input and gamma");
+ "CastRmsNorm requires bfloat16 input and gamma");
}
std::vector input_shape = input_info.GetShape();
@@ -345,15 +339,17 @@ struct CastRmsNormFusionCompute : FusionNodeCompute {
}
const musaStream_t stream = GetComputeStream(ctx);
+ DeviceInputBuffer input_buffer;
DeviceInputBuffer gamma_buffer;
+ RETURN_IF_ERROR(input_buffer.Bind(input, stream));
RETURN_IF_ERROR(gamma_buffer.Bind(gamma, stream));
- if (TryMudnnRmsNorm(input, gamma_buffer.data(), output, input_shape,
- gamma_shape, input_info.GetElementType(), epsilon,
- stream)) {
+ if (TryMudnnRmsNorm(input_buffer.data(), gamma_buffer.data(), output,
+ input_shape, gamma_shape, input_info.GetElementType(),
+ epsilon, stream)) {
return nullptr;
}
return LaunchStatus(LaunchMusaCastRmsNormBf16Kernel(
- input.GetTensorRawData(), gamma_buffer.data(),
+ input_buffer.data(), gamma_buffer.data(),
output.GetTensorMutableRawData(), rows, norm_size, epsilon, stream));
} catch (const Ort::Exception& ex) {
Ort::Status status(ex);
diff --git a/musa/ep/src/fusion/shape_expr.cc b/musa/ep/src/fusion/shape_expr.cc
new file mode 100644
index 0000000..b20bdb9
--- /dev/null
+++ b/musa/ep/src/fusion/shape_expr.cc
@@ -0,0 +1,1151 @@
+// Copyright (c) Moore Threads Technology Co., Ltd. All rights reserved.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fusion/shape_expr.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "graph/graph_utils.h"
+
+// Op encodings. `attrs` is the only per-op payload beyond the input references,
+// so every supported op states what it keeps there:
+//
+// Constant attrs = tensor shape, constant = row-major data
+// Shape inputs = {source slot}; attrs = {}
+// Gather inputs = {data, indices}; attrs = {axis}
+// Cast inputs = {data}; attrs = {target element type}
+// Concat inputs = {...}; attrs = {axis}
+// Unsqueeze inputs = {data}; attrs = axes
+// Squeeze inputs = {data}; attrs = axes (empty = drop every 1-dim)
+// Reshape inputs = {data}; attrs = {allowzero, dims...}
+// Slice inputs = {data}; attrs = {n, (axis, start, end) * n}
+// Add/Sub/Mul/Div/Mod
+// Floor/Ceil/Identity inputs = {..}; attrs = {}
+// ReduceProd inputs = {data}; attrs = {keepdims, n, axes...}
+// ConstantOfShape inputs = {shape}; attrs = {value}
+//
+// Only integer tensors are modelled. Every value carries its rank so that
+// Gather/Slice/Concat behave the way their ONNX definitions expect.
+
+namespace musa_ep {
+namespace {
+
+constexpr size_t kMaxShapeExprNodes = 512;
+constexpr int64_t kMaxConstantElements = 4096;
+
+struct Value {
+ std::vector shape;
+ std::vector data;
+};
+
+int64_t ElementCount(const std::vector& shape) {
+ int64_t count = 1;
+ for (int64_t dim : shape) {
+ if (dim < 0) {
+ return -1;
+ }
+ count *= dim;
+ }
+ return count;
+}
+
+std::vector Strides(const std::vector& shape) {
+ std::vector strides(shape.size(), 1);
+ for (size_t i = shape.size(); i-- > 0;) {
+ if (i + 1 < shape.size()) {
+ strides[i] = strides[i + 1] * shape[i + 1];
+ }
+ }
+ return strides;
+}
+
+void Advance(std::vector* coordinate,
+ const std::vector& shape) {
+ for (size_t d = coordinate->size(); d-- > 0;) {
+ if (++(*coordinate)[d] < shape[d]) {
+ return;
+ }
+ (*coordinate)[d] = 0;
+ }
+}
+
+template
+bool ReadConstantTensor(const Tensor& tensor, ShapeExprConstant* out) {
+ if (!tensor) {
+ return false;
+ }
+ auto info = tensor.GetTensorTypeAndShapeInfo();
+ const int64_t count = info.GetElementCount();
+ if (count < 0 || count > kMaxConstantElements) {
+ return false;
+ }
+ out->shape = info.GetShape();
+ if (info.GetElementType() == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64) {
+ const int64_t* data = tensor.template GetTensorData();
+ out->data.assign(data, data + count);
+ return true;
+ }
+ if (info.GetElementType() == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32) {
+ const int32_t* data = tensor.template GetTensorData();
+ out->data.reserve(static_cast(count));
+ for (int64_t i = 0; i < count; ++i) {
+ out->data.push_back(static_cast(data[i]));
+ }
+ return true;
+ }
+ return false;
+}
+
+bool TryGetIntAttribute(Ort::ConstNode node, const char* name, int64_t* value) {
+ Ort::ConstOpAttr attr;
+ Ort::Status status = node.GetAttributeByName(name, attr);
+ if (!status.IsOK() || !attr) {
+ return false;
+ }
+ int64_t read = 0;
+ status = attr.GetValue(read);
+ if (!status.IsOK()) {
+ return false;
+ }
+ *value = read;
+ return true;
+}
+
+bool TryGetIntsAttribute(Ort::ConstNode node, const char* name,
+ std::vector* values) {
+ Ort::ConstOpAttr attr;
+ Ort::Status status = node.GetAttributeByName(name, attr);
+ if (!status.IsOK() || !attr) {
+ return false;
+ }
+ std::vector read;
+ status = attr.GetValueArray(read);
+ if (!status.IsOK()) {
+ return false;
+ }
+ *values = std::move(read);
+ return true;
+}
+
+bool IsIntElementType(ONNXTensorElementDataType type) {
+ return type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 ||
+ type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64;
+}
+
+// ---------------------------------------------------------------------------
+// Evaluation
+// ---------------------------------------------------------------------------
+
+bool NormalizeAxes(const std::vector& axes, int64_t rank,
+ std::vector* out) {
+ out->clear();
+ out->reserve(axes.size());
+ for (int64_t axis : axes) {
+ int64_t normalized = axis < 0 ? axis + rank : axis;
+ if (normalized < 0 || normalized >= rank) {
+ return false;
+ }
+ out->push_back(normalized);
+ }
+ std::sort(out->begin(), out->end());
+ out->erase(std::unique(out->begin(), out->end()), out->end());
+ return true;
+}
+
+bool BroadcastShapes(const std::vector& lhs,
+ const std::vector& rhs,
+ std::vector* out) {
+ const size_t rank = std::max(lhs.size(), rhs.size());
+ out->assign(rank, 1);
+ for (size_t i = 0; i < rank; ++i) {
+ const int64_t lhs_dim =
+ i + lhs.size() < rank ? 1 : lhs[i + lhs.size() - rank];
+ const int64_t rhs_dim =
+ i + rhs.size() < rank ? 1 : rhs[i + rhs.size() - rank];
+ if (lhs_dim != rhs_dim && lhs_dim != 1 && rhs_dim != 1) {
+ return false;
+ }
+ (*out)[i] = std::max(lhs_dim, rhs_dim);
+ }
+ return true;
+}
+
+int64_t BroadcastOffset(const std::vector& source_shape,
+ const std::vector& out_shape,
+ const std::vector& out_coordinate) {
+ const size_t offset = out_shape.size() - source_shape.size();
+ int64_t index = 0;
+ for (size_t i = 0; i < source_shape.size(); ++i) {
+ const int64_t coordinate =
+ source_shape[i] == 1 ? 0 : out_coordinate[offset + i];
+ index = index * source_shape[i] + coordinate;
+ }
+ return index;
+}
+
+bool EvalRef(const ShapeExpr& expr, const ShapeExprRef& ref,
+ const std::vector>& source_shapes,
+ std::vector>* memo, Value* out);
+
+bool EvalNode(const ShapeExpr& expr, int index,
+ const std::vector>& source_shapes,
+ std::vector>* memo, Value* out) {
+ if (index < 0 || static_cast(index) >= expr.nodes.size()) {
+ return false;
+ }
+ std::optional& slot = (*memo)[static_cast(index)];
+ if (slot.has_value()) {
+ *out = *slot;
+ return true;
+ }
+
+ const ShapeExprNode& node = expr.nodes[static_cast(index)];
+ Value value;
+
+ if (node.op == "Constant") {
+ value.shape = node.attrs;
+ value.data = node.constant;
+ } else if (node.op == "Shape") {
+ if (node.inputs.size() != 1 || node.inputs[0].is_node ||
+ !EvalRef(expr, node.inputs[0], source_shapes, memo, &value)) {
+ return false;
+ }
+ } else if (node.op == "Gather") {
+ if (node.inputs.size() != 2) {
+ return false;
+ }
+ Value data;
+ Value indices;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data) ||
+ !EvalRef(expr, node.inputs[1], source_shapes, memo, &indices)) {
+ return false;
+ }
+ const int64_t rank = static_cast(data.shape.size());
+ if (rank == 0) {
+ return false;
+ }
+ int64_t axis = node.attrs.empty() ? 0 : node.attrs[0];
+ if (axis < 0) {
+ axis += rank;
+ }
+ if (axis < 0 || axis >= rank) {
+ return false;
+ }
+ const size_t axis_index = static_cast(axis);
+ const int64_t axis_dim = data.shape[axis_index];
+ int64_t inner = 1;
+ for (size_t i = axis_index + 1; i < data.shape.size(); ++i) {
+ inner *= data.shape[i];
+ }
+ value.shape.assign(data.shape.begin(), data.shape.begin() + axis);
+ value.shape.insert(value.shape.end(), indices.shape.begin(),
+ indices.shape.end());
+ value.shape.insert(value.shape.end(), data.shape.begin() + axis + 1,
+ data.shape.end());
+ const int64_t count = ElementCount(value.shape);
+ if (count < 0) {
+ return false;
+ }
+ value.data.assign(static_cast(count), 0);
+ const std::vector index_shape = indices.shape;
+ std::vector coordinate(value.shape.size(), 0);
+ for (int64_t out_index = 0; out_index < count; ++out_index) {
+ int64_t outer_index = 0;
+ for (size_t i = 0; i < axis_index; ++i) {
+ outer_index = outer_index * data.shape[i] + coordinate[i];
+ }
+ int64_t gathered = 0;
+ for (size_t i = 0; i < index_shape.size(); ++i) {
+ gathered = gathered * index_shape[i] + coordinate[axis_index + i];
+ }
+ int64_t position = indices.data[static_cast(gathered)];
+ if (position < 0) {
+ position += axis_dim;
+ }
+ if (position < 0 || position >= axis_dim) {
+ return false;
+ }
+ int64_t inner_index = 0;
+ for (size_t i = axis_index + 1; i < data.shape.size(); ++i) {
+ inner_index =
+ inner_index * data.shape[i] +
+ coordinate[axis_index + index_shape.size() + (i - axis_index - 1)];
+ }
+ value.data[static_cast(out_index)] =
+ data.data[static_cast(
+ (outer_index * axis_dim + position) * inner + inner_index)];
+ Advance(&coordinate, value.shape);
+ }
+ } else if (node.op == "Reshape") {
+ if (node.inputs.size() != 1 || node.attrs.empty()) {
+ return false;
+ }
+ Value data;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data)) {
+ return false;
+ }
+ const int64_t allowzero = node.attrs[0];
+ value.shape.assign(node.attrs.begin() + 1, node.attrs.end());
+ for (size_t i = 0; i < value.shape.size(); ++i) {
+ if (value.shape[i] == 0 && allowzero == 0) {
+ if (i >= data.shape.size()) {
+ return false;
+ }
+ value.shape[i] = data.shape[i];
+ }
+ }
+ int64_t inferred = -1;
+ for (size_t i = 0; i < value.shape.size(); ++i) {
+ if (value.shape[i] == -1) {
+ if (inferred >= 0) {
+ return false;
+ }
+ inferred = static_cast(i);
+ } else if (value.shape[i] < 0) {
+ return false;
+ }
+ }
+ const int64_t source_count = ElementCount(data.shape);
+ if (source_count < 0) {
+ return false;
+ }
+ if (inferred >= 0) {
+ int64_t known = 1;
+ for (int64_t dim : value.shape) {
+ if (dim > 0) {
+ known *= dim;
+ }
+ }
+ if (known == 0 || source_count % known != 0) {
+ return false;
+ }
+ value.shape[static_cast(inferred)] = source_count / known;
+ }
+ if (ElementCount(value.shape) != source_count) {
+ return false;
+ }
+ value.data = data.data;
+ } else if (node.op == "Slice") {
+ if (node.inputs.size() != 1 || node.attrs.empty()) {
+ return false;
+ }
+ Value data;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data)) {
+ return false;
+ }
+ const int64_t rank = static_cast(data.shape.size());
+ const int64_t entries = node.attrs[0];
+ if (entries < 0 ||
+ static_cast(entries) * 3 + 1 != node.attrs.size()) {
+ return false;
+ }
+ std::vector starts(static_cast(rank), 0);
+ std::vector ends = data.shape;
+ for (int64_t entry = 0; entry < entries; ++entry) {
+ int64_t axis = node.attrs[static_cast(1 + entry * 3)];
+ int64_t start = node.attrs[static_cast(2 + entry * 3)];
+ int64_t end = node.attrs[static_cast(3 + entry * 3)];
+ if (axis < 0) {
+ axis += rank;
+ }
+ if (axis < 0 || axis >= rank) {
+ return false;
+ }
+ const size_t axis_index = static_cast(axis);
+ const int64_t dim = data.shape[axis_index];
+ if (start < 0) {
+ start += dim;
+ }
+ if (end < 0) {
+ end += dim;
+ }
+ starts[axis_index] = std::clamp(start, 0, dim);
+ ends[axis_index] = std::clamp(end, 0, dim);
+ }
+ value.shape = data.shape;
+ for (size_t i = 0; i < value.shape.size(); ++i) {
+ value.shape[i] = ends[i] - starts[i];
+ if (value.shape[i] < 0) {
+ return false;
+ }
+ }
+ const int64_t count = ElementCount(value.shape);
+ if (count < 0) {
+ return false;
+ }
+ const std::vector strides = Strides(data.shape);
+ value.data.assign(static_cast(count), 0);
+ std::vector coordinate(value.shape.size(), 0);
+ for (int64_t i = 0; i < count; ++i) {
+ int64_t source_offset = 0;
+ for (size_t d = 0; d < coordinate.size(); ++d) {
+ source_offset += (coordinate[d] + starts[d]) * strides[d];
+ }
+ value.data[static_cast(i)] =
+ data.data[static_cast(source_offset)];
+ Advance(&coordinate, value.shape);
+ }
+ } else if (node.op == "Concat") {
+ if (node.inputs.empty() || node.attrs.empty()) {
+ return false;
+ }
+ std::vector operands(node.inputs.size());
+ for (size_t i = 0; i < node.inputs.size(); ++i) {
+ if (!EvalRef(expr, node.inputs[i], source_shapes, memo, &operands[i])) {
+ return false;
+ }
+ if (operands[i].shape.size() != operands.front().shape.size()) {
+ return false;
+ }
+ }
+ const int64_t rank = static_cast(operands.front().shape.size());
+ int64_t axis = node.attrs[0];
+ if (axis < 0) {
+ axis += rank;
+ }
+ if (rank == 0 || axis < 0 || axis >= rank) {
+ return false;
+ }
+ const size_t axis_index = static_cast(axis);
+ value.shape = operands.front().shape;
+ value.shape[axis_index] = 0;
+ for (const Value& operand : operands) {
+ for (size_t d = 0; d < value.shape.size(); ++d) {
+ if (d == axis_index) {
+ continue;
+ }
+ if (operand.shape[d] != value.shape[d]) {
+ return false;
+ }
+ }
+ value.shape[axis_index] += operand.shape[axis_index];
+ }
+ const int64_t count = ElementCount(value.shape);
+ if (count < 0) {
+ return false;
+ }
+ value.data.assign(static_cast(count), 0);
+ int64_t inner = 1;
+ for (size_t i = axis_index + 1; i < value.shape.size(); ++i) {
+ inner *= value.shape[i];
+ }
+ int64_t outer = 1;
+ for (size_t i = 0; i < axis_index; ++i) {
+ outer *= value.shape[i];
+ }
+ const int64_t row = value.shape[axis_index] * inner;
+ for (int64_t o = 0; o < outer; ++o) {
+ int64_t written = o * row;
+ for (const Value& operand : operands) {
+ const int64_t chunk = operand.shape[axis_index] * inner;
+ const int64_t begin = o * chunk;
+ std::copy(operand.data.begin() + begin,
+ operand.data.begin() + begin + chunk,
+ value.data.begin() + written);
+ written += chunk;
+ }
+ }
+ } else if (node.op == "Unsqueeze") {
+ if (node.inputs.size() != 1) {
+ return false;
+ }
+ Value data;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data)) {
+ return false;
+ }
+ const int64_t out_rank = static_cast(data.shape.size()) +
+ static_cast(node.attrs.size());
+ std::vector axes;
+ if (!NormalizeAxes(node.attrs, out_rank, &axes)) {
+ return false;
+ }
+ value.shape.clear();
+ size_t source = 0;
+ for (int64_t i = 0; i < out_rank; ++i) {
+ if (std::binary_search(axes.begin(), axes.end(), i)) {
+ value.shape.push_back(1);
+ } else {
+ if (source >= data.shape.size()) {
+ return false;
+ }
+ value.shape.push_back(data.shape[source++]);
+ }
+ }
+ if (source != data.shape.size()) {
+ return false;
+ }
+ value.data = data.data;
+ } else if (node.op == "Squeeze") {
+ if (node.inputs.size() != 1) {
+ return false;
+ }
+ Value data;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data)) {
+ return false;
+ }
+ std::vector axes;
+ if (node.attrs.empty()) {
+ for (size_t i = 0; i < data.shape.size(); ++i) {
+ if (data.shape[i] == 1) {
+ axes.push_back(static_cast(i));
+ }
+ }
+ } else if (!NormalizeAxes(node.attrs,
+ static_cast(data.shape.size()), &axes)) {
+ return false;
+ }
+ value.shape.clear();
+ for (size_t i = 0; i < data.shape.size(); ++i) {
+ if (std::binary_search(axes.begin(), axes.end(),
+ static_cast(i))) {
+ if (data.shape[i] != 1) {
+ return false;
+ }
+ continue;
+ }
+ value.shape.push_back(data.shape[i]);
+ }
+ value.data = data.data;
+ } else if (node.op == "ReduceProd") {
+ if (node.inputs.size() != 1 || node.attrs.size() < 2) {
+ return false;
+ }
+ Value data;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &data)) {
+ return false;
+ }
+ const int64_t keepdims = node.attrs[0];
+ const int64_t axis_count = node.attrs[1];
+ if (axis_count < 0 ||
+ static_cast(axis_count) + 2 != node.attrs.size()) {
+ return false;
+ }
+ std::vector axes;
+ const int64_t rank = static_cast(data.shape.size());
+ if (axis_count == 0) {
+ for (int64_t i = 0; i < rank; ++i) {
+ axes.push_back(i);
+ }
+ } else if (!NormalizeAxes(std::vector(node.attrs.begin() + 2,
+ node.attrs.end()),
+ rank, &axes)) {
+ return false;
+ }
+ std::vector reduced(data.shape.size(), false);
+ for (int64_t axis : axes) {
+ reduced[static_cast(axis)] = true;
+ }
+ value.shape.clear();
+ for (size_t i = 0; i < data.shape.size(); ++i) {
+ if (reduced[i]) {
+ if (keepdims != 0) {
+ value.shape.push_back(1);
+ }
+ } else {
+ value.shape.push_back(data.shape[i]);
+ }
+ }
+ const int64_t count = ElementCount(value.shape);
+ const int64_t source_count = ElementCount(data.shape);
+ if (count < 0 || source_count < 0) {
+ return false;
+ }
+ value.data.assign(static_cast(count), 1);
+ std::vector coordinate(data.shape.size(), 0);
+ for (int64_t i = 0; i < source_count; ++i) {
+ int64_t out_index = 0;
+ size_t out_dim = 0;
+ for (size_t d = 0; d < data.shape.size(); ++d) {
+ if (reduced[d]) {
+ continue;
+ }
+ out_index = out_index * value.shape[out_dim++] + coordinate[d];
+ }
+ value.data[static_cast(out_index)] *=
+ data.data[static_cast(i)];
+ Advance(&coordinate, data.shape);
+ }
+ } else if (node.op == "ConstantOfShape") {
+ if (node.inputs.size() != 1 || node.attrs.empty()) {
+ return false;
+ }
+ Value shape_value;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &shape_value)) {
+ return false;
+ }
+ value.shape = shape_value.data;
+ const int64_t count = ElementCount(value.shape);
+ if (count < 0) {
+ return false;
+ }
+ value.data.assign(static_cast(count), node.attrs[0]);
+ } else if (node.op == "Cast" || node.op == "Floor" || node.op == "Ceil" ||
+ node.op == "Identity") {
+ if (node.inputs.size() != 1 ||
+ !EvalRef(expr, node.inputs[0], source_shapes, memo, &value)) {
+ return false;
+ }
+ } else if (node.op == "Add" || node.op == "Sub" || node.op == "Mul" ||
+ node.op == "Div" || node.op == "Mod") {
+ if (node.inputs.size() != 2) {
+ return false;
+ }
+ Value lhs;
+ Value rhs;
+ if (!EvalRef(expr, node.inputs[0], source_shapes, memo, &lhs) ||
+ !EvalRef(expr, node.inputs[1], source_shapes, memo, &rhs)) {
+ return false;
+ }
+ if (!BroadcastShapes(lhs.shape, rhs.shape, &value.shape)) {
+ return false;
+ }
+ const int64_t count = ElementCount(value.shape);
+ if (count < 0) {
+ return false;
+ }
+ value.data.assign(static_cast(count), 0);
+ std::vector coordinate(value.shape.size(), 0);
+ for (int64_t i = 0; i < count; ++i) {
+ const int64_t a = lhs.data[static_cast(
+ BroadcastOffset(lhs.shape, value.shape, coordinate))];
+ const int64_t b = rhs.data[static_cast