optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out - #4

Merged
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels
Apr 24, 2026
Merged

optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_out#4
jgibson2 merged 4 commits into
polycamfrom
jgibson/neon-custom-kernels

Conversation

@jgibson2

@jgibson2jgibson2 commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR has been restructured. Previously it added NEON kernels via a separate `custom_kernels` static library and `kernels/custom/` source directory. That approach worked but didn't fit the existing `kernels/optimized/` integration pattern. This version ports both kernels to be proper optimized kernels, registered in `optimized.yaml` alongside `opt_add`, `opt_gelu`, etc., and sourced from `kernels/optimized/cpu/`.

Also opened as pytorch/executorch#19119 — this PR can be retired once that lands and is synced into polycam/main.

Measured end-to-end on a real depth model (Pixel 9 / arm64-v8a, fp16 inputs, shapes representative of the model's hot path):

OpPortableThis PRSpeedup
`grid_sampler_2d.out`17.3 ms3.4 ms5.1x
`sum.IntList_out` (5 calls, aggregate)3.0 ms0.56 ms5.4x

End-to-end `forward()` p50 on a 9.4 MB polycam depth model: 94 ms → 97 ms (within noise).

`grid_sampler_2d.out`

aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per iteration with a vectorized FMA chain. fp16 inputs promoted to fp32 for weight computation and accumulation, cast back on store — avoids fp16 catastrophic cancellation on `(ix_se - ix)`-style weight subtractions. Unsupported modes and non-aarch64 targets delegate to the portable kernel.

`sum.IntList_out`

`at::vec::Vectorized`-based implementation of the single-dim reduction fast path (both innermost-contiguous and strided cases). Cross-architecture SIMD; always accumulates in fp32. Multi-dim reductions, dtype-converting reductions, and complex types delegate to portable. ~2x faster than a handwritten-NEON equivalent, because the compiler optimizes the fp16→fp32 gather better than a manual temp-buffer implementation.

Integration

  • Sources added to `OPTIMIZED_KERNELS_SRCS` in `build_variables.bzl` and to `OPTIMIZED_ATEN_OPS` in `op_registration_util.bzl` — single source of truth for both Buck and CMake builds.
  • `optimized.yaml` registers the ops with the standard `opt_*` naming convention.
  • `kernels/optimized/CMakeLists.txt` scopes the `-march=armv8.2-a+fp16` flag to just `op_grid_sampler_2d.cpp` via `set_source_files_properties`, so x86_64 builds are unaffected.

Included `preset.cmake` fix

Carries a one-char fix (quote `${DESCRIPTION}` in the `set(...CACHE...)` call) that's also in PR #2, so this branch builds independently of #2's merge order. Drops to a no-op once #2 lands.

Test plan

  • Builds cleanly for Android arm64-v8a, Android x86_64 (via `scripts/build_android_library.sh`).
  • End-to-end Pixel 9 latency on the polycam depth model matches the previous handwritten-NEON integration (within run-to-run noise) while producing more accurate fp16 outputs.
  • The 5.1x / 5.4x op-level speedups are reproducible via `ETDump` analysis.

🤖 Generated with Claude Code

@jgibson2
jgibson2 changed the base branch from main to polycamApril 24, 2026 15:44

@meredithbaynemeredithbayne left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Before you merge, could you double check the rounding for the fp16 and fp32 paths? I'm not sure if fp16 grid_sampler has divergent weight-rounding between the NEON body and the scalar tail (tail rounds weights after the multiply; body rounds before)


// We only optimize bilinear + zeros — the only mode our model uses.
// Other modes fall through to the scalar path.
ET_KERNEL_CHECK(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need any of the other checks here, like the dim order check?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Added

@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Pushed two follow-up commits:

  • 995f6e3 — NEON fp16 grid_sampler now accumulates in fp32 internally (fp16 loads/stores, fp32 FMA chain). Fixes catastrophic cancellation on weight computation that was masking real precision divergence from the portable reference. Unmeasurable perf delta; max_abs vs fp32-reference drops ~0.1 → 0.
  • 0c2ba68 — adds a standalone on-device verify binary (verify_custom_kernels, opt-in via -DEXECUTORCH_BUILD_CUSTOM_VERIFY=ON) that cross-checks both NEON kernels against an fp32 reference across the shapes the polycam depth model uses. All 12 test cases pass; output prints both absolute and relative-off-zero divergence per case.

Reference for the fp16 tests is portable run on up-cast fp32 inputs, then down-cast to fp16. This keeps the verifier meaningful regardless of whether the portable-fp16 precision fix (pytorch#19117) has landed. Once that fix is merged and synced into the branch, both sides will converge further without a test-harness change.

…List_out
Two new optimized CPU kernels registered alongside the existing
optimized_kernels library. Both replace the portable reference kernel
(still available as fallback for unsupported inputs) with a vectorized
implementation that accumulates in fp32, avoiding the fp16 precision
issues noted in pytorch#19117 for grid_sampler_2d bilinear.
Measured end-to-end on a real depth model (Pixel 9, fp16 inputs, shapes
representative of the model's hot path):
| Op | Portable | This PR | Speedup |
| -------------------------------- | -------- | ------- | ------- |
| grid_sampler_2d.out | 17.3 ms | 3.4 ms | 5.1x |
| sum.IntList_out (5 calls, total) | 3.0 ms | 0.56 ms | 5.4x |
### grid_sampler_2d.out
aarch64 NEON, bilinear + zeros padding only. Processes 4 channels per
iteration with a vectorized FMA chain. fp16 inputs are promoted to fp32
for weight computation and accumulation, then cast back on store — the
portable kernel's fp16 weight subtractions like `(ix_se - ix)` otherwise
suffer catastrophic cancellation. Unsupported modes and non-aarch64
targets delegate to the portable kernel.
### sum.IntList_out
at::vec::Vectorized<float>-based implementation of the single-dim
reduction fast path (both innermost-contiguous and strided cases).
Cross-architecture SIMD via PyTorch's existing vector abstraction;
accumulates in fp32 regardless of input dtype. Multi-dim reductions,
dtype-converting reductions, and complex types delegate to portable.
### Integration
- Sources added to OPTIMIZED_KERNELS_SRCS in build_variables.bzl and to
OPTIMIZED_ATEN_OPS in op_registration_util.bzl. Single source of
truth for both Buck and CMake builds.
- optimized.yaml registers the ops with the standard opt_* naming
convention used by sibling kernels.
- kernels/optimized/CMakeLists.txt scopes the -march=armv8.2-a+fp16
flag to just op_grid_sampler_2d.cpp via set_source_files_properties,
so x86_64 builds are unaffected. The kernel has #ifdef __aarch64__
guards and falls through to portable on non-arm64 targets.
Same one-char fix as pytorch#19117 (and our PR #2): the
DESCRIPTION argument to `set(...CACHE TYPE DOCSTRING)` was expanded
unquoted, so multi-word descriptions on STRING options passed via `-D`
spilled their trailing words into subsequent set() args.
This was latent until PR #3 introduced EXECUTORCH_VULKAN_FP16_PRECISION
with a multi-word help string — builds that set it (e.g. via
scripts/build_android_library.sh forwarding the env var) then fail.
Carried here so this branch remains self-contained and buildable
independent of the merge order of PR #2. Drops cleanly after PR #2
lands; git will treat the duplicate line as a no-op.
@jgibson2
jgibson2force-pushed the jgibson/neon-custom-kernels branch from 0c2ba68 to f4086e3CompareApril 24, 2026 18:40
@jgibson2jgibson2 changed the title kernels/custom: NEON grid_sampler_2d + sum.IntList_out for arm64optimized: add NEON grid_sampler_2d.out and Vectorized<float> sum.IntList_outApr 24, 2026
@jgibson2

Copy link
Copy Markdown
CollaboratorAuthor

Force-pushed with a restructured approach. Previous version used a separate custom_kernels static library under kernels/custom/; this version ports the same kernels to be proper optimized kernels in kernels/optimized/cpu/ — registered in optimized.yaml alongside opt_gelu, opt_add, etc.

Same runtime performance, cleaner integration:

  • kernels/custom/ directory removed
  • custom_kernels static library removed from kernels/optimized/CMakeLists.txt and configurations/CMakeLists.txt
  • build_variables.bzl + op_registration_util.bzl updated so both Buck and CMake pick up the new sources automatically
  • Symbols renamed from custom::native::* to the standard torch::executor::native::opt_* convention
  • The sum implementation swapped from handwritten NEON intrinsics to at::vec::Vectorized<float> — compiler-optimized SIMD that's actually ~2× faster than the handwritten version
  • The on-device verify test (verify_custom_kernels) dropped from this PR — with the optimized kernels in place, the existing kernels/test/op_*_test.cpp gtest infrastructure covers us

Also pushed as pytorch/executorch#19119. Once that lands and syncs into polycam/main, this PR can be closed (or merged and then dropped).

Standalone aarch64 binary that cross-checks opt_grid_sampler_2d_out and
opt_sum_dim_out against an fp32 reference derived from the portable
kernel (portable run on up-cast fp32 inputs, then down-cast to fp16).
Reference is independent of portable's own fp16 path, so the test stays
meaningful regardless of pytorch#19117's merge state.
Pass/fail uses numpy.testing.assert_allclose semantics:
|a - b| <= abs_tol + rel_tol * |b|
Avoids the "relative error explodes at zero crossings" trap for
mean-zero reductions and bilinear samples near cancellation points.
Opt-in via -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON so default builds are
unaffected. Build + run:
cmake -DEXECUTORCH_BUILD_OPTIMIZED_VERIFY=ON ...
cmake --build <out> --target verify_optimized_kernels
adb push <out>/kernels/optimized/verify_optimized_kernels /data/local/tmp/
adb shell /data/local/tmp/verify_optimized_kernels
Exits 0 on all-pass; reports max_abs / max_rel(far) / near_zero / viol
per test case. 12 test cases across grid_sampler and sum, covering the
shapes the polycam depth model uses plus a few edge cases (odd channel
count, align_corners=1, multi-batch).
The NEON fast path indexes input/grid/out directly assuming contiguous
NCHW default-dim-order layout — no use of .strides() or .dim_order().
If the caller passes anything else (NHWC, transposed, strided, channels-
last), we'd read wrong memory and silently produce garbage output.
Add the same check pattern op_sum.cpp already uses at L150-151:
tensor_is_default_dim_order + tensor_is_contiguous on input, grid, and
out. If any fails, delegate to the portable kernel (which handles
arbitrary strides / dim orders correctly via .strides()).
No perf impact on the hot path — the checks are a handful of scalar
comparisons run once per call, and the common polycam depth model case
is already default-contiguous so the fast path is still taken.
@jgibson2
jgibson2 merged commit ac4fe82 into polycamApr 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jgibson2@meredithbayne