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))