Qualcomm AI Engine Direct - Adding LPAI Custom Op Support and Example - #22659
Qualcomm AI Engine Direct - Adding LPAI Custom Op Support and Example#22659qti-horodnic wants to merge 1 commit into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22659
Note: Links to docs will display an error until the docs builds have been completed.
|
|
@pytorchbot label "release notes: qualcomm" |
|
@claude review this pr |
|
Claude finished @psiddh's task in 3m 58s —— View job PR Review — LPAI Custom Op Support and ExampleI read the runtime C++ changes, the export/schema plumbing, Below are the observations, none of which are blocking. Correctness — looks solid
Minor suggestions
Things I checked and confirmed fine
Note for maintainersThe changed files include Overall: strong, careful work with excellent documentation of hard-won backend quirks. The suggestions above are all minor/optional. |
a12efe2 to
b22d5d1
Compare
|
Addressed Claude's comments. |
b22d5d1 to
1f0dba0
Compare
Summary
Adding an end-to-end example of a custom PyTorch operator delegated to the LPAI backend running on the aDSP in direct mode.
Note: Custom ops are currently only supported in Direct Mode, not FastRPC Mode.
Changes required to enable custom ops:
platform=HEXAGON- In direct mode the delegate is not__x86_64__or__ANDROID__, socurrent_platformstayedUNKNOWNand every op package registration was silently skipped. Added aHEXAGONplatform and anLPAItarget to the compiler spec schema, and setcurrent_platformfrom__hexagon__.registerOpPackage()- The 4th argument is backend specific. CPU/HTP take a processor target name; LPAI takes an optional target memory pool. The runtime now passesnullptrthere and lets the backend pick its default.sign_library.shand pushes them with SimpleADB's existingfiles=parameter.sign_library.shgained an--op_package_diroption to sign them. On device an LPAI op package requires direct mode (registration over FastRPC is not supported), and bothlibQnnExampleLpaiOpPackage.soandlibLpaiOpPackageIsland.socarry the kernel, so they must be rebuilt, signed and deployed together.No partitioner change is needed: op package registration happens when the
QnnManagerfor the compiler spec is created (InitBackend→BackendRegisterOpPackage), i.e. before partitioning, sovalidateOpConfig()already accepts an op package-backed node and it is validated like any other node.Note: if the op were rejected it would fall back to the CPU implementation that the example registers for eager mode, which computes the same values, so the values would still match, and the test would pass even while the op package was never exercised. The example therefore inspects the lowered program and fails if the custom op is still present as a CPU operator, or if the graph contains no delegate at all. Both tests assert on that in addition to comparing the output.
This pr also fixes 2 existing bugs found, not specific to custom ops:
QnnExecuTorchIdlWrapper's constructor could return early leavingmethod_ == nullptr, whichexecute_all()then dereferenced. The resulting DSP fault destroyed the real error message. It now reportsError::InvalidState.set_output_data_ptr()returningInvalidStatefor memory-planned outputs was treated as fatal even though it is documented as benign. Such outputs are now tracked and read back viamethod_->get_output().LPAI op package conventions that are not currently documented in the SDK, and that the example kernel encodes with comments so the next author does not have to rediscover them:
scale / 2^shiftwithshift > 31(the eNPU reportsshift=37).1u << shiftis undefined behavior for shifts >= 32 and evaluated to0here, turning the scale into+infand every requantized value intoNaN. The kernel usesldexpf().getPerTensorQuantParams()reports biased storage:code = stored - offset,stored = code + offset,value = scale * code, withoffset = -128for 8-bit. Both directions must wrap modulo the storage width.getTensorDataType()'s signedness is unusable (it reportsINT_8for unsigned tensors); only the width can be trusted.getTensorLayout()are in bytes, andlayoutOrder[0]is the slowest moving dimension (the last valid index is the fastest).getLayoutSupportFlagmust be populated in the inference build as well, or on-device registration fails withAEE_EBADSTATE (0x8000040D).Some more changes included in this pr:
examples/qualcomm/custom_op/README.md.custom_op_enablement.mdagent skill covering op packages for both HTP and LPAI.sign_library.shnow checks the signer's exit status. Previously every invocation wasyes | python $signer ...with no status check, so a failingelfsigner.pyleft the script exiting 0 and deployed stale or missing libraries.--op_package_arch(defaultv79) rather than being derived from--htp_archor--lpai_arch: on SM8850 the HTP is V81 and the LPAI hardware version is V6, while the op package builds for hexagon-V79.Constraints:
QnnLpaiOpPackage.hand the LPAI op package makefiles), and >= 2.49 for the on-device path, which additionally requires direct mode.QNN_DATATYPE_UFIXED_POINT_8/QNN_DATATYPE_UINT_8). A 16-bit activation arrives asQNN_DATATYPE_INT_32, and declaring INT_32 does pass validation and run, but only the first half of the output tensor comes back correct, reproducible with an identity requantization, which rules out the kernel's arithmetic and points at the backend's 16-bit tensor handover. That's an existing backend bug, unrelated to these changes.Test plan
Two new tests in
backends/qualcomm/tests/test_qnn_delegate.py:TestUtilsScript.test_custom_op_lpaibuilds the op package, signs it, deploys it, runs on device, and compares against the eager result.TestUtilsScript.test_custom_op_lpai_requant_edge_casescovers the two requantization paths the default run cannot reach, on the x86_64 simulator (the arithmetic is identical in both builds, so this avoids a DSP rebuild and re-sign): a small input whose code is biased into the upper half of the stored byte and has to be un-biased modulo the storage width, and an input above the calibrated range that has to saturate.Verified on device:
Passes:
my_ops.mul3.default | Truefrom the partitioner,_dom=adspin the runner's domain URI,unique: [3.]withmax abs err: 0.0, and13.533 msinqnn_executorch_execute_all.Verified the requantization edge cases on the LPAI x86_64 simulator:
Both subtests pass, each delegating the op:
calibration=1.0, inference=0.25matches eager, andcalibration=1.0, inference=2.0saturates to3.0(the graph's quantize node clamps the input to1.0, so3.0is the correct answer rather than6.0).Also exercised the host-only path directly through the simulator:
Checked that the delegation assertion actually fails when the op is not delegated, by forcing a fallback with
--skip_delegate_node_ops my_ops.mul3.default: