hexagon: support for multi-NPU devices (IQ9, IQ10) and fully asynchronous backend - #26501
Conversation
|
Hi @max-krasnyansky — I've been running this branch on an SA8797 (automotive, v81) and hit a limitation in the device→domain mapping. Patch below; it applies cleanly on The problem
The board's FastRPC runtime resolves these (queried with
The changeAsk the runtime for the id by domain name instead of computing it. Candidate names are probed in order — existing Results
I don't have an IQ9/IQ10 to check this doesn't regress there — the probe order should make it a no-op since Two other things I ran into on this board, separate from this patch, in case they're useful:
Patchdiff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp
index dadf3bd..898d36c 100644
--- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp
+++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp
@@ -13,6 +13,7 @@
#include <cstddef>
#include <stdexcept>
#include <string>
+#include <vector>
#include <sstream>
#include <iomanip>
#include <unordered_set>
@@ -77,15 +78,80 @@ struct ggml_hexagon_device_config {
static ggml_hexagon_device_config opt_device_configs[GGML_HEXAGON_MAX_SESSIONS];
-static int get_domain_id(int physical_idx) {
- return CDSP_DOMAIN_ID + physical_idx;
+// Resolve a device index to a FastRPC domain by asking the runtime.
+//
+// The names and ids are not the same on every SoC. Mobile parts expose cdsp and
+// cdsp1 at CDSP_DOMAIN_ID and CDSP_DOMAIN_ID+1, which is what the arithmetic
+// below assumes. Automotive parts do not: SA8797 has four NSPs named
+// nsp1000..nsp1003 whose effective domain ids are 1600, 1700, 1800 and 1900, so
+// both the naming and the CDSP_DOMAIN_ID+i formula stop working after the
+// second device.
+//
+// The Hexagon SDK acknowledges the split - incs/domain.h picks
+// incs/domain_auto.h over incs/domain_default.h under _AUTO - but domain_auto.h
+// is not part of the public 6.4.0.2 or 6.6.0.0 releases, so the static
+// supported_domains[] table available to us stops at CDSP1 = 4 and
+// htpdrv_get_domain() fails for anything beyond it.
+//
+// Asking FASTRPC_GET_EFFECTIVE_DOMAIN_ID for the id of a given domain *name*
+// avoids both problems and needs no table. Candidate names are probed in order
+// and the first that resolves wins, so no configuration is required; the
+// GGML_HEXAGON_DOMAIN_PREFIX override exists for SoCs that use some third
+// naming scheme.
+static bool query_domain_id(const std::string & name, int * out_id) {
+ remote_rpc_effective_domain_id_t d;
+ memset(&d, 0, sizeof(d));
+ d.domain_name = const_cast<char *>(name.c_str());
+ d.domain_name_len = name.size();
+ d.session_id = 0;
+
+ if (remote_session_control(FASTRPC_GET_EFFECTIVE_DOMAIN_ID, (void *) &d, sizeof(d)) != AEE_SUCCESS) {
+ return false;
+ }
+ if (out_id) {
+ *out_id = (int) d.effective_domain_id;
+ }
+ return true;
}
-static std::string get_domain_name(int physical_idx) {
+// Names to try for device i, most specific first.
+static std::vector<std::string> domain_name_candidates(int physical_idx) {
+ const char * prefix = getenv("GGML_HEXAGON_DOMAIN_PREFIX");
+ if (prefix && prefix[0] != '\0') {
+ return { std::string(prefix) + std::to_string(physical_idx) };
+ }
+
+ std::vector<std::string> names;
if (physical_idx == 0) {
- return CDSP_DOMAIN_NAME;
+ names.push_back(CDSP_DOMAIN_NAME);
+ } else {
+ names.push_back(std::string("cdsp") + std::to_string(physical_idx));
+ }
+ names.push_back(std::string("nsp100") + std::to_string(physical_idx));
+ return names;
+}
+
+static std::string get_domain_name(int physical_idx) {
+ const std::vector<std::string> names = domain_name_candidates(physical_idx);
+ for (const std::string & name : names) {
+ if (query_domain_id(name, nullptr)) {
+ return name;
+ }
+ }
+ return names.front();
+}
+
+static int get_domain_id(int physical_idx) {
+ int id = 0;
+ for (const std::string & name : domain_name_candidates(physical_idx)) {
+ if (query_domain_id(name, &id)) {
+ return id;
+ }
}
- return std::string("cdsp") + std::to_string(physical_idx);
+
+ // Runtimes that do not implement the query keep the legacy numbering, which
+ // is correct on the SoCs that only have cdsp and cdsp1.
+ return CDSP_DOMAIN_ID + physical_idx;
}
static int opt_arch = 0; // autodetect
@@ -2122,10 +2188,12 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) {
GGML_LOG_DEBUG("ggml-hex: %s allocating new session\n", this->name.c_str());
+ // Only used to build a fallback URI if FASTRPC_GET_URI fails below, so a
+ // domain the SDK's static table does not know about is not fatal.
domain * my_domain = htpdrv_get_domain(this->domain_id);
if (my_domain == NULL) {
- GGML_LOG_ERROR("ggml-hex: unable to get domain struct for CDSP (domain_id %d)\n", this->domain_id);
- throw std::runtime_error("ggml-hex: failed to get CDSP domain (see log for details)");
+ GGML_LOG_DEBUG("ggml-hex: no static domain entry for domain_id %d; relying on FASTRPC_GET_URI\n",
+ this->domain_id);
}
std::string dom_name = get_domain_name(phys_idx);
@@ -2168,6 +2236,12 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) {
int err = remote_session_control(FASTRPC_GET_URI, (void *) &u, sizeof(u));
if (err != AEE_SUCCESS) {
+ if (my_domain == NULL) {
+ GGML_LOG_ERROR("ggml-hex: FASTRPC_GET_URI failed for %s (error 0x%x) and there is no static entry for domain_id %d to fall back to\n",
+ dom_name.c_str(), err, this->domain_id);
+ throw std::runtime_error("ggml-hex: cannot build a session URI (see log for details)");
+ }
+
// fallback to single session uris
int htp_URI_domain_len = strlen(htp_uri) + MAX_DOMAIN_NAMELEN;AI usage disclosure: this patch and comment were written by Claude (Anthropic) working from my analysis and hardware; I reviewed both and ran all the measurements quoted above on my board. |
edfc782 to
bc43463
Compare
@CreaV sorry for the delay. Yes, we flagged this issue internally as well. The plan is to add runtime query in the next iteration. For now I updated the hardcoded domain IDs that should for your SA8797. The reason I didn't add the query thing is because that |
7426c7d to
24e22fa
Compare
|
Thanks for the update and for explaining the API availability constraint. I tested the latest head ( The new static mapping gets With the public Hexagon SDK 6.6.0.0, One configuration detail I also noticed: Thanks again for the quick turnaround and all the work on this backend. I'll be happy to test the optional runtime-query version when it is ready. |
… ALLREDUCE and things
…ut/stderr by default
…ng of cycle values
24e22fa to
6eb1a7a
Compare
|
@lhez for review/ack (I know it's huge but it got a bit stuck behind meta changes and got better and better :)) |
|
|
||
| switch (tensor->type) { | ||
| case GGML_TYPE_Q4_0: | ||
| GGML_ASSERT(offset == 0); | ||
| GGML_ASSERT(offset + size <= ggml_nbytes(tensor)); | ||
| repack_tiled_q4_0(data, tensor, size); | ||
| repack_tiled_q4_0(data, tensor, offset, size); |
There was a problem hiding this comment.
line 1278: GGML_ASSERT(offset == 0);
line 1280: repack_tiled_q4_0(data, tensor, offset, size);
under what scenario would offset != 0? is this added parameter offset intended for multi‑NPU cases?
There was a problem hiding this comment.
get_tensor_2d (used with tensor-split) uses offsets. And the set_tensor... path with host_buffer enabled does partial sets, our new shadow buffer hides that though.
Ah. Bummer. Ok. Let's fix it in the next round then. I forgot that we need more infra in there.
Yep. That's intentional. As part of testing the
Thanks |
|
@ggml-org/maintainers can I get the second approval. |
taronaeo
left a comment
There was a problem hiding this comment.
Providing another approval :)
Oops. My bad. I noticed the failure but at first glance it looks like one of those intermittent things (ie runner failed to call QDC API). But now I see that it's my script changes that broke it. Will fix asap. Thanks for the ping. |
|
Thanks for putting this together, @max-krasnyansky. Based on our root-cause analysis from the K3-K5 campaign, here is how our findings align with what this PR actually fixes regarding our current bottleneck:1. Where PR #26501 helps (Actionable & Validated)lm_head Offloading (Real Impact): Moving output.weight to HTP (via Q8_0 GET_ROWS support) is the single biggest win here. In our profiling, this single-handedly dropped CPU usage from ~974% down to ~10% on SA8797 by eliminating heavy CPU-side operations in the decode graph.Fully Async Backend (Moderate Impact): Making graph_compute, events, and tensor copies non-blocking is a great architectural improvement. While it doesn't reduce the orchestration overhead itself, it successfully allows overlapping DSP execution with AP-side work (~10-20% expected gain).2. The Remaining Bottleneck (Why we still need more)Despite these fixes, our profiling shows we are still bounded by the GGML scheduler generating ~730 micro-graphs per token. This leads to a massive RPC overhead ratio:$T_{\text{DSP}} \approx 66\text{ms}$ (stable, HTP hardware is fast)$T_{\text{AP/RPC}} \approx 65\text{ms}$ (orchestration overhead per split)Because interleaved CPU ops (like rms_norm and samplers) force the scheduler to split the graph at every CPU |
This comment was marked as spam.
This comment was marked as spam.
…nous backend (ggml-org#26501) * hexagon: use non-host bufs by default and make the backend fully async * hex-hb: remove optional hostbuf support and fix async copy * hex-unary: relax supported unary check * hex-bufs: use same get_alignment for host bufs * snapdragon: bump android_platform to 34 * hex-rows: super hacky get/set rows for q8_0 * hex-get-rows: fix q8_0 * hex-get-rows: supprot for f16 and cleanup for q8_0 * hex-get-rows: generic macros and specialized thread funcs * hex-get-rows: add DMA pipeline, vtcm_layout and kernel params * hex-set-rows: fix q8_0 support, add dma and tracing * hex-tests: override nmse threshold for HTP of Q8_0 quants * hex-fa: add support for Q8_0 with inplace dequantizers * hex-get-rows: simplify type dispatch * hex-rows: simplify GET/SET_ROWS DMA pipeline * hex-async: add events, set/get-tensor-async and rest of the async api support * hex-repack: use slice instead of expert in repack functions * hex-cpy: update event/async-cpy logging * hex-set-rows: optimize smaller tensors * hex-geglu: fix perf regression with larger tensors * hex-get-rows: add missing header * hex-set-rows: add missing header * hex-bufs: ressurect GGML_HEXAGON_HOSTBUF but disable it by default * hexagon: do not reject ops with non-heaxon buffers * hex-get-rows: apply >=32 restriction only for q8_0 * hex-res: bump vtcm acquire timeout to 10 seconds * hex-bufs: add support for cloning buffers between sessions to speed up tensor copies * hex-async: rework event recording and batch flushing and integrate with meta backend * hex-bufs: improved handling of repacked tensors * hex-repack: handle get_tensor_2d offsets * hex-dev: add support for devices with multiple NPUs * hex-sync: add support for sync tokens to synchronize npu devices for async splits * hex-mmap: cleanup mmap calls and add a retry for robustness * hex-sync: add failsafe if sync wait gets stuck * hex-sync: use sync_seq to check for completed events * hex-sync: rotate tokens for extra robustness * hex-devs: add supprot for legacy device names for now * hex-bufs: add support for auto-cloning buffers from diff sessions * hex-fusion: simplify and optimize htp-opnode fusion handling * hex-sync: override opnode name so that it shows up in the profiles * hex-trace: update scripts to handle multiple devices * hex-sync: bump the size of the opbatch queue and number of sync tokens * hex-cpy-sync: do not explicitly flush opbatches in cpy_tensor_async and add support for cpy-dma * hex-sync: add graph-flush threshold to avoid single op batches * hex-sync: add sync_peer so that we can flush peers we depend on during cross-device ops * hex-bufs: introduce tensor->extra and shadow_bufs for repacking * hex-l2: flush tiny tensors inline * hex-sync: use explicit l2flush for sync tokens * hex-extra: track weight flags via tensor extra * hex-fence: rename sync to fence * hex-repack: proper handling of set-tensor-2d in the shadow_buf * hex-trace: remove obsolete opstage mask that we used for profiling * hex-env: remove obsolete use_hmx variable * hexagon: new unified run.py and build.py and updated docs * snapdragon: update run script to auto-escapt test-backend-op -p argument * hex-scripts: fix trailing spaces * hex-scripts: fix flake8 warnings * snapdragon: cleanup dst lib/bin dirs before copying new build * hex-ops: add support for allreduce * hex-ar: improved allreduce with dma pipeline * hex-ar: align macros * hex-ar: consistent use of fence_seq * hex-ar: add AR_SELECT env var to select ALLREDUCE kernel or fallback * hex-ar: add proper synchronize handling for ALLREDUCE * hex-opbatch: looks like we now just rely on backend.synchronise to flush the batches, no need to flush them by threshold * hex-ar: bump block size to improve dma efficiency * hex-ar: fused ALLREDUCE+ADD * hex-ar: cleaner fence buffer management * hex-ar: futher allreduce tweaking to remove race conditions * hex-ar: add simple solver and remove non-dma kernels * hex-ar: add row-broadcast to fuse with bias ADD * hex-fence: pass seq numbers via op_params * hex-ar: allow for both entry/exit seq for completing entry wait * hex-ar: align macros * hex-ar: do not refetch broadcast row * hex-fusion: move all fusion into opbatch::add_op for consistency with ALLREDUCE and things * hex-fusion: fix incorrect MUL_MAT reordering * hex-mm: make fused 2x and 3x matmuls more generic * hex-fusion: move tensor fusion tagging to graph_compute * hexagon: make sure to copy tensor->extra by value * hex-get-rows: fix offset calc with row-chunking * hex-repack: get_tensor_2d fixes for non-zero offsets * snapdragon: make profile/trace scripts more robust and donot mix stdout/stderr by default * hex-devices: use legacy device nameing by default to ease the transition * hex-devices: hardcode CDSP domain IDs for current devices for now * hex-optrace: improve multi-NPU timestamp alignment and overall handling of cycle values * hex-optrace: more robust handling of the fence events
Overview
Kind of a big overhaul of the Hexagon backend to enable support for mulit-NPU devices, and to make it fully asynchronous: async graph-compute, async events, async tensor-copy, cross-device fences -- the whole enchilada!
The PR also includes everything required to properly support and enable multi-NPU tensor-split mode (ie integration with the meta backend) on devices like the IQ9 and IQ10 series.
Tested on the IQ9 EVK.
See examples below.
The asynchronous implementation ended up requiring many other updates besides just making
graph_computeandcpy_tensor_asyncnot block. To make it fully usable we need to use non-host buffers by default (ie to force async copies and repack), which in turn requires support for more types inSET/GET_ROWS, etc. The CPY op needed to be optimized to not slow things down, and to support fused FENCE op to allow for truly async copies.Hence this PR includes all those updates.
The multi-NPU support requires new device naming scheme. The original naming
HTP0,...HTP4is still supported and is enabled if the environment variableGGML_HEXAGON_NDEVis defined. The new scheme isHTP0:0, HTP1:0where the first number is physical NPU id and the second number is the virtual id of the session.To simplify the migration and support for the devices I included new
./scripts/snapdragon/build.pyand./scripts/snapdragon/run.pyand update the documenation to cover the new scripts, devices, etc.Additional information
Here is an example gemma-4-E4B (q4_0 QAT model) on IQ9 EVK in tensor split mode
Requirements