From 4e7481175cbd4759df8bee2f1c1a0073effbebd7 Mon Sep 17 00:00:00 2001 From: Trivikram Reddy <127072883+trivikram-reddy1@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:43:38 -0500 Subject: [PATCH] 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 --- docs/backend/snapdragon/CMakeUserPresets.json | 2 + ggml/src/ggml-hexagon/CMakeLists.txt | 3 +- scripts/snapdragon/ggml-hexagon-inspect.py | 166 ++++++++++++++++-- 3 files changed, 155 insertions(+), 16 deletions(-) diff --git a/docs/backend/snapdragon/CMakeUserPresets.json b/docs/backend/snapdragon/CMakeUserPresets.json index afc7392323..83eba9eb2d 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 845b922bfe..5cb178a0a1 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 c977f6a17a..7b0e2aefa3 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 ): annotated disassembly showing hardware loop bounds, packet boundaries, and spill instructions. - Crash address resolution (--addr2line ): 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))