File size: 19,620 Bytes
d2816aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | #!/usr/bin/env python3
#
# Run llama.cpp tools on Snapdragon devices (natively, via ADB, or SSH).
#
import sys
import os
import argparse
import subprocess
import platform
import shlex
import logging
logger = logging.getLogger("run")
def parse_target(target_str):
if not target_str:
return None, None
if target_str.startswith("adb") or target_str.startswith("android"):
parts = target_str.split(":", 1)
serial = parts[1] if len(parts) > 1 else None
return "android", serial
elif target_str.startswith("lnx") or target_str.startswith("linux") or target_str.startswith("ubuntu"):
parts = target_str.split(":", 1)
host = parts[1] if len(parts) > 1 else None
return "linux", host
elif target_str in ("wos", "windows"):
return "windows", None
else:
return None, None
def shlex_join(args_list):
if hasattr(shlex, 'join'):
return shlex.join(args_list)
import pipes
return " ".join(pipes.quote(x) for x in args_list)
def main():
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Split arguments at '--'
if '--' in sys.argv:
idx = sys.argv.index('--')
run_args = sys.argv[1:idx]
cmd_args = sys.argv[idx + 1:]
else:
run_args = sys.argv[1:]
cmd_args = []
parser = argparse.ArgumentParser(
description="Unified runner for llama.cpp tools on Snapdragon (natively, via ADB, or via SSH)."
)
parser.add_argument("--target", help="Execution target (e.g. android[:serial]/adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, windows/wos) (default: local run)")
parser.add_argument("--target-dir", help="Target directory on the device (default: /data/local/tmp/llama.cpp for Android, ~/llama.cpp for Linux)")
parser.add_argument("--install-dir", help="Install directory name (defaults to pkg-TARGET or pkg-TARGET-dbg prefix based on target)")
parser.add_argument("--debug", action="store_true", help="Use debug build (defaults to pkg-TARGET-dbg folder)")
parser.add_argument("--devices", "--device", "-d", help="Select execution devices (split into NPU and OpenCL GPUs automatically, default: HTP0)")
parser.add_argument("--verbose", help="Verbose level (enables both Hexagon and OpenCL kernel cache debugging)")
parser.add_argument("--profile", help="Profiling flag (enables Hexagon profiling and OpenCL autotuning)")
parser.add_argument("--sched-debug", action="store_true", help="Enable GGML/llama.cpp scheduler debug output (GGML_SCHED_DEBUG=2)")
parser.add_argument("--mtmd-device", help="Specify the backend device ID for Multi-Threaded Multi-Device setup (MTMD_BACKEND_DEVICE)")
# Hexagon specific parameters
parser.add_argument("--hex-verbose", help="Enable verbose logging (GGML_HEXAGON_VERBOSE)")
parser.add_argument("--hex-profile", help="Enable NPU/Hexagon profiling and performance metrics print (GGML_HEXAGON_PROFILE)")
parser.add_argument("--hex-nhvx", help="Number of HVX units to use (GGML_HEXAGON_NHVX)")
parser.add_argument("--hex-nhmx", help="Number of HMX units to use. 0 disables HMX power-up (GGML_HEXAGON_NHMX)")
parser.add_argument("--hex-hostbuf", help="Enable host buffers (GGML_HEXAGON_HOSTBUF)")
parser.add_argument("--hex-opbatch", help="Maximum number of operations to batch into a single HTP execution (GGML_HEXAGON_OPBATCH)")
parser.add_argument("--hex-opqueue", help="Size of the asynchronous NPU operation queue (GGML_HEXAGON_OPQUEUE)")
parser.add_argument("--hex-oppoll", default="1", help="Enable (1) or Disable (0) polling for NPU opbatch completion (GGML_HEXAGON_OPPOLL) (default: 1)")
parser.add_argument("--hex-opfilter", help="Regex pattern to filter/select which operators are offloaded to NPU (GGML_HEXAGON_OPFILTER)")
parser.add_argument("--hex-opfusion", help="NPU graph node fusion optimization level (0: disabled, 1: enabled) (GGML_HEXAGON_OPFUSION)")
parser.add_argument("--hex-vmem", help="Maximum NPU VMEM size limit in MB to allocate (GGML_HEXAGON_VMEM)")
parser.add_argument("--hex-mbuf", help="Maximum host buffer size limit in MB to allocate (GGML_HEXAGON_MBUF)")
parser.add_argument("--hex-mm-select", help="Select MUL_MAT and MUL_MAT_ID kernel (GGML_HEXAGON_MM_SELECT) 3:HMX,2:HVX-tiled,1:HVX-flat,0:disable")
parser.add_argument("--hex-fa-select", help="Select Flash Attention kernel (GGML_HEXAGON_FA_SELECT) 2:HMX,1:HVX,0:disable")
parser.add_argument("--hex-ar-select", help="Select All-Reduce kernel (GGML_HEXAGON_AR_SELECT) 1:enable,0:disable")
parser.add_argument("--hex-etm", help="Enable Embedded Trace Macrocell hardware tracing / trace logging (GGML_HEXAGON_ETM)")
parser.add_argument("--hex-arch", help="Target Hexagon NPU architecture version override (v73, v75, v79, v81, etc.) (GGML_HEXAGON_ARCH)")
parser.add_argument("--hex-optrace", help="Trace buffer size in number of records (GGML_HEXAGON_OPTRACE)")
# OpenCL specific parameters
parser.add_argument("--cl-platform", help="Select OpenCL platform name/regex (e.g. Qualified Qualcomm OpenCL platform) (GGML_OPENCL_PLATFORM)")
parser.add_argument("--cl-device", help="Select OpenCL device name/regex (e.g. Adreno GPU) (GGML_OPENCL_DEVICE)")
parser.add_argument("--cl-opfilter", help="Regex pattern to filter/select which operators are offloaded to OpenCL (GGML_OPENCL_OPFILTER)")
parser.add_argument("--cl-disable-fusion", action="store_true", help="Disable OpenCL kernel fusion optimizations (GGML_OPENCL_DISABLE_FUSION)")
parser.add_argument("--cl-cache-dir", help="Directory path to store compiled OpenCL program binaries (GGML_OPENCL_KERNEL_CACHE_DIR)")
parser.add_argument("--cl-cache-debug", help="Enable verbose debugging logs for the kernel caching system (GGML_OPENCL_KERNEL_CACHE_DEBUG)")
parser.add_argument("--cl-fa-tune", action="store_true", help="Enable automatic Flash Attention kernel autotuning (GGML_OPENCL_FA_TUNE)")
parser.add_argument("--cl-adreno-xmem", action="store_true", help="Enforce matmul using texture/image (xmem) memory paths on Adreno GPUs (GGML_OPENCL_ADRENO_XMEM_GEMM)")
parser.add_argument("--cl-adreno-large-buffer", action="store_true", help="Allow allocating larger buffer sizes on Adreno GPUs (GGML_OPENCL_ADRENO_USE_LARGE_BUFFER)")
args = parser.parse_args(run_args)
if not cmd_args:
parser.print_help()
logger.error("\nError: No command specified after '--'")
sys.exit(1)
target_type = None
target_val = None
target_prefix = None
if args.target:
target_type, target_val = parse_target(args.target)
if not target_type:
logger.error(f"Error: Invalid target format '{args.target}'. Must be android[:serial]/adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, or windows/wos.")
sys.exit(1)
target_prefix = args.target.split(":", 1)[0]
# Resolve install directory
install_dir = args.install_dir
if not install_dir:
if target_prefix:
suffix = "-dbg" if args.debug else ""
install_dir = f"pkg-{target_prefix}{suffix}"
else:
# Smart branch folder detection for local run if default is not set
prefixes = ("wos", "windows", "lnx", "linux", "ubuntu", "adb", "android")
suffixes = ("-dbg", "") if args.debug else ("", "-dbg")
found = False
for suffix in suffixes:
for prefix in prefixes:
test_path = f"./pkg-{prefix}{suffix}/llama.cpp"
if os.path.exists(test_path):
install_dir = f"pkg-{prefix}{suffix}"
found = True
break
if found:
break
if not install_dir:
install_dir = "pkg-android" # Fallback default
# Host side package path
package_path = os.path.join(install_dir, "llama.cpp")
# Environment variables to map
env_vars = {}
def set_env(env_name, opt_val):
if opt_val is not None:
env_vars[env_name] = str(opt_val)
elif env_name in os.environ:
env_vars[env_name] = os.environ[env_name]
# Resolve and filter devices (HTP vs OpenCL)
device_in_cmd = None
for i, arg in enumerate(cmd_args):
if arg == "--device" and i + 1 < len(cmd_args):
device_in_cmd = cmd_args[i + 1]
break
elif arg.startswith("--device="):
device_in_cmd = arg.split("=", 1)[1]
break
if args.devices is not None:
devices_val = args.devices
elif device_in_cmd is not None:
devices_val = device_in_cmd
else:
devices_val = "HTP0"
if devices_val.isdigit():
hex_devices = devices_val
cl_device = ""
else:
parts = [p.strip() for p in devices_val.split(",")]
# Any device containing "htp" is Hexagon, rest is OpenCL
hex_parts = [p for p in parts if "htp" in p.lower()]
cl_parts = [
p for p in parts
if "htp" not in p.lower()
and p.lower() not in ("none", "cpu")
and not p.lower().startswith("gpuopencl")
]
hex_devices = ",".join(hex_parts)
cl_device = ",".join(cl_parts)
# Set Hexagon devices
if hex_devices:
env_vars["GGML_HEXAGON_DEVICES"] = hex_devices
elif "GGML_HEXAGON_DEVICES" in os.environ:
env_vars["GGML_HEXAGON_DEVICES"] = os.environ["GGML_HEXAGON_DEVICES"]
# Set OpenCL device (unless overridden by --cl-device)
final_cl_device = args.cl_device if args.cl_device is not None else cl_device
if final_cl_device:
env_vars["GGML_OPENCL_DEVICE"] = final_cl_device
elif "GGML_OPENCL_DEVICE" in os.environ:
env_vars["GGML_OPENCL_DEVICE"] = os.environ["GGML_OPENCL_DEVICE"]
# Map shared & backend-specific parameters with correct overrides
# Verbose logging mapping
hex_verbose_val = args.hex_verbose if args.hex_verbose is not None else args.verbose
set_env("GGML_HEXAGON_VERBOSE", hex_verbose_val)
cl_cache_debug_val = args.cl_cache_debug if args.cl_cache_debug is not None else args.verbose
set_env("GGML_OPENCL_KERNEL_CACHE_DEBUG", cl_cache_debug_val)
# Profiling mapping
hex_profile_val = args.hex_profile if args.hex_profile is not None else args.profile
set_env("GGML_HEXAGON_PROFILE", hex_profile_val)
if args.cl_fa_tune or args.profile is not None:
env_vars["GGML_OPENCL_FA_TUNE"] = "1"
elif "GGML_OPENCL_FA_TUNE" in os.environ:
env_vars["GGML_OPENCL_FA_TUNE"] = os.environ["GGML_OPENCL_FA_TUNE"]
# Other Hexagon environment variables
set_env("GGML_HEXAGON_NHVX", args.hex_nhvx)
set_env("GGML_HEXAGON_NHMX", args.hex_nhmx)
set_env("GGML_HEXAGON_HOSTBUF", args.hex_hostbuf)
set_env("GGML_HEXAGON_OPBATCH", args.hex_opbatch)
set_env("GGML_HEXAGON_OPQUEUE", args.hex_opqueue)
set_env("GGML_HEXAGON_OPPOLL", args.hex_oppoll)
set_env("GGML_HEXAGON_OPFILTER", args.hex_opfilter)
set_env("GGML_HEXAGON_OPFUSION", args.hex_opfusion)
set_env("GGML_HEXAGON_VMEM", args.hex_vmem)
set_env("GGML_HEXAGON_MBUF", args.hex_mbuf)
set_env("GGML_HEXAGON_MM_SELECT", args.hex_mm_select)
set_env("GGML_HEXAGON_FA_SELECT", args.hex_fa_select)
set_env("GGML_HEXAGON_AR_SELECT", args.hex_ar_select)
set_env("GGML_HEXAGON_ETM", args.hex_etm)
set_env("GGML_HEXAGON_ARCH", args.hex_arch)
set_env("GGML_HEXAGON_OPTRACE", args.hex_optrace)
set_env("MTMD_BACKEND_DEVICE", args.mtmd_device)
# OpenCL environment variables
set_env("GGML_OPENCL_PLATFORM", args.cl_platform)
set_env("GGML_OPENCL_OPFILTER", args.cl_opfilter)
set_env("GGML_OPENCL_KERNEL_CACHE_DIR", args.cl_cache_dir)
if args.cl_disable_fusion:
env_vars["GGML_OPENCL_DISABLE_FUSION"] = "1"
elif "GGML_OPENCL_DISABLE_FUSION" in os.environ:
env_vars["GGML_OPENCL_DISABLE_FUSION"] = os.environ["GGML_OPENCL_DISABLE_FUSION"]
if args.cl_adreno_xmem:
env_vars["GGML_OPENCL_ADRENO_XMEM_GEMM"] = "1"
elif "GGML_OPENCL_ADRENO_XMEM_GEMM" in os.environ:
env_vars["GGML_OPENCL_ADRENO_XMEM_GEMM"] = os.environ["GGML_OPENCL_ADRENO_XMEM_GEMM"]
if args.cl_adreno_large_buffer:
env_vars["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] = "1"
elif "GGML_OPENCL_ADRENO_USE_LARGE_BUFFER" in os.environ:
env_vars["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] = os.environ["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"]
if args.sched_debug:
env_vars["GGML_SCHED_DEBUG"] = "2"
# Resolve executable path
executable = cmd_args[0]
known_binaries = ["llama-cli", "llama-bench", "llama-completion", "llama-mtmd-cli", "test-backend-ops"]
if executable in known_binaries:
if target_type in ("android", "linux"):
resolved_exec = f"./bin/{executable}"
else:
if platform.system() == "Windows":
resolved_exec = os.path.normpath(os.path.join(package_path, "bin", f"{executable}.exe"))
else:
resolved_exec = os.path.normpath(os.path.join(package_path, "bin", executable))
cmd_args[0] = resolved_exec
# Infer device string to pass to the tool
basename = os.path.basename(executable)
if basename.endswith(".exe"):
basename = basename[:-4]
device_val = None
if basename == "test-backend-ops":
for i in range(len(cmd_args)):
if cmd_args[i] in ("-p", "--params") and i + 1 < len(cmd_args):
val = cmd_args[i + 1]
new_val = ""
for j, char in enumerate(val):
if char in ('[', ']'):
if j > 0 and val[j - 1] == '\\':
new_val += char
else:
new_val += '\\' + char
else:
new_val += char
cmd_args[i + 1] = new_val
has_b = any(arg == "-b" for arg in cmd_args)
if not has_b:
if args.devices:
if args.devices.isdigit():
n = int(args.devices)
device_val = ",".join(f"HTP{i}" for i in range(n))
else:
device_val = args.devices
elif "D" in os.environ:
device_val = os.environ["D"]
elif "DEVICE" in os.environ:
device_val = os.environ["DEVICE"]
else:
device_val = "HTP0"
if device_val:
cmd_args += ["-b", device_val]
else:
has_device = any(arg.startswith("--device") for arg in cmd_args)
if not has_device:
if args.devices:
if args.devices.isdigit():
n = int(args.devices)
device_val = ",".join(f"HTP{i}" for i in range(n))
else:
device_val = args.devices
elif "D" in os.environ:
device_val = os.environ["D"]
elif "DEVICE" in os.environ:
device_val = os.environ["DEVICE"]
else:
device_val = "HTP0"
if device_val:
cmd_args += ["--device", device_val]
# Automatically add -v to known llama tools if sched-debug, verbose, or profile are set
verbose_trigger = (
args.sched_debug
or args.verbose is not None
or args.profile is not None
or args.hex_verbose is not None
or args.hex_profile is not None
or args.hex_optrace is not None
)
if verbose_trigger and basename in ("llama-cli", "llama-completion", "llama-bench", "llama-server", "llama-mtmd-cli"):
if "-v" not in cmd_args and "--verbose" not in cmd_args:
cmd_args.append("-v")
# Inject defaults for llama-cli, llama-completion, and llama-server if not overridden by the user
if basename in ("llama-cli", "llama-completion", "llama-server"):
if "-ngl" not in cmd_args and "--n-gpu-layers" not in cmd_args:
cmd_args += ["-ngl", "99"]
if "-fa" not in cmd_args and "--flash-attn" not in cmd_args:
cmd_args += ["-fa", "on"]
# Use ubatch-size 1024 for hexagon backend (HTP devices)
if hex_devices and basename in ("llama-cli", "llama-completion", "llama-server", "llama-bench"):
if "--ubatch-size" not in cmd_args and "-ub" not in cmd_args:
cmd_args += ["--ubatch-size", "1024"]
elif basename in ("llama-cli", "llama-completion", "llama-server"):
if "--ubatch-size" not in cmd_args and "-ub" not in cmd_args:
cmd_args += ["--ubatch-size", "1024"]
if basename in ("llama-cli", "llama-completion", "llama-server", "llama-bench"):
if "-t" not in cmd_args and "--threads" not in cmd_args:
cmd_args += ["-t", "6"]
# Resolve target directory on device
target_dir = args.target_dir
if not target_dir:
target_dir = "/data/local/tmp/llama.cpp" if target_type == "android" else "~/llama.cpp"
if target_type == "android":
# Run via ADB
adb_base = ["adb"]
if target_val: # serial
adb_base += ["-s", target_val]
env_parts = [
"LD_LIBRARY_PATH=./lib",
"ADSP_LIBRARY_PATH=./lib"
]
for k, v in env_vars.items():
env_parts.append(f"{k}={v}")
env_str = " ".join(env_parts)
cmd_str = shlex_join(cmd_args)
adb_shell_cmd = f"cd {target_dir} && ulimit -c unlimited && {env_str} {cmd_str}"
full_cmd = adb_base + ["shell", adb_shell_cmd]
logger.info(f"+ {' '.join(full_cmd)}")
res = subprocess.run(full_cmd)
sys.exit(res.returncode)
elif target_type == "linux":
ssh_host = target_val
if not ssh_host:
logger.error("Error: SSH host not specified in target (e.g. use linux:user@host, lnx:user@host, or ubuntu:user@host). Cannot execute.")
sys.exit(1)
# Linux remote run via SSH
env_parts = [
"LD_LIBRARY_PATH=./lib",
"ADSP_LIBRARY_PATH=./lib"
]
for k, v in env_vars.items():
env_parts.append(f"{k}={v}")
env_str = " ".join(env_parts)
cmd_str = shlex_join(cmd_args)
ssh_shell_cmd = f"cd {target_dir} && ulimit -c unlimited && {env_str} {cmd_str}"
full_cmd = ["ssh", ssh_host, ssh_shell_cmd]
logger.info(f"+ {' '.join(full_cmd)}")
res = subprocess.run(full_cmd)
sys.exit(res.returncode)
elif target_type == "windows":
logger.info("Windows target execution is currently a stub.")
sys.exit(0)
else:
# Run locally
local_env = os.environ.copy()
lib_dir = os.path.normpath(os.path.join(package_path, "lib"))
local_env["ADSP_LIBRARY_PATH"] = lib_dir
if platform.system() == "Windows":
local_env["PATH"] = lib_dir + os.path.pathsep + local_env.get("PATH", "")
else:
local_env["LD_LIBRARY_PATH"] = lib_dir + os.path.pathsep + local_env.get("LD_LIBRARY_PATH", "")
for k, v in env_vars.items():
local_env[k] = v
logger.info(f"+ {shlex_join(cmd_args)}")
res = subprocess.run(cmd_args, env=local_env)
sys.exit(res.returncode)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("\nInterrupted by user.")
sys.exit(130)
|