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
This commit is contained in:
Trivikram Reddy
2026-09-25 19:43:38 -07:00
committed by GitHub
parent 171e8846b4
commit 4e7481175c
3 changed files with 155 additions and 16 deletions
@@ -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" ] },
+2 -1
View File
@@ -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}
+151 -15
View File
@@ -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))