mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-27 00:17:27 -05:00
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:
@@ -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" ] },
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user