Skip to content

Add Stft op support to the OSS QNN backend (#24031) - #24031

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
billmguo:export-D126511338
Oct 21, 2026
Merged

meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
billmguo:export-D126511338

Conversation

@billmguo

@billmguo billmguo commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

The OSS ExecuTorch QNN backend (executorch/backends/qualcomm) had no torch.stft support, so any model using it (e.g. the llama4 STITO speech front end) decomposed stft into unfold + _fft_r2c on CPU. QNN has a native Stft op on HTP (FP16/FP32 only, no quantized configuration, per the QNN 2.48/2.50 HtpOpDefSupplement), so this lowers aten.stft.default to it.

ConvertComplexStft (export pass): torch.stft returns a complex tensor that QNN cannot represent, so this rewrites the supported complex consumers into the real-valued form: view_as_real(stft(return_complex=True)) becomes stft(return_complex=False), abs(stft) becomes sqrt(sum(x**2, -1)), and the common power-spectrum case abs(stft)**2 becomes sum(x**2, -1). An stft with any other complex consumer is left alone and decomposes as before.

op_stft.py: lowers the real-valued aten.stft.default as Reshape -> Stft -> Transpose [-> Reshape]. QNN takes [batch, signal_length, 1] and produces [batch, frames, freq, 2] while torch produces [batch, freq, frames, 2] (no batch dim for a 1D signal). The window is zero-padded on both sides to n_fft, matching torch when win_length < n_fft.

partition/utils.py: aten.stft.default is added to get_skip_decomp_table(), and filter_fn only preserves nodes QNN can run (real output, float input, normalized=False, and a window given or win_length == n_fft, since QNN's default rectangular window spans n_fft). Everything else decomposes to the existing CPU path, so models that don't match are unchanged.

fbcode and xplat copies are kept byte-identical.

Differential Revision: D126511338

cc @cbilgin @psiddh

@billmguo
billmguo requested a review from psiddh as a code owner October 20, 2026 22:26
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 20, 2026
@pytorch-bot

pytorch-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24031

Note: Links to docs will display an error until the docs builds have been completed.

❌ 1 Cancelled Job, 6 Pending

As of commit 241a7de with merge base a4e65bf (image):

CANCELLED JOB - The following job was cancelled. Please retry:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-codesync

meta-codesync Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

@billmguo has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126511338.

@psiddh psiddh added the module: qnn Issues related to Qualcomm's QNN delegate and code under backends/qualcomm/ label Oct 20, 2026
@meta-codesync meta-codesync Bot changed the title Add Stft op support to the OSS QNN backend Add Stft op support to the OSS QNN backend (#24031) Oct 20, 2026
billmguo added a commit to billmguo/executorch that referenced this pull request Oct 20, 2026
Summary:

The OSS ExecuTorch QNN backend (`executorch/backends/qualcomm`) had no `torch.stft` support, so any model using it (e.g. the llama4 STITO speech front end) decomposed stft into `unfold` + `_fft_r2c` on CPU. QNN has a native `Stft` op on HTP (FP16/FP32 only, no quantized configuration, per the QNN 2.48/2.50 HtpOpDefSupplement), so this lowers `aten.stft.default` to it.

`ConvertComplexStft` (export pass): `torch.stft` returns a complex tensor that QNN cannot represent, so this rewrites the supported complex consumers into the real-valued form: `view_as_real(stft(return_complex=True))` becomes `stft(return_complex=False)`, `abs(stft)` becomes `sqrt(sum(x**2, -1))`, and the common power-spectrum case `abs(stft)**2` becomes `sum(x**2, -1)`. An stft with any other complex consumer is left alone and decomposes as before.

`op_stft.py`: lowers the real-valued `aten.stft.default` as Reshape -> Stft -> Transpose [-> Reshape]. QNN takes `[batch, signal_length, 1]` and produces `[batch, frames, freq, 2]` while torch produces `[batch, freq, frames, 2]` (no batch dim for a 1D signal). The window is zero-padded on both sides to `n_fft`, matching torch when `win_length < n_fft`.

`partition/utils.py`: `aten.stft.default` is added to `get_skip_decomp_table()`, and `filter_fn` only preserves nodes QNN can run (real output, float input, `normalized=False`, and a window given or `win_length == n_fft`, since QNN's default rectangular window spans `n_fft`). Everything else decomposes to the existing CPU path, so models that don't match are unchanged.

fbcode and xplat copies are kept byte-identical.

Differential Revision: D126511338
billmguo added a commit to billmguo/executorch that referenced this pull request Oct 20, 2026
Summary:
Pull Request resolved: pytorch#24031

The OSS ExecuTorch QNN backend (`executorch/backends/qualcomm`) had no `torch.stft` support, so any model using it (e.g. the llama4 STITO speech front end) decomposed stft into `unfold` + `_fft_r2c` on CPU. QNN has a native `Stft` op on HTP (FP16/FP32 only, no quantized configuration, per the QNN 2.48/2.50 HtpOpDefSupplement), so this lowers `aten.stft.default` to it.

`ConvertComplexStft` (export pass): `torch.stft` returns a complex tensor that QNN cannot represent, so this rewrites the supported complex consumers into the real-valued form: `view_as_real(stft(return_complex=True))` becomes `stft(return_complex=False)`, `abs(stft)` becomes `sqrt(sum(x**2, -1))`, and the common power-spectrum case `abs(stft)**2` becomes `sum(x**2, -1)`. An stft with any other complex consumer is left alone and decomposes as before.

`op_stft.py`: lowers the real-valued `aten.stft.default` as Reshape -> Stft -> Transpose [-> Reshape]. QNN takes `[batch, signal_length, 1]` and produces `[batch, frames, freq, 2]` while torch produces `[batch, freq, frames, 2]` (no batch dim for a 1D signal). The window is zero-padded on both sides to `n_fft`, matching torch when `win_length < n_fft`.

`partition/utils.py`: `aten.stft.default` is added to `get_skip_decomp_table()`, and `filter_fn` only preserves nodes QNN can run (real output, float input, `normalized=False`, and a window given or `win_length == n_fft`, since QNN's default rectangular window spans `n_fft`). Everything else decomposes to the existing CPU path, so models that don't match are unchanged.

fbcode and xplat copies are kept byte-identical.

Differential Revision: D126511338
Summary:

The OSS ExecuTorch QNN backend (`executorch/backends/qualcomm`) had no `torch.stft` support, so any model using it (e.g. the llama4 STITO speech front end) decomposed stft into `unfold` + `_fft_r2c` on CPU. QNN has a native `Stft` op on HTP (FP16/FP32 only, no quantized configuration, per the QNN 2.48/2.50 HtpOpDefSupplement), so this lowers `aten.stft.default` to it.

`ConvertComplexStft` (export pass): `torch.stft` returns a complex tensor that QNN cannot represent, so this rewrites the supported complex consumers into the real-valued form: `view_as_real(stft(return_complex=True))` becomes `stft(return_complex=False)`, `abs(stft)` becomes `sqrt(sum(x**2, -1))`, and the common power-spectrum case `abs(stft)**2` becomes `sum(x**2, -1)`. An stft with any other complex consumer is left alone and decomposes as before.

`op_stft.py`: lowers the real-valued `aten.stft.default` as Reshape -> Stft -> Transpose [-> Reshape]. QNN takes `[batch, signal_length, 1]` and produces `[batch, frames, freq, 2]` while torch produces `[batch, freq, frames, 2]` (no batch dim for a 1D signal). The window is zero-padded on both sides to `n_fft`, matching torch when `win_length < n_fft`.

`partition/utils.py`: `aten.stft.default` is added to `get_skip_decomp_table()`, and `filter_fn` only preserves nodes QNN can run (real output, float input, `normalized=False`, and a window given or `win_length == n_fft`, since QNN's default rectangular window spans `n_fft`). Everything else decomposes to the existing CPU path, so models that don't match are unchanged.

fbcode and xplat copies are kept byte-identical.

Differential Revision: D126511338
@billmguo

Copy link
Copy Markdown
Contributor Author

@pytorchbot label "release notes: none"

@pytorch-bot pytorch-bot Bot added the release notes: none Do not include this in the release notes label Oct 21, 2026
@meta-codesync
meta-codesync Bot merged commit 20675ac into pytorch:main Oct 21, 2026
225 of 226 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported module: qnn Issues related to Qualcomm's QNN delegate and code under backends/qualcomm/ release notes: none Do not include this in the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants