Commit 4e7481175 for llama.cpp
commit 4e7481175cbd4759df8bee2f1c1a0073effbebd7
Author: Trivikram Reddy <127072883+trivikram-reddy1@users.noreply.github.com>
Date: Fri Sep 25 21:43:38 2026 -0500
hexagon: find software divide calls using binary inspection tool (#29449)
* hex-scripts: fix table alignment
* hex-scripts: find sw div calls using binary inspection tool
diff --git a/docs/backend/snapdragon/CMakeUserPresets.json b/docs/backend/snapdragon/CMakeUserPresets.json
index afc739232..83eba9eb2 100644
--- a/docs/backend/snapdragon/CMakeUserPresets.json
+++ b/docs/backend/snapdragon/CMakeUserPresets.json
@@ -77,6 +77,8 @@
{ "name": "arm64-android-snapdragon-debug" , "inherits": [ "base", "arm64-android-snapdragon", "debug" ] },
{ "name": "arm64-android-snapdragon-release", "inherits": [ "base", "arm64-android-snapdragon", "release" ] },
+ { "name": "arm64-android-snapdragon-relwithdebinfo", "inherits": [ "arm64-android-snapdragon-release" ],
+ "cacheVariables": { "GGML_HEXAGON_HTP_BUILD_TYPE": "RelWithDebInfo" } },
{ "name": "arm64-windows-snapdragon-debug" , "inherits": [ "base", "arm64-windows-snapdragon", "debug" ] },
{ "name": "arm64-windows-snapdragon-release", "inherits": [ "base", "arm64-windows-snapdragon", "release" ] },
diff --git a/ggml/src/ggml-hexagon/CMakeLists.txt b/ggml/src/ggml-hexagon/CMakeLists.txt
index 845b922bf..5cb178a0a 100644
--- a/ggml/src/ggml-hexagon/CMakeLists.txt
+++ b/ggml/src/ggml-hexagon/CMakeLists.txt
@@ -23,6 +23,7 @@ include(${HEXAGON_SDK_ROOT}/build/cmake/hexagon_fun.cmake)
include(ExternalProject)
option(GGML_HEXAGON_HTP_DEBUG "ggml-hexagon: enable HTP debug output" OFF)
+set(GGML_HEXAGON_HTP_BUILD_TYPE "Release" CACHE STRING "ggml-hexagon: HTP skel build type (Release, RelWithDebInfo, Debug)")
set(GGML_HEXAGON_HTP_CERT "$ENV{HEXAGON_HTP_CERT}" CACHE PATH "ggml-hexagon: enable HTP library signing using certificate")
add_library(htp_iface OBJECT
@@ -64,7 +65,7 @@ function(build_htp_skel V)
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/htp BUILD_ALWAYS ON
BUILD_BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/libggml-htp-${V}.so
CMAKE_ARGS
- -DCMAKE_BUILD_TYPE=Release
+ -DCMAKE_BUILD_TYPE=${GGML_HEXAGON_HTP_BUILD_TYPE}
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_CURRENT_SOURCE_DIR}/htp/cmake-toolchain.cmake
-DCMAKE_INSTALL_LIBDIR=${CMAKE_CURRENT_BINARY_DIR}
-DHEXAGON_SDK_ROOT=${HEXAGON_SDK_ROOT}
diff --git a/scripts/snapdragon/ggml-hexagon-inspect.py b/scripts/snapdragon/ggml-hexagon-inspect.py
index c977f6a17..7b0e2aefa 100755
--- a/scripts/snapdragon/ggml-hexagon-inspect.py
+++ b/scripts/snapdragon/ggml-hexagon-inspect.py
@@ -5,6 +5,9 @@ ggml-hexagon-inspect.py - Hexagon DSP binary inspection and diagnostic tool.
Inspects Hexagon ELF binaries (libggml-htp-v*.so) for:
- Register spills (--spills): counts scalar and HVX vector stack spills,
separating in-loop spills from frame setup/teardown.
+ - Soft-float promotions (--promotions): calls to __trunc*/__extend* helpers.
+ - Software divides (--swdiv): calls to __hexagon_udivdi3 and related
+ integer/float divide and modulo helpers, with source call sites.
- Function disassembly (--disasm <func>): annotated disassembly showing
hardware loop bounds, packet boundaries, and spill instructions.
- Crash address resolution (--addr2line <addr...>): maps hex crash offsets
@@ -18,6 +21,10 @@ Usage:
./scripts/snapdragon/ggml-hexagon-inspect.py --spills --func "^compute_"
./scripts/snapdragon/ggml-hexagon-inspect.py --spills --func "^compute_" --strict
+ # Find functions that call software divide helpers
+ ./scripts/snapdragon/ggml-hexagon-inspect.py --swdiv
+ ./scripts/snapdragon/ggml-hexagon-inspect.py --swdiv --inline --func "^op_cpy$"
+
# Disassemble a function with annotated loop and spill markers
./scripts/snapdragon/ggml-hexagon-inspect.py --disasm compute_same_shape_div_f32
@@ -108,6 +115,9 @@ class FuncStats:
self.promotions_in_loop = 0
self.promotions_total = 0
self.promotion_targets: Dict[str, int] = {}
+ self.swdivs_in_loop = 0
+ self.swdivs_total = 0
+ self.swdiv_sites: List[Tuple[int, str, bool]] = []
self.calls_in_loop = 0
self.calls_total = 0
self.loops: List[LoopStats] = []
@@ -136,6 +146,9 @@ RE_VEC_OP = re.compile(r"\b(v[0-9]+|w[0-9]+|q[0-3]|vmemu?)\b")
RE_PROMOTION_CALL = re.compile(
r"\b(?:call|jump)\s+(?:0x[0-9a-fA-F]+\s+)?<(__(?:trunc|extend)[a-zA-Z0-9_]+)(?:@plt)?>"
)
+RE_SWDIV_CALL = re.compile(
+ r"\b(?:call|jump)\s+(?:0x[0-9a-fA-F]+\s+)?<(__hexagon_(?:u?(?:div|mod)[sd]i3|div[sd]f3))(?:@plt)?>"
+)
RE_ANY_CALL = re.compile(r"\bcallr?\b")
@@ -550,6 +563,12 @@ def parse_disassembly(
stats.promotion_targets[ptarget] = stats.promotion_targets.get(ptarget, 0) + 1
if in_loop:
stats.promotions_in_loop += 1
+ swdiv_m = RE_SWDIV_CALL.search(insn)
+ if swdiv_m:
+ stats.swdivs_total += 1
+ stats.swdiv_sites.append((cur_addr, swdiv_m.group(1), in_loop))
+ if in_loop:
+ stats.swdivs_in_loop += 1
stats.insns.append(
InsnInfo(
@@ -680,6 +699,7 @@ def annotate_disasm_line(
is_event = True
prom_m = RE_PROMOTION_CALL.search(asm_chunk)
+ swdiv_m = RE_SWDIV_CALL.search(asm_chunk)
if prom_m:
ptarget = prom_m.group(1)
if in_loop:
@@ -689,6 +709,15 @@ def annotate_disasm_line(
tag = f"[PROMOTION:{ptarget}]"
tags.append(f"\033[1;35m{tag}\033[0m" if use_color else tag)
is_event = True
+ elif swdiv_m:
+ dtarget = swdiv_m.group(1)
+ if in_loop:
+ tag = f"[SW-DIV:{dtarget}:IN-LOOP]"
+ tags.append(f"\033[1;31m{tag}\033[0m" if use_color else tag)
+ else:
+ tag = f"[SW-DIV:{dtarget}]"
+ tags.append(f"\033[1;35m{tag}\033[0m" if use_color else tag)
+ is_event = True
elif RE_ANY_CALL.search(asm_chunk):
if in_loop:
tag = "[CALL:IN-LOOP]"
@@ -757,15 +786,16 @@ def run_spills(
col_stot = "S-Tot"
col_notes = "Notes"
+ name_w = max([40] + [len(f.name) for f in reported])
hdr = (
- f"{col_addr:<10} | {col_name:<40} | {col_pkts:>7} | {col_insn:>6} | "
+ f"{col_addr:<10} | {col_name:<{name_w}} | {col_pkts:>7} | {col_insn:>6} | "
f"{col_vec:>7} | {col_vloop:>14} | {col_vtot:>5} | {col_sloop:>14} | {col_stot:>5} | {col_notes}"
)
sep = "-" * len(hdr)
logger.info("\n" + sep)
logger.info(hdr)
- logger.info(sep)
+ logger.info(re.sub(r"[^|]", "-", hdr))
tot_vloop = 0
tot_sloop = 0
@@ -813,7 +843,7 @@ def run_spills(
sloop_str = f"{sloop_detail:>14}"
logger.info(
- f"0x{f.address:08x} | {f.name:<40} | {f.packet_count:>7} | {f.insn_count:>6} | "
+ f"0x{f.address:08x} | {f.name:<{name_w}} | {f.packet_count:>7} | {f.insn_count:>6} | "
f"{f.vec_insn_count:>7} | {vloop_str} | {f.vspills_total:>5} | {sloop_str} | {f.sspills_total:>5} | {notes}"
)
@@ -877,12 +907,13 @@ def run_promotions(
col_tot = "Total"
col_targets = "Promotion Targets"
- hdr = f"{col_addr:<10} | {col_name:<44} | {col_loop:>5} | {col_inloop:>7} | {col_tot:>5} | {col_targets}"
+ name_w = max([40] + [len(f.name) for f in reported])
+ hdr = f"{col_addr:<10} | {col_name:<{name_w}} | {col_loop:>5} | {col_inloop:>7} | {col_tot:>5} | {col_targets}"
sep = "-" * max(len(hdr), 110)
logger.info("\n" + sep)
logger.info(hdr)
- logger.info(sep)
+ logger.info(re.sub(r"[^|]", "-", hdr).ljust(len(sep), "-"))
tot_inloop = 0
tot_prom = 0
@@ -908,7 +939,7 @@ def run_promotions(
targets_str = ", ".join(f"{t}: {c}" for t, c in sorted(f.promotion_targets.items()))
logger.info(
- f"0x{f.address:08x} | {f.name:<44} | {f.loop_count:>5} | {inloop_str} | {f.promotions_total:>5} | {targets_str}"
+ f"0x{f.address:08x} | {f.name:<{name_w}} | {f.loop_count:>5} | {inloop_str} | {f.promotions_total:>5} | {targets_str}"
)
logger.info(sep)
@@ -939,6 +970,97 @@ def run_promotions(
return 0
+def run_swdiv(
+ toolchain: HexagonToolchain,
+ lib_path: Path,
+ args: argparse.Namespace,
+) -> int:
+ # Scan and report software divide/modulo helper calls across binary functions
+ logger.info(f"Inspecting library: {lib_path}")
+ disasm_text = toolchain.run_tool("hexagon-llvm-objdump", ["-d", str(lib_path)])
+
+ func_re = re.compile(args.func) if args.func else None
+ funcs = parse_disassembly(disasm_text, func_re)
+
+ reported = [f for f in funcs if args.all or f.swdivs_total > 0]
+
+ # Sort: in-loop divides desc, then total divides desc
+ reported.sort(key=lambda x: (x.swdivs_in_loop, x.swdivs_total), reverse=True)
+
+ use_color = not args.no_color and sys.stdout.isatty()
+
+ # Resolve call sites to source lines, falling back to function offsets without debug info
+ site_chains: Dict[int, List[str]] = {}
+ sites = [a for f in reported for a, _, _ in f.swdiv_sites]
+ if sites:
+ # With -i each address prints its inlined frames innermost first, one block per address;
+ # the last frame with a known line is the call site in the reported function itself
+ raw = toolchain.run_tool("hexagon-addr2line", ["-e", str(lib_path), "-a", "-i"] + [f"0x{a:x}" for a in sites])
+ for block in re.split(r"\n\s*\n", raw.strip()):
+ addr_line, *frames = block.strip().splitlines()
+ locs = [m for m in (re.match(r"^(.*?):(\d+)(?::\d+)?$", fr.strip()) for fr in frames) if m]
+ chain = [f"{os.path.basename(m.group(1))}:{m.group(2)}" for m in locs if m.group(1) != "??" and m.group(2) != "0"]
+ if chain:
+ site_chains[int(addr_line, 16)] = chain
+ if not site_chains:
+ logger.info("Note: no source line info in library (build with -g); showing call-site offsets instead.")
+
+ col_addr = "Address"
+ col_name = "Function"
+ col_inloop = "In-Loop"
+ col_tot = "Total"
+ col_sites = "Call Sites"
+
+ name_w = max([40] + [len(f.name) for f in reported])
+ hdr = f"{col_addr:<10} | {col_name:<{name_w}} | {col_inloop:>7} | {col_tot:>5} | {col_sites}"
+ sep = "-" * max(len(hdr), 110)
+
+ logger.info("\n" + sep)
+ logger.info(hdr)
+ logger.info(re.sub(r"[^|]", "-", hdr).ljust(len(sep), "-"))
+
+ tot_inloop = 0
+ tot_divs = 0
+ tot_funcs_with_divs = 0
+
+ for f in reported:
+ tot_inloop += f.swdivs_in_loop
+ tot_divs += f.swdivs_total
+ if f.swdivs_total > 0:
+ tot_funcs_with_divs += 1
+
+ inloop_str = f"{f.swdivs_in_loop:>7}"
+ if f.swdivs_in_loop > 0 and use_color:
+ inloop_str = f"\033[1;31m{inloop_str}\033[0m"
+
+ # Several calls can share a source line; list each location once
+ locs = [site_chains[a][-1] if a in site_chains else f"+0x{a - f.address:x}" for a, _, _ in f.swdiv_sites]
+ sites_str = ", ".join(dict.fromkeys(locs))
+ logger.info(
+ f"0x{f.address:08x} | {f.name:<{name_w}} | {inloop_str} | {f.swdivs_total:>5} | {sites_str}".rstrip()
+ )
+
+ # Per-call detail: helper and full inline chain, innermost (the divide itself) first
+ if args.inline:
+ helper_w = max(len(re.sub(r"^__hexagon_", "", h)) for _, h, _ in f.swdiv_sites)
+ for a, helper, in_loop in f.swdiv_sites:
+ chain = " <- ".join(site_chains.get(a, ["?"]))
+ loop_tag = " [IN-LOOP]" if in_loop else ""
+ if in_loop and use_color:
+ loop_tag = f"\033[1;31m{loop_tag}\033[0m"
+ off = f"+0x{a - f.address:x}"
+ logger.info(f" {off:<8} {re.sub(r'^__hexagon_', '', helper):<{helper_w}} {chain}{loop_tag}")
+
+ logger.info(sep)
+ logger.info(
+ f"Total functions analyzed: {len(funcs)} | Reported: {len(reported)} | "
+ f"Functions with sw divides: {tot_funcs_with_divs} | "
+ f"Total sw divide calls: {tot_divs} | In-loop: {tot_inloop}"
+ )
+
+ return 0
+
+
def run_disasm(
toolchain: HexagonToolchain,
lib_path: Path,
@@ -1003,19 +1125,21 @@ def run_disasm(
)
logger.info(
f"Calls: Total: {func_stats.calls_total} (in-loop: {func_stats.calls_in_loop}) | "
- f"Float promotions: {func_stats.promotions_total} (in-loop: {func_stats.promotions_in_loop})"
+ f"Float promotions: {func_stats.promotions_total} (in-loop: {func_stats.promotions_in_loop}) | "
+ f"SW divides: {func_stats.swdivs_total} (in-loop: {func_stats.swdivs_in_loop})"
)
logger.info(hdr_border)
# Print Loop Breakdown Table if function has loops
if func_stats.loops:
- logger.info(f"\n--- Loops ({len(func_stats.loops)}) " + "-" * 67)
loop_hdr = (
- f"{'#':<3} | {'Type':<5} | {'Address Range':<25} | {'Packets':>7} | "
+ f"{'#':<3} | {'Type':<5} | {'Address Range':<23} | {'Packets':>7} | "
f"{'HVX Ops':>7} | {'Vec/Pkt':>7} | {'V-Spills (st, ld)':>17} | {'S-Spills (st, ld)':>17} | Notes"
)
- logger.info(loop_hdr)
+ logger.info(f"\nLoops ({len(func_stats.loops)})")
logger.info("-" * len(loop_hdr))
+ logger.info(loop_hdr)
+ logger.info(re.sub(r"[^|]", "-", loop_hdr))
for loop in func_stats.loops:
vspill_str = f"{loop.vspills_total} ({loop.vspills_st}s,{loop.vspills_ld}l)"
sspill_str = f"{loop.sspills_total} ({loop.sspills_st}s,{loop.sspills_ld}l)"
@@ -1028,7 +1152,7 @@ def run_disasm(
notes.append("\033[1;32mdual-hvx\033[0m" if use_color else "dual-hvx")
notes_str = ", ".join(notes)
logger.info(
- f"{loop.loop_id:<3} | {loop.loop_type:<5} | 0x{loop.start_addr:08x} - 0x{loop.end_addr:08x} | "
+ f"{loop.loop_id:<3} | {loop.loop_type:<5} | {f'0x{loop.start_addr:08x} - 0x{loop.end_addr:08x}':<23} | "
f"{loop.packet_count:>7} | {loop.vec_insn_count:>7} | {loop.vec_density:>7.2f} | "
f"{vspill_str:>17} | {sspill_str:>17} | {notes_str}"
)
@@ -1061,7 +1185,7 @@ def run_disasm(
to_show[j] = True
if not any(to_show):
- logger.info(" (No spills, promotions, or in-loop calls detected in this function)\n")
+ logger.info(" (No spills, promotions, sw divides, or in-loop calls detected in this function)\n")
else:
in_gap = False
for idx, show in enumerate(to_show):
@@ -1223,6 +1347,16 @@ def main():
action="store_true",
help="Scan binary and report functions with soft-float promotion calls (__trunc*, __extend*).",
)
+ parser.add_argument(
+ "--swdiv",
+ action="store_true",
+ help="Scan binary and report functions with software divide/modulo calls (__hexagon_udivdi3, __hexagon_udivsi3, ...).",
+ )
+ parser.add_argument(
+ "--inline",
+ action="store_true",
+ help="In --swdiv, list every call under its function with the helper and its inlined source chain.",
+ )
parser.add_argument(
"--disasm",
nargs="?",
@@ -1233,7 +1367,7 @@ def main():
parser.add_argument(
"--spills-only",
action="store_true",
- help="In --disasm, only display packets containing spills, promotions, or in-loop calls, with surrounding context.",
+ help="In --disasm, only display packets containing spills, promotions, sw divides, or in-loop calls, with surrounding context.",
)
parser.add_argument(
"-C",
@@ -1261,13 +1395,13 @@ def main():
"--func",
"--fn",
"-f",
- help="Regex filter for function names in --spills, --promotions, or --disasm.",
+ help="Regex filter for function names in --spills, --promotions, --swdiv, or --disasm.",
)
parser.add_argument(
"--all",
"-a",
action="store_true",
- help="Show all functions in table, even those with 0 spills/promotions.",
+ help="Show all functions in table, even those with 0 spills/promotions/sw divides.",
)
parser.add_argument(
"--no-color",
@@ -1378,6 +1512,8 @@ def main():
sys.exit(run_disasm(toolchain, lib_path, args))
elif args.promotions:
sys.exit(run_promotions(toolchain, lib_path, args))
+ elif args.swdiv:
+ sys.exit(run_swdiv(toolchain, lib_path, args))
else:
# Default action is --spills
sys.exit(run_spills(toolchain, lib_path, args))