chore: import upstream snapshot with attribution
Copilot Setup Steps / copilot-setup-steps (push) Failing after 2s
Python check requirements.txt / check-requirements (push) Has been cancelled
Python Type-Check / python type-check (push) Has been cancelled
Update Operations Documentation / update-ops-docs (push) Has been cancelled
Check Pre-Tokenizer Hashes / pre-tokenizer-hashes (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Failing after 2s
Python check requirements.txt / check-requirements (push) Has been cancelled
Python Type-Check / python type-check (push) Has been cancelled
Update Operations Documentation / update-ops-docs (push) Has been cancelled
Check Pre-Tokenizer Hashes / pre-tokenizer-hashes (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
0xffff
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
|
||||
# Basedir on device
|
||||
basedir=/data/local/tmp/llama.cpp
|
||||
|
||||
branch=.
|
||||
[ "$B" != "" ] && branch=$B
|
||||
|
||||
adbserial=
|
||||
[ "$S" != "" ] && adbserial="-s $S"
|
||||
|
||||
adbhost=
|
||||
[ "$H" != "" ] && adbhost="-H $H"
|
||||
|
||||
model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
[ "$M" != "" ] && model="$M"
|
||||
|
||||
device="HTP0"
|
||||
[ "$D" != "" ] && device="$D"
|
||||
|
||||
verbose=
|
||||
[ "$V" != "" ] && verbose="GGML_HEXAGON_VERBOSE=$V" cli_opts="$cli_opts -v"
|
||||
|
||||
profile=
|
||||
[ "$PROF" != "" ] && profile="GGML_HEXAGON_PROFILE=$PROF" cli_opts="$cli_opts -v"
|
||||
|
||||
opmask=
|
||||
[ "$OPSTAGE" != "" ] && opmask="GGML_HEXAGON_OPSTAGE=$OPSTAGE"
|
||||
|
||||
nhvx=
|
||||
[ "$NHVX" != "" ] && nhvx="GGML_HEXAGON_NHVX=$NHVX"
|
||||
|
||||
ndev=
|
||||
[ "$NDEV" != "" ] && ndev="GGML_HEXAGON_NDEV=$NDEV"
|
||||
|
||||
hb=
|
||||
[ "$HB" != "" ] && hb="GGML_HEXAGON_HOSTBUF=$HB"
|
||||
|
||||
set -x
|
||||
|
||||
adb $adbserial $adbhost shell " \
|
||||
cd $basedir; \
|
||||
LD_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
ADSP_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
$ndev $nhvx $opmask $verbose $profile $hb ./$branch/bin/llama-bench --device $device --mmap 0 -m $basedir/../gguf/$model \
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 \
|
||||
--ubatch-size 1024 -fa 1 -ngl 99 $cli_opts $@ \
|
||||
"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
|
||||
# Basedir on device
|
||||
basedir=/data/local/tmp/llama.cpp
|
||||
|
||||
cli_opts=
|
||||
|
||||
branch=.
|
||||
[ "$B" != "" ] && branch=$B
|
||||
|
||||
adbserial=
|
||||
[ "$S" != "" ] && adbserial="-s $S"
|
||||
|
||||
adbhost=
|
||||
[ "$H" != "" ] && adbhost="-H $H"
|
||||
|
||||
model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
[ "$M" != "" ] && model="$M"
|
||||
|
||||
device="HTP0"
|
||||
[ "$D" != "" ] && device="$D"
|
||||
|
||||
verbose=
|
||||
[ "$V" != "" ] && verbose="GGML_HEXAGON_VERBOSE=$V" cli_opts="$cli_opts -v"
|
||||
|
||||
sched=
|
||||
[ "$SCHED" != "" ] && sched="GGML_SCHED_DEBUG=2" cli_opts="$cli_opts -v"
|
||||
|
||||
profile=
|
||||
[ "$PROF" != "" ] && profile="GGML_HEXAGON_PROFILE=$PROF" cli_opts="$cli_opts -v"
|
||||
|
||||
opmask=
|
||||
[ "$OPSTAGE" != "" ] && opmask="GGML_HEXAGON_OPSTAGE=$OPSTAGE"
|
||||
|
||||
nhvx=
|
||||
[ "$NHVX" != "" ] && nhvx="GGML_HEXAGON_NHVX=$NHVX"
|
||||
|
||||
hmx=
|
||||
[ "$HMX" != "" ] && hmx="GGML_HEXAGON_USE_HMX=$HMX"
|
||||
|
||||
ndev=
|
||||
[ "$NDEV" != "" ] && ndev="GGML_HEXAGON_NDEV=$NDEV"
|
||||
|
||||
hb=
|
||||
[ "$HB" != "" ] && hb="GGML_HEXAGON_HOSTBUF=$HB"
|
||||
|
||||
opbatch=
|
||||
[ "$OB" != "" ] && opbatch="GGML_HEXAGON_OPBATCH=$OB"
|
||||
|
||||
opqueue=
|
||||
[ "$OQ" != "" ] && opqueue="GGML_HEXAGON_OPQUEUE=$OQ"
|
||||
|
||||
opflt=
|
||||
[ "$OF" != "" ] && opflt="GGML_HEXAGON_OPFILTER=$OF"
|
||||
|
||||
vmem=
|
||||
[ "$VM" != "" ] && opflt="GGML_HEXAGON_VMEM=$VM"
|
||||
|
||||
mbuf=
|
||||
[ "$MB" != "" ] && opflt="GGML_HEXAGON_MBUF=$MB"
|
||||
vmem=
|
||||
[ "$VM" != "" ] && vmem="GGML_HEXAGON_VMEM=$VM"
|
||||
|
||||
mbuf=
|
||||
[ "$MB" != "" ] && mbuf="GGML_HEXAGON_MBUF=$MB"
|
||||
set -x
|
||||
|
||||
adb $adbserial $adbhost shell " \
|
||||
cd $basedir; ulimit -c unlimited; \
|
||||
LD_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
ADSP_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
$verbose $sched $opmask $profile $nhvx $hmx $ndev $hb $opbatch $opqueue $opflt $vmem $mbuf \
|
||||
./$branch/bin/llama-cli --no-mmap -m $basedir/../gguf/$model \
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 \
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on \
|
||||
-ngl 99 --device $device $cli_opts $@ \
|
||||
"
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
|
||||
# Basedir on device
|
||||
basedir=/data/local/tmp/llama.cpp
|
||||
|
||||
cli_opts=
|
||||
|
||||
branch=.
|
||||
[ "$B" != "" ] && branch=$B
|
||||
|
||||
adbserial=
|
||||
[ "$S" != "" ] && adbserial="-s $S"
|
||||
|
||||
adbhost=
|
||||
[ "$H" != "" ] && adbhost="-H $H"
|
||||
|
||||
model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
[ "$M" != "" ] && model="$M"
|
||||
|
||||
device="HTP0"
|
||||
[ "$D" != "" ] && device="$D"
|
||||
|
||||
verbose=
|
||||
[ "$V" != "" ] && verbose="GGML_HEXAGON_VERBOSE=$V" cli_opts="$cli_opts -v"
|
||||
|
||||
sched=
|
||||
[ "$SCHED" != "" ] && sched="GGML_SCHED_DEBUG=2" cli_opts="$cli_opts -v"
|
||||
|
||||
profile=
|
||||
[ "$PROF" != "" ] && profile="GGML_HEXAGON_PROFILE=$PROF" cli_opts="$cli_opts -v"
|
||||
|
||||
opmask=
|
||||
[ "$OPSTAGE" != "" ] && opmask="GGML_HEXAGON_OPSTAGE=$OPSTAGE"
|
||||
|
||||
nhvx=
|
||||
[ "$NHVX" != "" ] && nhvx="GGML_HEXAGON_NHVX=$NHVX"
|
||||
|
||||
hmx=
|
||||
[ "$HMX" != "" ] && hmx="GGML_HEXAGON_USE_HMX=$HMX"
|
||||
|
||||
ndev=
|
||||
[ "$NDEV" != "" ] && ndev="GGML_HEXAGON_NDEV=$NDEV"
|
||||
|
||||
hb=
|
||||
[ "$HB" != "" ] && hb="GGML_HEXAGON_HOSTBUF=$HB"
|
||||
|
||||
opbatch=
|
||||
[ "$OB" != "" ] && opbatch="GGML_HEXAGON_OPBATCH=$OB"
|
||||
|
||||
opqueue=
|
||||
[ "$OQ" != "" ] && opqueue="GGML_HEXAGON_OPQUEUE=$OQ"
|
||||
|
||||
oppoll=
|
||||
[ "$OP" != "" ] && oppoll="GGML_HEXAGON_OPPOLL=$OP"
|
||||
|
||||
opflt=
|
||||
[ "$OF" != "" ] && opflt="GGML_HEXAGON_OPFILTER=$OF"
|
||||
|
||||
opfuse=
|
||||
[ "$OC" != "" ] && opfuse="GGML_HEXAGON_OPFUSION=$OC"
|
||||
|
||||
vmem=
|
||||
[ "$VM" != "" ] && vmem="GGML_HEXAGON_VMEM=$VM"
|
||||
|
||||
mbuf=
|
||||
[ "$MB" != "" ] && mbuf="GGML_HEXAGON_MBUF=$MB"
|
||||
|
||||
mmsel=
|
||||
[ "$MM" != "" ] && mmsel="GGML_HEXAGON_MM_SELECT=$MM"
|
||||
|
||||
fasel=
|
||||
[ "$FA" != "" ] && fasel="GGML_HEXAGON_FA_SELECT=$FA"
|
||||
|
||||
set -x
|
||||
|
||||
adb $adbserial $adbhost shell " \
|
||||
cd $basedir; ulimit -c unlimited; \
|
||||
LD_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
ADSP_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
$verbose $sched $opmask $profile $nhvx $hmx $ndev $hb $opbatch $opqueue $oppoll $opflt $opfuse $vmem $mbuf $mmsel $fasel \
|
||||
./$branch/bin/llama-completion --no-mmap -m $basedir/../gguf/$model \
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 \
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on \
|
||||
-ngl 99 --device $device $cli_opts $@ \
|
||||
"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
|
||||
# Basedir on device
|
||||
basedir=/data/local/tmp/llama.cpp
|
||||
|
||||
cli_opts=
|
||||
|
||||
branch=.
|
||||
[ "$B" != "" ] && branch=$B
|
||||
|
||||
adbserial=
|
||||
[ "$S" != "" ] && adbserial="-s $S"
|
||||
|
||||
adbhost=
|
||||
[ "$H" != "" ] && adbhost="-H $H"
|
||||
|
||||
model="gemma-3-4b-it-Q4_0.gguf"
|
||||
[ "$M" != "" ] && model="$M"
|
||||
|
||||
mmproj="mmproj-F16.gguf"
|
||||
[ "$MMPROJ" != "" ] && mmproj="$MMPROJ"
|
||||
|
||||
image=
|
||||
[ "$IMG" != "" ] && image="$IMG"
|
||||
|
||||
device="HTP0"
|
||||
[ "$D" != "" ] && device="$D"
|
||||
|
||||
verbose=
|
||||
[ "$V" != "" ] && verbose="GGML_HEXAGON_VERBOSE=$V"
|
||||
|
||||
experimental="GGML_HEXAGON_EXPERIMENTAL=1"
|
||||
[ "$E" != "" ] && experimental="GGML_HEXAGON_EXPERIMENTAL=$E"
|
||||
|
||||
sched=
|
||||
[ "$SCHED" != "" ] && sched="GGML_SCHED_DEBUG=2" cli_opts="$cli_opts -v"
|
||||
|
||||
profile=
|
||||
[ "$PROF" != "" ] && profile="GGML_HEXAGON_PROFILE=$PROF"
|
||||
|
||||
opmask=
|
||||
[ "$OPSTAGE" != "" ] && opmask="GGML_HEXAGON_OPSTAGE=$OPSTAGE"
|
||||
|
||||
nhvx=
|
||||
[ "$NHVX" != "" ] && nhvx="GGML_HEXAGON_NHVX=$NHVX"
|
||||
|
||||
hmx=
|
||||
[ "$HMX" != "" ] && hmx="GGML_HEXAGON_USE_HMX=$HMX"
|
||||
|
||||
ndev=
|
||||
[ "$NDEV" != "" ] && ndev="GGML_HEXAGON_NDEV=$NDEV"
|
||||
|
||||
# MTMD backend device for vision model (defaults to CPU if not set)
|
||||
mtmd_backend=
|
||||
[ "$MTMD_DEVICE" != "" ] && mtmd_backend="MTMD_BACKEND_DEVICE=$MTMD_DEVICE"
|
||||
|
||||
set -x
|
||||
|
||||
adb $adbserial $adbhost shell " \
|
||||
cd $basedir; ulimit -c unlimited; \
|
||||
LD_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
ADSP_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
$verbose $experimental $sched $opmask $profile $hmx $nhvx $ndev $mtmd_backend \
|
||||
./$branch/bin/llama-mtmd-cli --no-mmap -m $basedir/../gguf/$model \
|
||||
--mmproj $basedir/../gguf/$mmproj \
|
||||
--image $basedir/../gguf/$image \
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 \
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on \
|
||||
-ngl 99 --device $device -v $cli_opts $@ \
|
||||
"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
|
||||
# Basedir on device
|
||||
basedir=/data/local/tmp/llama.cpp
|
||||
|
||||
cli_opts=
|
||||
|
||||
branch=.
|
||||
[ "$B" != "" ] && branch=$B
|
||||
|
||||
adbserial=
|
||||
[ "$S" != "" ] && adbserial="-s $S"
|
||||
|
||||
adbhost=
|
||||
[ "$H" != "" ] && adbhost="-H $H"
|
||||
|
||||
device="HTP0"
|
||||
[ "$D" != "" ] && device="$D"
|
||||
|
||||
verbose=
|
||||
[ "$V" != "" ] && verbose="GGML_HEXAGON_VERBOSE=$V"
|
||||
|
||||
sched=
|
||||
[ "$SCHED" != "" ] && sched="GGML_SCHED_DEBUG=2" cli_opts="$cli_opts -v"
|
||||
|
||||
profile=
|
||||
[ "$PROF" != "" ] && profile="GGML_HEXAGON_PROFILE=$PROF"
|
||||
|
||||
opmask=
|
||||
[ "$OPSTAGE" != "" ] && opmask="GGML_HEXAGON_OPSTAGE=$OPSTAGE"
|
||||
|
||||
nhvx=
|
||||
[ "$NHVX" != "" ] && nhvx="GGML_HEXAGON_NHVX=$NHVX"
|
||||
|
||||
hmx=
|
||||
[ "$HMX" != "" ] && hmx="GGML_HEXAGON_USE_HMX=$HMX"
|
||||
|
||||
ndev=
|
||||
[ "$NDEV" != "" ] && ndev="GGML_HEXAGON_NDEV=$NDEV"
|
||||
|
||||
hb=
|
||||
[ "$HB" != "" ] && hb="GGML_HEXAGON_HOSTBUF=$HB"
|
||||
|
||||
opbatch=
|
||||
[ "$OB" != "" ] && opbatch="GGML_HEXAGON_OPBATCH=$OB"
|
||||
|
||||
opqueue=
|
||||
[ "$OQ" != "" ] && opqueue="GGML_HEXAGON_OPQUEUE=$OQ"
|
||||
|
||||
oppoll=
|
||||
[ "$OP" != "" ] && oppoll="GGML_HEXAGON_OPPOLL=$OP"
|
||||
|
||||
opfuse=
|
||||
[ "$OC" != "" ] && opfuse="GGML_HEXAGON_OPFUSION=$OC"
|
||||
|
||||
mmsel=
|
||||
[ "$MM" != "" ] && mmsel="GGML_HEXAGON_MM_SELECT=$MM"
|
||||
|
||||
fasel=
|
||||
[ "$FA" != "" ] && fasel="GGML_HEXAGON_FA_SELECT=$FA"
|
||||
|
||||
set -x
|
||||
|
||||
tool=$1; shift
|
||||
|
||||
adb $adbserial $adbhost shell " \
|
||||
cd $basedir; ulimit -c unlimited; \
|
||||
LD_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
ADSP_LIBRARY_PATH=$basedir/$branch/lib \
|
||||
$verbose $sched $opmask $profile $nhvx $hmx $ndev $hb $opbatch $opqueue $oppoll $opfuse $mmsel $fasel ./$branch/bin/$tool $@ \
|
||||
"
|
||||
Executable
+475
@@ -0,0 +1,475 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
import statistics
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
# Mapping of cli-friendly names to (internal_data_key, Display Header, numeric_sort_key)
|
||||
COL_MAP = {
|
||||
"tot-usec": ("tot_usec", "Tot usec", "_sort_tot_usec"),
|
||||
"op": ("op", "Op", "op"),
|
||||
"dims": ("dims", "Dims", "dims"),
|
||||
"dtypes": ("dtypes", "DTypes", "dtypes"),
|
||||
"count": ("count", "Count", "_sort_count"),
|
||||
"max-usec": ("max_usec", "Max usec", "_sort_max_usec"),
|
||||
"avg-usec": ("avg_usec", "Avg usec", "_sort_avg_usec"),
|
||||
"max-cycles": ("max_cycles", "Max Cycles", "_sort_max_cycles"),
|
||||
"avg-cycles": ("avg_cycles", "Avg Cycles", "_sort_avg_cycles"),
|
||||
"max-pmu": ("max_pmu", "Max PMU", "_sort_max_pmu"),
|
||||
"avg-pmu": ("avg_pmu", "Avg PMU", "_sort_avg_pmu"),
|
||||
}
|
||||
|
||||
op_pattern = re.compile(
|
||||
r"profile-op\s+(?P<op_name>[A-Z_0-9+]+):\s+.*?\s+:\s+(?P<dims>[\d:x\s\->!]+)\s+:\s+(?P<types>[a-z\d_\s\->x]+)\s+:\s+.*?\s+:\s+(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?"
|
||||
)
|
||||
|
||||
trace_pattern = re.compile(
|
||||
r"trace-op\s+(?P<op_name>[A-Z_0-9+]+):\s+thread\s+(?P<thread>\d+)\s+event\s+(?P<event>[A-Z_0-9\-]+)\s+info\s+(?P<info>\d+)\s+(?P<state>start|stop)\s+(?P<cycles>\d+)"
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ggml-hexagon-profile")
|
||||
|
||||
|
||||
def normalize_event_name(evt_type):
|
||||
if evt_type == "HVX_COMP":
|
||||
return "V-COMP"
|
||||
if evt_type == "HMX_COMP":
|
||||
return "M-COMP"
|
||||
|
||||
# Strip HVX_ or HMX_ prefixes
|
||||
name = evt_type
|
||||
if name.startswith("HVX_") or name.startswith("HMX_"):
|
||||
name = name[4:]
|
||||
return name.replace("_", "-")
|
||||
|
||||
|
||||
class CycleUnwrapper:
|
||||
def __init__(self):
|
||||
self.last_raw = None
|
||||
self.high_part = 0
|
||||
|
||||
def unwrap(self, raw):
|
||||
if self.last_raw is None:
|
||||
self.last_raw = raw
|
||||
return raw
|
||||
diff = raw - self.last_raw
|
||||
if diff < -0x80000000:
|
||||
self.high_part += 0x100000000
|
||||
elif diff > 0x80000000:
|
||||
self.high_part -= 0x100000000
|
||||
self.last_raw = raw
|
||||
return raw + self.high_part
|
||||
|
||||
|
||||
def parse_log(file_path, pmu_index=None):
|
||||
try:
|
||||
if file_path != "-":
|
||||
f = open(file_path, 'r', encoding='utf-8', errors='ignore')
|
||||
else:
|
||||
f = os.fdopen(0, 'r', encoding='utf-8', errors='ignore')
|
||||
except FileNotFoundError:
|
||||
logger.error(f"file '{file_path}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
all_ops: List[Dict[str, Any]] = []
|
||||
current_op: Optional[Dict[str, Any]] = None
|
||||
|
||||
timestamp_pattern = re.compile(r"^(?P<min>\d+)\.(?P<sec>\d+)\.(?P<ms>\d+)\.(?P<us>\d+)\s+[A-Z]\s+")
|
||||
unwrapper = CycleUnwrapper()
|
||||
|
||||
for line in f:
|
||||
ts_match = timestamp_pattern.match(line)
|
||||
abs_usec = 0
|
||||
if ts_match:
|
||||
abs_usec = (
|
||||
(int(ts_match.group('min')) * 60 + int(ts_match.group('sec'))) * 1000000
|
||||
+ int(ts_match.group('ms')) * 1000
|
||||
+ int(ts_match.group('us'))
|
||||
)
|
||||
|
||||
if "|" in line and "profile-op" in line:
|
||||
parts = [p.strip() for p in line.split("|")]
|
||||
prefix = parts[0]
|
||||
prefix_match = re.search(r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)", prefix)
|
||||
if not prefix_match:
|
||||
continue
|
||||
|
||||
if len(parts) == 7:
|
||||
dims, types, timings = parts[2], parts[3], parts[6]
|
||||
elif len(parts) == 6:
|
||||
dims, types, timings = parts[2], parts[3], parts[5]
|
||||
else:
|
||||
continue
|
||||
|
||||
timing_match = re.search(
|
||||
r"(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?",
|
||||
timings
|
||||
)
|
||||
if not timing_match:
|
||||
continue
|
||||
|
||||
op_match = timing_match
|
||||
op_name = prefix_match.group("op_name")
|
||||
else:
|
||||
op_match = op_pattern.search(line)
|
||||
if op_match:
|
||||
op_name = op_match.group('op_name')
|
||||
dims = op_match.group('dims').strip()
|
||||
types = op_match.group('types').strip()
|
||||
else:
|
||||
op_match = None
|
||||
|
||||
if op_match:
|
||||
pmu_raw = op_match.group('pmu') if 'pmu' in op_match.groupdict() else None
|
||||
pmu_val = None
|
||||
if pmu_raw and pmu_index is not None:
|
||||
try:
|
||||
pmu_list = [int(x.strip()) for x in pmu_raw.split(',')]
|
||||
if len(pmu_list) > pmu_index:
|
||||
pmu_val = pmu_list[pmu_index]
|
||||
except (ValueError, IndexError):
|
||||
pmu_val = None
|
||||
|
||||
evt_raw = op_match.group('evt') if 'evt' in op_match.groupdict() else None
|
||||
evt_val = None
|
||||
if evt_raw:
|
||||
try:
|
||||
evt_val = [int(x.strip()) for x in evt_raw.split(',')]
|
||||
except ValueError:
|
||||
evt_val = None
|
||||
|
||||
cycles_start_raw = op_match.group('start')
|
||||
unwrapped_cycles_start = None
|
||||
if cycles_start_raw:
|
||||
unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw))
|
||||
|
||||
idx = line.find("profile-op ")
|
||||
op_text = line[idx + 11:].strip() if idx != -1 else line.strip()
|
||||
|
||||
current_op = {
|
||||
'name': op_name,
|
||||
'dims': dims,
|
||||
'types': types,
|
||||
'op_text': op_text,
|
||||
'usec': int(op_match.group('usec')),
|
||||
'cycles': int(op_match.group('cycles')),
|
||||
'cycles_start': int(cycles_start_raw) if cycles_start_raw else None,
|
||||
'unwrapped_cycles_start': unwrapped_cycles_start,
|
||||
'pmu_val': pmu_val,
|
||||
'evt_val': evt_val,
|
||||
'abs_usec': abs_usec,
|
||||
'trace_events': []
|
||||
}
|
||||
all_ops.append(current_op)
|
||||
continue
|
||||
|
||||
trace_match = trace_pattern.search(line)
|
||||
if trace_match and current_op:
|
||||
if trace_match.group('op_name') == current_op['name']:
|
||||
raw_cyc = int(trace_match.group('cycles'))
|
||||
current_op['trace_events'].append({
|
||||
'thread': int(trace_match.group('thread')),
|
||||
'event': trace_match.group('event'),
|
||||
'info': int(trace_match.group('info')),
|
||||
'cycles': raw_cyc,
|
||||
'unwrapped_cycles': unwrapper.unwrap(raw_cyc),
|
||||
'state': trace_match.group('state')
|
||||
})
|
||||
|
||||
f.close()
|
||||
return all_ops
|
||||
|
||||
|
||||
def print_ascii_timeline(op_name, dims, types, usec, cycles, events, evt_val=None):
|
||||
evt_str = ""
|
||||
if evt_val:
|
||||
evt_str = " - evt [" + ",".join(str(x) for x in evt_val) + "]"
|
||||
logger.info("=" * 100)
|
||||
logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles{evt_str}")
|
||||
logger.info("=" * 100)
|
||||
|
||||
events = sorted(events, key=lambda e: e['cycles'])
|
||||
if not events:
|
||||
logger.info(" No trace events recorded.")
|
||||
return
|
||||
|
||||
min_cycles = events[0]['cycles']
|
||||
|
||||
logger.info("Cycles %-30s" % "EventDetails" + " ".join(f"T{i:<2}" for i in range(10)) + " HMX")
|
||||
logger.info("-" * 100)
|
||||
|
||||
thread_stacks = [[] for _ in range(11)]
|
||||
|
||||
for e in events:
|
||||
t = e['thread']
|
||||
if t < 0 or t > 10:
|
||||
continue
|
||||
|
||||
if e['cycles'] >= min_cycles:
|
||||
rel_cycles = e['cycles'] - min_cycles
|
||||
else:
|
||||
rel_cycles = (e['cycles'] + 0x100000000) - min_cycles
|
||||
|
||||
state = e['state']
|
||||
evt_type = e['event']
|
||||
|
||||
# Determine char representing the event
|
||||
norm_evt = normalize_event_name(evt_type)
|
||||
char = '?'
|
||||
if norm_evt == 'V-COMP':
|
||||
char = 'V'
|
||||
elif norm_evt == 'M-COMP':
|
||||
char = 'H'
|
||||
elif norm_evt == 'A-QUANT':
|
||||
char = 'Q'
|
||||
elif norm_evt == 'A-PREP':
|
||||
char = 'A'
|
||||
elif norm_evt == 'Q-PREP':
|
||||
char = 'q'
|
||||
elif norm_evt == 'K-PREP':
|
||||
char = 'k'
|
||||
elif norm_evt == 'V-PREP':
|
||||
char = 'v'
|
||||
elif norm_evt == 'W-DEQUANT':
|
||||
char = 'D'
|
||||
elif norm_evt == 'O-PROC':
|
||||
char = 'O'
|
||||
elif norm_evt == 'W-PREP':
|
||||
char = 'P'
|
||||
elif norm_evt == 'DMA':
|
||||
char = 'M'
|
||||
|
||||
if state == 'start':
|
||||
thread_stacks[t].append(char)
|
||||
elif state == 'stop':
|
||||
if thread_stacks[t]:
|
||||
if thread_stacks[t][-1] == char:
|
||||
thread_stacks[t].pop()
|
||||
elif char in thread_stacks[t]:
|
||||
thread_stacks[t].remove(char)
|
||||
else:
|
||||
thread_stacks[t].pop()
|
||||
|
||||
cols = []
|
||||
for i in range(11):
|
||||
if thread_stacks[i]:
|
||||
cols.append(f"[{thread_stacks[i][-1]}]")
|
||||
else:
|
||||
cols.append(" | ")
|
||||
|
||||
evt_desc = f"T{t}: {evt_type} {state} ({e['info']})"
|
||||
logger.info(f"{rel_cycles:10d} %-30s" % evt_desc + " ".join(cols[:10]) + " " + cols[10])
|
||||
logger.info("-" * 100)
|
||||
|
||||
|
||||
def print_ascii_summary(op_name, dims, types, usec, cycles, events, evt_val=None):
|
||||
evt_str = ""
|
||||
if evt_val:
|
||||
evt_str = " - evt [" + ",".join(str(x) for x in evt_val) + "]"
|
||||
logger.info("=" * 100)
|
||||
logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles{evt_str}")
|
||||
logger.info("=" * 100)
|
||||
|
||||
events = sorted(events, key=lambda e: e['cycles'])
|
||||
if not events:
|
||||
logger.info(" No trace events recorded.")
|
||||
return
|
||||
|
||||
active_starts = {}
|
||||
thread_totals = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for e in events:
|
||||
t = e['thread']
|
||||
evt = e['event']
|
||||
info = e['info']
|
||||
cyc = e['cycles']
|
||||
state = e['state']
|
||||
|
||||
key = (t, evt, info)
|
||||
if state == 'start':
|
||||
active_starts[key] = cyc
|
||||
elif state == 'stop':
|
||||
if key in active_starts:
|
||||
start_cyc = active_starts[key]
|
||||
del active_starts[key]
|
||||
|
||||
if cyc >= start_cyc:
|
||||
dur = cyc - start_cyc
|
||||
else:
|
||||
dur = (cyc + 0x100000000) - start_cyc
|
||||
|
||||
norm_evt = normalize_event_name(evt)
|
||||
thread_totals[t][norm_evt] += dur
|
||||
|
||||
for t in sorted(thread_totals.keys()):
|
||||
thread_name = f"Thread {t} (HVX)" if t != 10 else "Thread 10 (HMX)"
|
||||
sorted_evts = sorted(thread_totals[t].items(), key=lambda item: item[0])
|
||||
|
||||
evt_strs = []
|
||||
for evt, dur in sorted_evts:
|
||||
pct = (dur / cycles * 100) if cycles > 0 else 0
|
||||
evt_strs.append(f"{evt} {dur} ({pct:.1f}%)")
|
||||
|
||||
logger.info(f" {thread_name:<16}: " + " | ".join(evt_strs))
|
||||
|
||||
|
||||
def generate_report(ops, top_n, width_overrides, sort_col, pmu_name=None):
|
||||
if not ops:
|
||||
logger.info("No valid records found.")
|
||||
return
|
||||
|
||||
grouped = defaultdict(list)
|
||||
for op in ops:
|
||||
key = (op['name'], op['dims'], op['types'])
|
||||
grouped[key].append(op)
|
||||
|
||||
group_stats = []
|
||||
for (name, dims, types), group_ops in grouped.items():
|
||||
usecs = [o['usec'] for o in group_ops]
|
||||
cycles = [o['cycles'] for o in group_ops]
|
||||
pmu_vals = [o['pmu_val'] for o in group_ops if o['pmu_val'] is not None]
|
||||
|
||||
avg_usec_val = statistics.mean(usecs)
|
||||
count_val = len(group_ops)
|
||||
tot_usec_val = avg_usec_val * count_val
|
||||
|
||||
group_stats.append({
|
||||
'op': name,
|
||||
'dims': dims,
|
||||
'dtypes': types,
|
||||
'count': str(count_val),
|
||||
'max_usec': str(max(usecs)),
|
||||
'avg_usec': f"{avg_usec_val:.2f}",
|
||||
'tot_usec': f"{tot_usec_val:.2f}",
|
||||
'max_cycles': str(max(cycles)),
|
||||
'avg_cycles': f"{statistics.mean(cycles):.2f}",
|
||||
'max_pmu': str(max(pmu_vals)) if pmu_vals else "0",
|
||||
'avg_pmu': f"{statistics.mean(pmu_vals):.2f}" if pmu_vals else "0.00",
|
||||
# Numeric values for accurate sorting
|
||||
'_sort_count': count_val,
|
||||
'_sort_max_usec': max(usecs),
|
||||
'_sort_avg_usec': avg_usec_val,
|
||||
'_sort_tot_usec': tot_usec_val,
|
||||
'_sort_max_cycles': max(cycles),
|
||||
'_sort_avg_cycles': statistics.mean(cycles),
|
||||
'_sort_max_pmu': max(pmu_vals) if pmu_vals else 0,
|
||||
'_sort_avg_pmu': statistics.mean(pmu_vals) if pmu_vals else 0
|
||||
})
|
||||
|
||||
# Sorting logic
|
||||
actual_sort_key = COL_MAP[sort_col][2]
|
||||
is_numeric = actual_sort_key.startswith("_") or actual_sort_key == "count"
|
||||
sorted_groups = sorted(group_stats, key=lambda x: x[actual_sort_key], reverse=is_numeric)[:top_n]
|
||||
|
||||
# Define initial column order
|
||||
active_cols = ["op", "dims", "dtypes"]
|
||||
if pmu_name:
|
||||
active_cols += ["max-pmu", "avg-pmu"]
|
||||
active_cols += ["tot-usec", "avg-usec", "avg-cycles", "max-usec", "max-cycles", "count"]
|
||||
|
||||
final_headers, final_keys, final_widths = [], [], []
|
||||
|
||||
for col_name in active_cols:
|
||||
data_key, header_text, _ = COL_MAP[col_name]
|
||||
if "pmu" in col_name and pmu_name:
|
||||
header_text = header_text.replace("PMU", pmu_name)
|
||||
|
||||
natural_width = max([len(str(row[data_key])) for row in sorted_groups] + [len(header_text)])
|
||||
target_width = width_overrides.get(col_name, natural_width)
|
||||
|
||||
if target_width == 0:
|
||||
continue
|
||||
|
||||
final_headers.append(header_text)
|
||||
final_keys.append(data_key)
|
||||
final_widths.append(target_width)
|
||||
|
||||
# Print Report
|
||||
logger.info(f"\n# Profile Report (Top {top_n} Ops sorted by {sort_col})\n")
|
||||
header_line = "| " + " | ".join(f"{h:<{final_widths[i]}}" for i, h in enumerate(final_headers)) + " |"
|
||||
sep_line = "| " + " | ".join("-" * final_widths[i] for i in range(len(final_headers))) + " |"
|
||||
logger.info(header_line)
|
||||
logger.info(sep_line)
|
||||
|
||||
for group in sorted_groups:
|
||||
row_vals = []
|
||||
for i, key in enumerate(final_keys):
|
||||
val = str(group[key])
|
||||
if len(val) > final_widths[i]:
|
||||
val = val[:final_widths[i] - 3] + "..."
|
||||
row_vals.append(f"{val:<{final_widths[i]}}")
|
||||
logger.info("| " + " | ".join(row_vals) + " |")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Post-process Op profile info.")
|
||||
parser.add_argument("logfile")
|
||||
parser.add_argument("-n", "--top", type=int, default=100)
|
||||
parser.add_argument("--sort", type=str, default="tot-usec", choices=list(COL_MAP.keys()))
|
||||
parser.add_argument("--pmu-index", type=int)
|
||||
parser.add_argument("--pmu-name", type=str)
|
||||
parser.add_argument("--width", action='append', default=['dims:40'], help="Override column width, e.g. --width dims:50")
|
||||
parser.add_argument("--timeline", type=str, nargs='?', const='summary', choices=["summary", "diagram"],
|
||||
help="Output ASCII art event summary or timing diagram (default: summary)")
|
||||
parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line")
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--head", type=int, help="Limit to first N ops")
|
||||
group.add_argument("--tail", type=int, help="Limit to last N ops")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
|
||||
if "pmu" in args.sort and args.pmu_index is None:
|
||||
logger.error(f"Cannot sort by '{args.sort}' without --pmu-index.")
|
||||
sys.exit(1)
|
||||
|
||||
overrides = {}
|
||||
if args.width:
|
||||
for w in args.width:
|
||||
try:
|
||||
name, val = w.split(':')
|
||||
overrides[name.lower()] = int(val)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid width format '{w}'")
|
||||
|
||||
final_pmu_name = (args.pmu_name or f"#{args.pmu_index}") if args.pmu_index is not None else None
|
||||
ops = parse_log(args.logfile, pmu_index=args.pmu_index)
|
||||
|
||||
if args.filter:
|
||||
try:
|
||||
filter_re = re.compile(args.filter)
|
||||
except re.error as e:
|
||||
logger.error(f"Invalid regex filter: {e}")
|
||||
sys.exit(1)
|
||||
ops = [op for op in ops if filter_re.search(op['op_text'])]
|
||||
|
||||
if args.head is not None:
|
||||
ops = ops[:args.head]
|
||||
elif args.tail is not None:
|
||||
ops = ops[-args.tail:]
|
||||
|
||||
if args.timeline:
|
||||
logger.info(f"\n# ASCII Timing {args.timeline.capitalize()}\n")
|
||||
printed_cnt = 0
|
||||
for op in ops:
|
||||
if args.timeline == "summary":
|
||||
print_ascii_summary(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'], op.get('evt_val'))
|
||||
elif args.timeline == "diagram":
|
||||
print_ascii_timeline(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'], op.get('evt_val'))
|
||||
printed_cnt += 1
|
||||
if printed_cnt >= args.top:
|
||||
break
|
||||
else:
|
||||
generate_report(ops, args.top, overrides, args.sort, pmu_name=final_pmu_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+499
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
import statistics
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from collections import defaultdict
|
||||
|
||||
logger = logging.getLogger("ggml-hexagon-trace")
|
||||
|
||||
op_pattern = re.compile(
|
||||
r"profile-op\s+(?P<op_name>[A-Z_0-9+]+):\s+.*?\s+:\s+(?P<dims>[\d:x\s\->!]+)\s+:\s+(?P<types>[a-z\d_\s\->x]+)\s+:\s+(?P<strides>[\d:x\s\->!]+?)\s+:\s+(?:(?P<params>.*?)\s+:\s+)?(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?"
|
||||
)
|
||||
|
||||
trace_pattern = re.compile(
|
||||
r"trace-op\s+(?P<op_name>[A-Z_0-9+]+):\s+thread\s+(?P<thread>\d+)\s+event\s+(?P<event>[A-Z_0-9\-]+)\s+info\s+(?P<info>\d+)\s+(?P<state>start|stop)\s+(?P<cycles>\d+)"
|
||||
)
|
||||
|
||||
|
||||
def normalize_event_name(evt_type):
|
||||
if evt_type == "HVX_COMP":
|
||||
return "V-COMP"
|
||||
if evt_type == "HMX_COMP":
|
||||
return "M-COMP"
|
||||
name = evt_type
|
||||
if name.startswith("HVX_") or name.startswith("HMX_"):
|
||||
name = name[4:]
|
||||
return name.replace("_", "-")
|
||||
|
||||
|
||||
class CycleUnwrapper:
|
||||
def __init__(self):
|
||||
self.last_raw = None
|
||||
self.high_part = 0
|
||||
|
||||
def unwrap(self, raw):
|
||||
if self.last_raw is None:
|
||||
self.last_raw = raw
|
||||
return raw
|
||||
diff = raw - self.last_raw
|
||||
if diff < -0x80000000:
|
||||
self.high_part += 0x100000000
|
||||
elif diff > 0x80000000:
|
||||
self.high_part -= 0x100000000
|
||||
self.last_raw = raw
|
||||
return raw + self.high_part
|
||||
|
||||
|
||||
def parse_log(file_path):
|
||||
try:
|
||||
if file_path != "-":
|
||||
f = open(file_path, 'r', encoding='utf-8', errors='ignore')
|
||||
else:
|
||||
f = os.fdopen(0, 'r', encoding='utf-8', errors='ignore')
|
||||
except FileNotFoundError:
|
||||
logger.error(f"file '{file_path}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
all_ops: List[Dict[str, Any]] = []
|
||||
current_op: Optional[Dict[str, Any]] = None
|
||||
unwrapper = CycleUnwrapper()
|
||||
line_idx = 0
|
||||
|
||||
for line in f:
|
||||
line_idx += 1
|
||||
if "|" in line and "profile-op" in line:
|
||||
parts = [p.strip() for p in line.split("|")]
|
||||
prefix = parts[0]
|
||||
prefix_match = re.search(r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)", prefix)
|
||||
if not prefix_match:
|
||||
continue
|
||||
|
||||
if len(parts) == 7:
|
||||
dims, types, strides, params, timings = parts[2], parts[3], parts[4], parts[5], parts[6]
|
||||
elif len(parts) == 6:
|
||||
dims, types, strides, params, timings = parts[2], parts[3], parts[4], "", parts[5]
|
||||
else:
|
||||
continue
|
||||
|
||||
timing_match = re.search(
|
||||
r"(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?",
|
||||
timings
|
||||
)
|
||||
if not timing_match:
|
||||
continue
|
||||
|
||||
op_match = timing_match
|
||||
op_name = prefix_match.group("op_name")
|
||||
else:
|
||||
op_match = op_pattern.search(line)
|
||||
if op_match:
|
||||
op_name = op_match.group('op_name')
|
||||
dims = op_match.group('dims').strip() if op_match.group('dims') else ''
|
||||
types = op_match.group('types').strip() if op_match.group('types') else ''
|
||||
strides = op_match.group('strides').strip() if op_match.group('strides') else ''
|
||||
params = op_match.group('params').strip() if ('params' in op_match.groupdict() and op_match.group('params')) else ''
|
||||
else:
|
||||
op_match = None
|
||||
|
||||
if op_match:
|
||||
cycles_start_raw = op_match.group('start')
|
||||
unwrapped_cycles_start = None
|
||||
if cycles_start_raw:
|
||||
unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw))
|
||||
|
||||
idx = line.find("profile-op ")
|
||||
op_text = line[idx + 11:].strip() if idx != -1 else line.strip()
|
||||
|
||||
current_op = {
|
||||
'name': op_name,
|
||||
'dims': dims,
|
||||
'types': types,
|
||||
'strides': strides,
|
||||
'params': params,
|
||||
'op_text': op_text,
|
||||
'usec': int(op_match.group('usec')),
|
||||
'cycles': int(op_match.group('cycles')),
|
||||
'cycles_start': int(cycles_start_raw) if cycles_start_raw else None,
|
||||
'unwrapped_cycles_start': unwrapped_cycles_start,
|
||||
'trace_events': [],
|
||||
'line_num': line_idx
|
||||
}
|
||||
all_ops.append(current_op)
|
||||
continue
|
||||
|
||||
trace_match = trace_pattern.search(line)
|
||||
if trace_match and current_op:
|
||||
if trace_match.group('op_name') == current_op['name']:
|
||||
raw_cyc = int(trace_match.group('cycles'))
|
||||
current_op['trace_events'].append({
|
||||
'thread': int(trace_match.group('thread')),
|
||||
'event': trace_match.group('event'),
|
||||
'info': int(trace_match.group('info')),
|
||||
'cycles': raw_cyc,
|
||||
'unwrapped_cycles': unwrapper.unwrap(raw_cyc),
|
||||
'state': trace_match.group('state')
|
||||
})
|
||||
|
||||
f.close()
|
||||
return all_ops
|
||||
|
||||
# --- Simple protobuf encoder ---
|
||||
|
||||
|
||||
def write_varint(val):
|
||||
if val < 0:
|
||||
val = (1 << 64) + val
|
||||
res = bytearray()
|
||||
while True:
|
||||
towrite = val & 0x7f
|
||||
val >>= 7
|
||||
if val > 0:
|
||||
res.append(towrite | 0x80)
|
||||
else:
|
||||
res.append(towrite)
|
||||
break
|
||||
return bytes(res)
|
||||
|
||||
|
||||
def pb_field(num, wire, data):
|
||||
return write_varint((num << 3) | wire) + data
|
||||
|
||||
|
||||
def pb_varint(num, val):
|
||||
return pb_field(num, 0, write_varint(val))
|
||||
|
||||
|
||||
def pb_length_delimited(num, data):
|
||||
return pb_field(num, 2, write_varint(len(data)) + data)
|
||||
|
||||
|
||||
def pb_string(num, text):
|
||||
return pb_length_delimited(num, text.encode('utf-8'))
|
||||
|
||||
|
||||
# Message Encoders
|
||||
def make_process_descriptor(pid, name):
|
||||
return pb_varint(1, pid) + pb_string(6, name)
|
||||
|
||||
|
||||
def make_thread_descriptor(pid, tid, name, sort_index=None):
|
||||
payload = pb_varint(1, pid) + pb_varint(2, tid) + pb_string(5, name)
|
||||
if sort_index is not None:
|
||||
payload += pb_varint(3, sort_index)
|
||||
return payload
|
||||
|
||||
|
||||
def make_track_descriptor(uuid, name=None, parent_uuid=None, thread=None, process=None, sibling_merge_behavior=None, child_ordering=None, sibling_order_rank=None):
|
||||
payload = pb_varint(1, uuid)
|
||||
if name is not None:
|
||||
payload += pb_string(2, name)
|
||||
if parent_uuid is not None:
|
||||
payload += pb_varint(5, parent_uuid)
|
||||
if process is not None:
|
||||
payload += pb_length_delimited(3, process)
|
||||
if thread is not None:
|
||||
payload += pb_length_delimited(4, thread)
|
||||
if sibling_merge_behavior is not None:
|
||||
payload += pb_varint(15, sibling_merge_behavior)
|
||||
if child_ordering is not None:
|
||||
payload += pb_varint(11, child_ordering)
|
||||
if sibling_order_rank is not None:
|
||||
payload += pb_varint(12, sibling_order_rank)
|
||||
return payload
|
||||
|
||||
|
||||
def make_debug_annotation(name, string_val=None, int_val=None):
|
||||
payload = pb_string(10, name)
|
||||
if string_val is not None:
|
||||
payload += pb_string(6, string_val)
|
||||
elif int_val is not None:
|
||||
payload += pb_varint(4, int_val)
|
||||
return payload
|
||||
|
||||
|
||||
def make_track_event(event_type, track_uuid, name=None, category=None, debug_annotations=None):
|
||||
payload = pb_varint(9, event_type)
|
||||
payload += pb_varint(11, track_uuid)
|
||||
if name is not None:
|
||||
payload += pb_string(23, name)
|
||||
if category is not None:
|
||||
payload += pb_string(22, category)
|
||||
if debug_annotations is not None:
|
||||
for da in debug_annotations:
|
||||
payload += pb_length_delimited(4, da)
|
||||
return payload
|
||||
|
||||
|
||||
def make_trace_packet(timestamp, track_event=None, track_descriptor=None, seq_id=1):
|
||||
payload = pb_varint(8, timestamp)
|
||||
payload += pb_varint(10, seq_id)
|
||||
if track_event is not None:
|
||||
payload += pb_length_delimited(11, track_event)
|
||||
if track_descriptor is not None:
|
||||
payload += pb_length_delimited(60, track_descriptor)
|
||||
return payload
|
||||
|
||||
|
||||
def write_trace_packet_to_file(f, packet_bytes):
|
||||
# Write as field 1 of top-level Trace message
|
||||
f.write(pb_length_delimited(1, packet_bytes))
|
||||
|
||||
# --- End Protobuf Encoder ---
|
||||
|
||||
|
||||
def generate_perfetto_trace(filtered_ops, output_path):
|
||||
if not filtered_ops:
|
||||
logger.warning("No operators found after filtering.")
|
||||
return
|
||||
|
||||
# Compute average frequency
|
||||
frequencies = []
|
||||
for op in filtered_ops:
|
||||
if op['usec'] > 0 and op['cycles'] > 0:
|
||||
frequencies.append(op['cycles'] / op['usec'])
|
||||
avg_freq_mhz = statistics.mean(frequencies) if frequencies else 1000.0
|
||||
if avg_freq_mhz <= 0:
|
||||
avg_freq_mhz = 1000.0
|
||||
|
||||
# Assign start and end cycles to each operator
|
||||
for op in filtered_ops:
|
||||
op['start_cycles'] = op['unwrapped_cycles_start']
|
||||
op['end_cycles'] = op['start_cycles'] + op['cycles']
|
||||
|
||||
global_min_cyc = min(op['start_cycles'] for op in filtered_ops if op['start_cycles'] is not None)
|
||||
|
||||
# Process events
|
||||
completed_events = []
|
||||
for op in filtered_ops:
|
||||
events = op['trace_events']
|
||||
if not events:
|
||||
continue
|
||||
events = sorted(events, key=lambda e: e['unwrapped_cycles'])
|
||||
|
||||
active_starts = {}
|
||||
for e in events:
|
||||
t = e['thread']
|
||||
evt = e['event']
|
||||
info = e['info']
|
||||
state = e['state']
|
||||
cyc = e['unwrapped_cycles']
|
||||
|
||||
key = (t, evt, info)
|
||||
if state == 'start':
|
||||
active_starts[key] = cyc
|
||||
elif state == 'stop':
|
||||
if key in active_starts:
|
||||
start_cyc = active_starts[key]
|
||||
del active_starts[key]
|
||||
completed_events.append({
|
||||
'thread': t,
|
||||
'event': evt,
|
||||
'info': info,
|
||||
'start_cyc': start_cyc,
|
||||
'end_cyc': cyc,
|
||||
'op_name': op['name']
|
||||
})
|
||||
|
||||
completed_events.sort(key=lambda e: e['start_cyc'])
|
||||
|
||||
# Convert event times to microseconds and apply clamp rounded to 1ns resolution (3 decimals)
|
||||
for e in completed_events:
|
||||
start_us = (e['start_cyc'] - global_min_cyc) / avg_freq_mhz
|
||||
dur_us = (e['end_cyc'] - e['start_cyc']) / avg_freq_mhz
|
||||
e['ts_ns'] = int(round(start_us * 1000))
|
||||
e['dur_ns'] = int(round(max(dur_us, 0.1) * 1000))
|
||||
|
||||
# Allocate slots (sub-tracks) to prevent overlaps on same virtual track
|
||||
active_slots = defaultdict(list)
|
||||
for e in completed_events:
|
||||
t = e['thread']
|
||||
evt = e['event']
|
||||
ts = e['ts_ns']
|
||||
dur = e['dur_ns']
|
||||
|
||||
norm_evt = normalize_event_name(evt)
|
||||
if norm_evt == "DMA":
|
||||
track_key = (t, "DMA")
|
||||
elif t == 10:
|
||||
track_key = (t, "HMX")
|
||||
else:
|
||||
track_key = (t, "HVX")
|
||||
|
||||
slots = active_slots[track_key]
|
||||
allocated_slot = -1
|
||||
for idx, slot_end_ns in enumerate(slots):
|
||||
if ts >= slot_end_ns:
|
||||
slots[idx] = ts + dur
|
||||
allocated_slot = idx
|
||||
break
|
||||
if allocated_slot == -1:
|
||||
slots.append(ts + dur)
|
||||
allocated_slot = len(slots) - 1
|
||||
e['slot'] = allocated_slot
|
||||
|
||||
# Generate Track IDs and track definitions
|
||||
used_tracks = {}
|
||||
for e in completed_events:
|
||||
t = e['thread']
|
||||
evt = e['event']
|
||||
slot = e['slot']
|
||||
|
||||
norm_evt = normalize_event_name(evt)
|
||||
if norm_evt == "DMA":
|
||||
track_evt = "DMA"
|
||||
evt_id = 1
|
||||
elif t == 10:
|
||||
track_evt = "HMX"
|
||||
evt_id = 3
|
||||
else:
|
||||
track_evt = "HVX"
|
||||
evt_id = 2
|
||||
|
||||
t_sort = 1 if t == 10 else t + 2
|
||||
# Unique UUID for each sub-track
|
||||
if t == 10:
|
||||
uuid = 20 # HMX thread track UUID
|
||||
else:
|
||||
uuid = int(t_sort * 1000000 + evt_id * 1000 + slot)
|
||||
e['uuid'] = uuid
|
||||
used_tracks[uuid] = (t, track_evt, slot)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
# Define Process with EXPLICIT child sorting
|
||||
proc_desc = make_process_descriptor(1, "HTP NPU")
|
||||
proc_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(1, process=proc_desc, child_ordering=3))
|
||||
write_trace_packet_to_file(f, proc_packet)
|
||||
|
||||
# Define Operators Track (UUID = 2) as a thread track at rank 1, tid 8
|
||||
op_thread_desc = make_thread_descriptor(1, 8, "Ops", sort_index=1)
|
||||
op_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(2, parent_uuid=1, thread=op_thread_desc))
|
||||
write_trace_packet_to_file(f, op_packet)
|
||||
|
||||
# Define HMX Thread Track (UUID = 20) at rank 2, tid 9
|
||||
hmx_thread_desc = make_thread_descriptor(1, 9, "HMX", sort_index=2)
|
||||
hmx_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(20, parent_uuid=1, thread=hmx_thread_desc))
|
||||
write_trace_packet_to_file(f, hmx_packet)
|
||||
|
||||
# Define Thread Tracks (T0, T1, ..., T9)
|
||||
unique_threads = sorted(list(set(t for (t, _, _) in used_tracks.values() if t != 10)))
|
||||
for t in unique_threads:
|
||||
thread_uuid = 10 + t
|
||||
thread_name = f"T{t}"
|
||||
# Sort order starts from index 3 (T0 -> 3, T1 -> 4, etc.)
|
||||
sort_index = 3 + t
|
||||
tid = 10 + t
|
||||
thread_desc = make_thread_descriptor(1, tid, thread_name, sort_index=sort_index)
|
||||
thread_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(
|
||||
thread_uuid,
|
||||
parent_uuid=1,
|
||||
thread=thread_desc,
|
||||
sibling_order_rank=sort_index,
|
||||
child_ordering=3 # Explicit child sorting for sub-tracks
|
||||
))
|
||||
write_trace_packet_to_file(f, thread_packet)
|
||||
|
||||
# Define Track descriptors for sub-tracks parented to thread tracks
|
||||
for uuid in sorted(used_tracks.keys()):
|
||||
if uuid == 20:
|
||||
continue
|
||||
t, evt, slot = used_tracks[uuid]
|
||||
name = f"T{t} {evt}"
|
||||
rank = 0 if evt == "HVX" else 1
|
||||
parent_thread_uuid = 10 + t
|
||||
# Sibling merge behavior: 1 (SIBLING_MERGE_BEHAVIOR_BY_TRACK_NAME)
|
||||
track_desc = make_track_descriptor(
|
||||
uuid=uuid,
|
||||
name=name,
|
||||
parent_uuid=parent_thread_uuid,
|
||||
sibling_merge_behavior=1,
|
||||
sibling_order_rank=rank
|
||||
)
|
||||
track_packet = make_trace_packet(0, track_descriptor=track_desc)
|
||||
write_trace_packet_to_file(f, track_packet)
|
||||
|
||||
# Emit Operators
|
||||
last_op_end_ns = 0
|
||||
for op in filtered_ops:
|
||||
op_start_ns = int(round(((op['start_cycles'] - global_min_cyc) / avg_freq_mhz) * 1000))
|
||||
op_dur_ns = int(round((op['cycles'] / avg_freq_mhz) * 1000))
|
||||
if op_start_ns < last_op_end_ns:
|
||||
op_start_ns = last_op_end_ns
|
||||
clamped_dur = max(op_dur_ns, 100) # Clamp to 100ns (0.1us)
|
||||
|
||||
# Debug annotations for Ops
|
||||
debug_annots = []
|
||||
if 'line_num' in op:
|
||||
debug_annots.append(make_debug_annotation("line", int_val=op['line_num']))
|
||||
if 'strides' in op and op['strides']:
|
||||
debug_annots.append(make_debug_annotation("strides", string_val=op['strides']))
|
||||
if 'params' in op and op['params'] and op['params'] != '----':
|
||||
debug_annots.append(make_debug_annotation("params", string_val=op['params']))
|
||||
|
||||
# Slice Begin
|
||||
evt_begin = make_track_event(1, 2, name=f"{op['name']} ({op['dims']})", category="operator", debug_annotations=debug_annots)
|
||||
packet_begin = make_trace_packet(op_start_ns, track_event=evt_begin)
|
||||
write_trace_packet_to_file(f, packet_begin)
|
||||
|
||||
# Slice End
|
||||
evt_end = make_track_event(2, 2)
|
||||
packet_end = make_trace_packet(op_start_ns + clamped_dur, track_event=evt_end)
|
||||
write_trace_packet_to_file(f, packet_end)
|
||||
|
||||
last_op_end_ns = op_start_ns + clamped_dur
|
||||
|
||||
# Emit Thread Trace Events
|
||||
for e in completed_events:
|
||||
norm_name = normalize_event_name(e['event'])
|
||||
name = f"DMA {e['info']}" if norm_name == "DMA" else norm_name
|
||||
|
||||
# Slice Begin
|
||||
evt_begin = make_track_event(1, e['uuid'], name=name, category="trace")
|
||||
packet_begin = make_trace_packet(e['ts_ns'], track_event=evt_begin)
|
||||
write_trace_packet_to_file(f, packet_begin)
|
||||
|
||||
# Slice End
|
||||
evt_end = make_track_event(2, e['uuid'])
|
||||
packet_end = make_trace_packet(e['ts_ns'] + e['dur_ns'], track_event=evt_end)
|
||||
write_trace_packet_to_file(f, packet_end)
|
||||
|
||||
logger.info(f"Successfully generated Perfetto trace at {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Convert Hexagon Op profile logs to native Perfetto Protobuf traces.")
|
||||
parser.add_argument("logfile", help="Path to hex-log profile file")
|
||||
parser.add_argument("-o", "--output", default="optrace.perfetto-trace", help="Output trace file path (default: optrace.perfetto-trace)")
|
||||
parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line")
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--head", type=int, help="Limit to first N ops")
|
||||
group.add_argument("--tail", type=int, help="Limit to last N ops")
|
||||
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
|
||||
ops = parse_log(args.logfile)
|
||||
|
||||
if args.filter:
|
||||
try:
|
||||
filter_re = re.compile(args.filter)
|
||||
except re.error as e:
|
||||
logger.error(f"Invalid regex filter: {e}")
|
||||
sys.exit(1)
|
||||
ops = [op for op in ops if filter_re.search(op['op_text'])]
|
||||
|
||||
if args.head is not None:
|
||||
ops = ops[:args.head]
|
||||
elif args.tail is not None:
|
||||
ops = ops[-args.tail:]
|
||||
|
||||
generate_perfetto_trace(ops, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
Appium-Python-Client==5.2.4
|
||||
attrs==25.4.0
|
||||
certifi==2025.10.5
|
||||
exceptiongroup==1.3.0
|
||||
h11==0.16.0
|
||||
idna==3.11
|
||||
iniconfig==2.1.0
|
||||
outcome==1.3.0.post0
|
||||
packaging==25.0
|
||||
pluggy==1.6.0
|
||||
PySocks==1.7.1
|
||||
pytest==8.4.2
|
||||
selenium==4.36.0
|
||||
sniffio==1.3.1
|
||||
sortedcontainers==2.4.0
|
||||
tomli==2.3.0
|
||||
trio==0.31.0
|
||||
trio-websocket==0.12.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
websocket-client==1.9.0
|
||||
wsproto==1.2.0
|
||||
@@ -0,0 +1,684 @@
|
||||
"""Run llama.cpp Hexagon tests in a single QDC job.
|
||||
|
||||
Bundles test scripts into one artifact and submits a single QDC job:
|
||||
|
||||
1. run_bench_tests_posix.py — llama-cli and llama-bench on CPU / GPU / NPU
|
||||
(from scripts/snapdragon/qdc/)
|
||||
|
||||
Results are written to $GITHUB_STEP_SUMMARY when set (GitHub Actions).
|
||||
|
||||
Prerequisites:
|
||||
pip install /path/to/qualcomm_device_cloud_sdk*.whl
|
||||
|
||||
Platform is inferred from --device:
|
||||
android Appium + pytest (Android phones: SM8750 / SM8650 / SM8850)
|
||||
linux BASH (Linux IoT: QCS9075M)
|
||||
|
||||
Required environment variables:
|
||||
QDC_API_KEY API key from QDC UI -> Users -> Settings -> API Keys
|
||||
|
||||
Usage:
|
||||
python run_qdc_jobs.py \\
|
||||
--pkg-dir pkg-snapdragon/llama.cpp \\
|
||||
--model-url https://.../Llama-3.2-1B-Instruct-Q4_0.gguf \\
|
||||
--device SM8750
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from qualcomm_device_cloud_sdk.api import qdc_api
|
||||
from qualcomm_device_cloud_sdk.logging import configure_logging
|
||||
from qualcomm_device_cloud_sdk.models import (
|
||||
ArtifactType,
|
||||
JobMode,
|
||||
JobState,
|
||||
JobSubmissionParameter,
|
||||
JobType,
|
||||
TestFramework,
|
||||
)
|
||||
|
||||
# configure_logging only sets up the SDK logger; basicConfig is needed for
|
||||
# our own log.info to reach stdout.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(name)s %(levelname)s - %(message)s",
|
||||
handlers=[logging.StreamHandler()],
|
||||
)
|
||||
configure_logging(level=logging.INFO, handlers=[logging.StreamHandler()])
|
||||
# Silence per-poll GET/status spam from the SDK and its HTTP client.
|
||||
logging.getLogger("qualcomm_device_cloud").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POLL_INTERVAL = 30
|
||||
JOB_TIMEOUT = 3600
|
||||
LOG_UPLOAD_TIMEOUT = 600
|
||||
CAPACITY_TIMEOUT = 1800
|
||||
CAPACITY_POLL = 60
|
||||
MAX_CONCURRENT_JOBS = 5
|
||||
DEFAULT_RETRIES = 0
|
||||
RETRY_DELAY = 300
|
||||
TERMINAL_STATES = {JobState.COMPLETED, JobState.CANCELED}
|
||||
NON_TERMINAL_STATES = {JobState.DISPATCHED, JobState.RUNNING, JobState.SETUP, JobState.SUBMITTED}
|
||||
|
||||
|
||||
class DeviceUnavailableError(Exception):
|
||||
"""Raised when the QDC device resource is not available (retryable)."""
|
||||
|
||||
|
||||
_SCRIPTS_DIR = Path(__file__).parent
|
||||
_TESTS_DIR = _SCRIPTS_DIR / "tests"
|
||||
|
||||
# --- Shared test assets -------------------------------------------------------
|
||||
_UTILS = _TESTS_DIR / "utils.py"
|
||||
_CONFTEST = _TESTS_DIR / "conftest.py"
|
||||
_PYTEST_LINE_RE = re.compile(
|
||||
r"(?:[\w/]+\.py::)?(?:\w+::)?([\w\[\].-]+)\s+(PASSED|FAILED|ERROR|SKIPPED)"
|
||||
)
|
||||
_EXCLUDED_LOGS = {
|
||||
"qdc_android_whole_host-000.log",
|
||||
"qdc_kernel_host-000.log",
|
||||
"qdc_LE_whole_host-000.log",
|
||||
"qdc_LE_kernel_host-000.log",
|
||||
"script.log",
|
||||
}
|
||||
_NON_TERMINAL_STATE_VALUES = {s.value for s in NON_TERMINAL_STATES}
|
||||
|
||||
# --- Android (Appium + pytest) assets ----------------------------------------
|
||||
_RUN_BENCH = _TESTS_DIR / "run_bench_tests_posix.py"
|
||||
_RUN_BACKEND_OPS = _TESTS_DIR / "run_backend_ops_posix.py"
|
||||
_REQUIREMENTS = _SCRIPTS_DIR / "requirements.txt"
|
||||
_UPSTREAM_ADB_SCRIPTS = (
|
||||
"https://raw.githubusercontent.com/ggml-org/llama.cpp/master/scripts/snapdragon/adb"
|
||||
)
|
||||
_ADB_SCRIPT_NAMES = [
|
||||
"run-bench.sh",
|
||||
"run-cli.sh",
|
||||
"run-completion.sh",
|
||||
"run-tool.sh",
|
||||
]
|
||||
|
||||
# --- Linux (BASH) assets ------------------------------------------------------
|
||||
_RUN_LINUX_TEMPLATE = _TESTS_DIR / "linux" / "run_linux.sh"
|
||||
_LINUX_ENTRY_SCRIPT = "/bin/bash /data/local/tmp/TestContent/run_linux.sh"
|
||||
|
||||
# =============================================================================
|
||||
# Artifact builders (per platform)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobResult:
|
||||
passed: bool
|
||||
tests: dict[str, bool] = field(default_factory=dict)
|
||||
raw_logs: dict[str, str] = field(default_factory=dict)
|
||||
failure_details: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _write_lf(path: Path, content: str) -> None:
|
||||
"""Write text with LF line endings (required by /bin/bash on Linux)."""
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _build_android_artifact(
|
||||
pkg_dir: Path,
|
||||
stage_dir: Path,
|
||||
test_mode: str,
|
||||
model_url: str | None,
|
||||
) -> Path:
|
||||
"""Android zip (Appium/pytest). Extracted by QDC under /qdc/appium/.
|
||||
|
||||
Zip structure:
|
||||
llama_cpp_bundle/ installed package (adb pushed to /data/local/tmp/)
|
||||
run-{bench,cli,completion,tool}.sh upstream adb wrappers (patched)
|
||||
tests/
|
||||
utils.py shared adb helpers
|
||||
conftest.py Appium pytest fixtures
|
||||
test_bench_posix.py bench + cli tests (for --test bench or all)
|
||||
test_backend_ops_posix.py test-backend-ops on HTP0
|
||||
requirements.txt
|
||||
pytest.ini addopts = --junitxml=results.xml
|
||||
"""
|
||||
bundle_dir = stage_dir / "llama_cpp_bundle"
|
||||
shutil.copytree(pkg_dir, bundle_dir)
|
||||
|
||||
# Download upstream adb scripts so they land at /qdc/appium/ on the QDC
|
||||
# runner. They wrap `adb shell` internally. Patch in `chmod +x bin/* lib/*`
|
||||
# right after `cd $basedir` so device binaries are executable.
|
||||
for name in _ADB_SCRIPT_NAMES:
|
||||
url = f"{_UPSTREAM_ADB_SCRIPTS}/{name}"
|
||||
dest = stage_dir / name
|
||||
log.info("Downloading %s", url)
|
||||
urllib.request.urlretrieve(url, str(dest))
|
||||
content = dest.read_text()
|
||||
content = content.replace(
|
||||
"cd $basedir;",
|
||||
"cd $basedir; chmod +x bin/* lib/* 2>/dev/null;",
|
||||
)
|
||||
dest.write_text(content)
|
||||
dest.chmod(0o755)
|
||||
|
||||
tests_dir = stage_dir / "tests"
|
||||
tests_dir.mkdir()
|
||||
|
||||
shutil.copy(_UTILS, tests_dir / "utils.py")
|
||||
shutil.copy(_CONFTEST, tests_dir / "conftest.py")
|
||||
|
||||
if test_mode in ("bench", "all"):
|
||||
assert model_url is not None
|
||||
(tests_dir / "test_bench_posix.py").write_text(
|
||||
_RUN_BENCH.read_text().replace("<<MODEL_URL>>", model_url)
|
||||
)
|
||||
if test_mode in ("backend-ops", "all"):
|
||||
shutil.copy(_RUN_BACKEND_OPS, tests_dir / "test_backend_ops_posix.py")
|
||||
|
||||
shutil.copy(_REQUIREMENTS, stage_dir / "requirements.txt")
|
||||
(stage_dir / "pytest.ini").write_text(
|
||||
"[pytest]\naddopts = --junitxml=results.xml\n"
|
||||
)
|
||||
|
||||
zip_base = str(stage_dir / "artifact")
|
||||
shutil.make_archive(zip_base, "zip", stage_dir)
|
||||
return Path(f"{zip_base}.zip")
|
||||
|
||||
|
||||
def _build_linux_artifact(
|
||||
pkg_dir: Path,
|
||||
stage_dir: Path,
|
||||
test_mode: str,
|
||||
model_url: str | None,
|
||||
) -> Path:
|
||||
"""Linux IoT zip (BASH framework). Extracted by QDC to /data/local/tmp/TestContent/.
|
||||
|
||||
Zip structure:
|
||||
run_linux.sh entry script (placeholder-substituted, LF line endings)
|
||||
llama_cpp_bundle/ installed package
|
||||
"""
|
||||
bundle_dir = stage_dir / "llama_cpp_bundle"
|
||||
shutil.copytree(pkg_dir, bundle_dir)
|
||||
|
||||
template = _RUN_LINUX_TEMPLATE.read_text(encoding="utf-8")
|
||||
rendered = template.replace("{MODEL_URL}", model_url or "").replace(
|
||||
"{TEST_MODE}", test_mode
|
||||
)
|
||||
script_path = stage_dir / "run_linux.sh"
|
||||
_write_lf(script_path, rendered)
|
||||
script_path.chmod(0o755)
|
||||
|
||||
zip_base = str(stage_dir / "artifact")
|
||||
shutil.make_archive(zip_base, "zip", stage_dir)
|
||||
return Path(f"{zip_base}.zip")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Platform enum + strategy table
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class Platform(enum.Enum):
|
||||
ANDROID = "android"
|
||||
LINUX = "linux"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformSpec:
|
||||
test_framework: TestFramework
|
||||
entry_script: str | None
|
||||
build_artifact: Callable[[Path, Path, str, str | None], Path]
|
||||
job_name_fmt: str
|
||||
|
||||
|
||||
PLATFORM_SPECS: dict[Platform, PlatformSpec] = {
|
||||
Platform.ANDROID: PlatformSpec(
|
||||
test_framework=TestFramework.APPIUM,
|
||||
entry_script=None,
|
||||
build_artifact=_build_android_artifact,
|
||||
job_name_fmt="{base}",
|
||||
),
|
||||
Platform.LINUX: PlatformSpec(
|
||||
test_framework=TestFramework.BASH,
|
||||
entry_script=_LINUX_ENTRY_SCRIPT,
|
||||
build_artifact=_build_linux_artifact,
|
||||
job_name_fmt="{base} (Linux)",
|
||||
),
|
||||
}
|
||||
|
||||
DEVICE_PLATFORM: dict[str, Platform] = {
|
||||
"SM8750": Platform.ANDROID,
|
||||
"SM8650": Platform.ANDROID,
|
||||
"SM8850": Platform.ANDROID,
|
||||
"QCS9075M": Platform.LINUX,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Shared QDC job plumbing
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def wait_for_job(client, job_id: str, timeout: int) -> str:
|
||||
elapsed = 0
|
||||
last_state = None
|
||||
consecutive_errors = 0
|
||||
max_consecutive_errors = 5
|
||||
while elapsed < timeout:
|
||||
try:
|
||||
raw = qdc_api.get_job_status(client, job_id)
|
||||
consecutive_errors = 0
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
log.warning(
|
||||
"Transient error polling job %s (%d/%d): %s",
|
||||
job_id,
|
||||
consecutive_errors,
|
||||
max_consecutive_errors,
|
||||
e,
|
||||
)
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
raise
|
||||
time.sleep(POLL_INTERVAL)
|
||||
elapsed += POLL_INTERVAL
|
||||
continue
|
||||
try:
|
||||
status = JobState(raw)
|
||||
except ValueError:
|
||||
status = raw
|
||||
if status in TERMINAL_STATES:
|
||||
return raw.lower()
|
||||
if raw != last_state:
|
||||
log.info("Job %s: %s", job_id, raw)
|
||||
last_state = raw
|
||||
time.sleep(POLL_INTERVAL)
|
||||
elapsed += POLL_INTERVAL
|
||||
# Abort to free the QDC concurrency slot instead of leaking it.
|
||||
try:
|
||||
qdc_api.abort_job(client, job_id)
|
||||
log.warning("Aborted job %s after timeout to free concurrency slot", job_id)
|
||||
except Exception as e:
|
||||
log.warning("Failed to abort job %s: %s", job_id, e)
|
||||
raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")
|
||||
|
||||
|
||||
def wait_for_log_upload(client, job_id: str) -> None:
|
||||
elapsed = 0
|
||||
while elapsed <= LOG_UPLOAD_TIMEOUT:
|
||||
try:
|
||||
status = (qdc_api.get_job_log_upload_status(client, job_id) or "").lower()
|
||||
except Exception as e:
|
||||
log.warning("get_job_log_upload_status failed: %s — will retry", e)
|
||||
status = ""
|
||||
if status in {"completed", "failed"}:
|
||||
return
|
||||
log.info("Waiting for log upload (status=%s) ...", status)
|
||||
time.sleep(POLL_INTERVAL)
|
||||
elapsed += POLL_INTERVAL
|
||||
log.warning("Timed out waiting for log upload after %ds", LOG_UPLOAD_TIMEOUT)
|
||||
|
||||
|
||||
def wait_for_capacity(client, max_jobs: int = MAX_CONCURRENT_JOBS) -> None:
|
||||
"""Block until the user's active (non-terminal) QDC job count is below max_jobs."""
|
||||
elapsed = 0
|
||||
while elapsed < CAPACITY_TIMEOUT:
|
||||
jobs_page = qdc_api.get_jobs_list(client, page_number=0, page_size=50)
|
||||
if jobs_page is None:
|
||||
log.warning(
|
||||
"Could not retrieve job list; proceeding without capacity check"
|
||||
)
|
||||
return
|
||||
items = getattr(jobs_page, "data", []) or []
|
||||
active = sum(
|
||||
1 for j in items if getattr(j, "state", None) in _NON_TERMINAL_STATE_VALUES
|
||||
)
|
||||
if active < max_jobs:
|
||||
log.info("Active QDC jobs: %d / %d — proceeding", active, max_jobs)
|
||||
return
|
||||
log.info(
|
||||
"Active QDC jobs: %d / %d — waiting %ds ...",
|
||||
active,
|
||||
max_jobs,
|
||||
CAPACITY_POLL,
|
||||
)
|
||||
time.sleep(CAPACITY_POLL)
|
||||
elapsed += CAPACITY_POLL
|
||||
raise TimeoutError(
|
||||
f"Capacity wait timed out after {CAPACITY_TIMEOUT}s"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Log parsing helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _parse_junit_xml(content: str) -> tuple[dict[str, bool], dict[str, str]]:
|
||||
try:
|
||||
root = ET.fromstring(content)
|
||||
except ET.ParseError:
|
||||
return {}, {}
|
||||
results: dict[str, bool] = {}
|
||||
failures: dict[str, str] = {}
|
||||
for tc in root.iter("testcase"):
|
||||
name = tc.get("name", "")
|
||||
if classname := tc.get("classname", ""):
|
||||
name = f"{classname}.{name}"
|
||||
failure_el = tc.find("failure")
|
||||
if failure_el is None:
|
||||
failure_el = tc.find("error")
|
||||
results[name] = failure_el is None
|
||||
if failure_el is not None:
|
||||
parts = [failure_el.get("message", ""), failure_el.text or ""]
|
||||
failures[name] = "\n".join(p for p in parts if p).strip()
|
||||
return results, failures
|
||||
|
||||
|
||||
def _parse_pytest_output(content: str) -> dict[str, bool]:
|
||||
results: dict[str, bool] = {}
|
||||
for m in _PYTEST_LINE_RE.finditer(content):
|
||||
results[m.group(1)] = m.group(2) == "PASSED"
|
||||
return results
|
||||
|
||||
|
||||
def fetch_logs_and_parse_tests(
|
||||
client, job_id: str, max_retries: int = 5, retry_delay: int = 30
|
||||
) -> tuple[dict[str, bool], dict[str, str], dict[str, str]]:
|
||||
"""Returns (test_results, raw_logs, failure_details)."""
|
||||
log_files = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
log_files = qdc_api.get_job_log_files(client, job_id)
|
||||
break
|
||||
except Exception as e:
|
||||
if attempt < max_retries:
|
||||
log.warning(
|
||||
"get_job_log_files failed (attempt %d/%d): %s — retrying in %ds",
|
||||
attempt, max_retries, e, retry_delay,
|
||||
)
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
log.error(
|
||||
"get_job_log_files failed after %d attempts: %s", max_retries, e
|
||||
)
|
||||
return {}, {}, {}
|
||||
if not log_files:
|
||||
log.warning("No log files returned for job %s", job_id)
|
||||
return {}, {}, {}
|
||||
|
||||
test_results: dict[str, bool] = {}
|
||||
pytest_fallback: dict[str, bool] = {}
|
||||
raw_logs: dict[str, str] = {}
|
||||
failure_details: dict[str, str] = {}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for lf in log_files:
|
||||
zip_path = os.path.join(tmpdir, "log.zip")
|
||||
log.info("Downloading log file: %s", lf.filename)
|
||||
qdc_api.download_job_log_files(client, lf.filename, zip_path)
|
||||
try:
|
||||
shutil.unpack_archive(zip_path, tmpdir, "zip")
|
||||
except Exception as e:
|
||||
log.warning("Could not unpack %s as zip: %s", lf.filename, e)
|
||||
|
||||
for root_dir, _, files in os.walk(tmpdir):
|
||||
for fname in sorted(files):
|
||||
fpath = os.path.join(root_dir, fname)
|
||||
content = Path(fpath).read_text(errors="replace")
|
||||
if fname.endswith(".xml"):
|
||||
results, failures = _parse_junit_xml(content)
|
||||
test_results.update(results)
|
||||
failure_details.update(failures)
|
||||
elif fname.endswith(".log"):
|
||||
if fname in _EXCLUDED_LOGS:
|
||||
continue
|
||||
log.info("--- %s ---\n%s", fname, content)
|
||||
raw_logs[fname] = content
|
||||
pytest_fallback.update(_parse_pytest_output(content))
|
||||
|
||||
return (
|
||||
(test_results if test_results else pytest_fallback),
|
||||
raw_logs,
|
||||
failure_details,
|
||||
)
|
||||
|
||||
|
||||
def write_summary(result: JobResult, title: str = "QDC Test Results") -> None:
|
||||
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if not summary_path:
|
||||
return
|
||||
|
||||
icon = "✅" if result.passed else "❌"
|
||||
|
||||
lines = [
|
||||
f"## {title}\n",
|
||||
f"Overall: {icon} {'PASSED' if result.passed else 'FAILED'}\n",
|
||||
]
|
||||
reportable = {n: ok for n, ok in result.tests.items() if "test_install" not in n}
|
||||
if reportable:
|
||||
lines += ["| Test | Result |", "| ---- | ------ |"]
|
||||
for name, ok in reportable.items():
|
||||
lines.append(f"| `{name}` | {'✅' if ok else '❌'} |")
|
||||
passed_n = sum(1 for v in reportable.values() if v)
|
||||
failed_n = sum(1 for v in reportable.values() if not v)
|
||||
lines += ["", f"**{passed_n} passed, {failed_n} failed**"]
|
||||
else:
|
||||
lines.append("_No per-test data available._")
|
||||
|
||||
failed_names = [n for n, ok in reportable.items() if not ok]
|
||||
if failed_names:
|
||||
lines += ["", "### Failures"]
|
||||
for name in failed_names:
|
||||
detail = result.failure_details.get(name)
|
||||
if detail:
|
||||
lines += [
|
||||
f"<details><summary><code>{name}</code></summary>",
|
||||
"",
|
||||
"```",
|
||||
detail,
|
||||
"```",
|
||||
"",
|
||||
"</details>",
|
||||
]
|
||||
|
||||
if result.raw_logs:
|
||||
lines += ["", "### Raw Logs"]
|
||||
for fname, content in sorted(result.raw_logs.items()):
|
||||
lines += [
|
||||
f"<details><summary>{fname}</summary>",
|
||||
"",
|
||||
"```",
|
||||
content.rstrip(),
|
||||
"```",
|
||||
"",
|
||||
"</details>",
|
||||
]
|
||||
|
||||
with open(summary_path, "a") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI + main
|
||||
# =============================================================================
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
p.add_argument("--pkg-dir", required=True, type=Path,
|
||||
help="Installed llama.cpp package directory (contains bin/ and lib/)")
|
||||
p.add_argument("--model-url",
|
||||
help="Direct URL to the GGUF model file (required for --test bench)")
|
||||
p.add_argument("--device", required=True,
|
||||
help="QDC chipset name, e.g. SM8750")
|
||||
p.add_argument("--test", choices=["bench", "backend-ops", "all"], default="bench",
|
||||
help="Test suite to run (default: bench)")
|
||||
p.add_argument("--job-timeout", type=int, default=JOB_TIMEOUT, metavar="SECONDS",
|
||||
help=f"Max seconds to wait for job completion (default: {JOB_TIMEOUT})")
|
||||
p.add_argument("--retries", type=int, default=DEFAULT_RETRIES, metavar="N",
|
||||
help="Number of retries when device is unavailable (default: 0)")
|
||||
p.add_argument("--retry-delay", type=int, default=RETRY_DELAY, metavar="SECONDS",
|
||||
help=f"Seconds to wait between retries (default: {RETRY_DELAY})")
|
||||
args = p.parse_args()
|
||||
if args.test in ("bench", "all") and not args.model_url:
|
||||
p.error("--model-url is required when --test bench or --test all")
|
||||
return args
|
||||
|
||||
|
||||
def _submit_and_run_job(client, args, spec, target_id, artifact_id) -> JobResult:
|
||||
"""Submit a QDC job and wait for results.
|
||||
|
||||
Raises DeviceUnavailableError for transient device/resource issues that
|
||||
are worth retrying. Returns JobResult for definitive outcomes (pass or
|
||||
test failure).
|
||||
"""
|
||||
try:
|
||||
wait_for_capacity(client)
|
||||
except TimeoutError:
|
||||
raise DeviceUnavailableError("Capacity wait timed out — device busy")
|
||||
|
||||
job_name = spec.job_name_fmt.format(base="llama.cpp Hexagon tests")
|
||||
|
||||
job_id = qdc_api.submit_job(
|
||||
public_api_client=client,
|
||||
target_id=target_id,
|
||||
job_name=job_name,
|
||||
external_job_id=None,
|
||||
job_type=JobType.AUTOMATED,
|
||||
job_mode=JobMode.APPLICATION,
|
||||
timeout=max(1, args.job_timeout // 60),
|
||||
test_framework=spec.test_framework,
|
||||
entry_script=spec.entry_script,
|
||||
job_artifacts=[artifact_id],
|
||||
monkey_events=None,
|
||||
monkey_session_timeout=None,
|
||||
job_parameters=[JobSubmissionParameter.WIFIENABLED],
|
||||
)
|
||||
if job_id is None:
|
||||
raise DeviceUnavailableError("Job submission failed — device may be unavailable")
|
||||
log.info("Job submitted: %s (device=%s)", job_id, args.device)
|
||||
|
||||
try:
|
||||
job_status = wait_for_job(client, job_id, timeout=args.job_timeout)
|
||||
except TimeoutError as e:
|
||||
raise DeviceUnavailableError(str(e))
|
||||
log.info("Job %s finished: %s", job_id, job_status)
|
||||
|
||||
wait_for_log_upload(client, job_id)
|
||||
tests, raw_logs, failure_details = fetch_logs_and_parse_tests(client, job_id)
|
||||
|
||||
job_ok = job_status == JobState.COMPLETED.value.lower()
|
||||
|
||||
if not job_ok and not tests:
|
||||
raise DeviceUnavailableError(
|
||||
f"Job did not complete (status={job_status}) and produced no test results"
|
||||
)
|
||||
|
||||
passed = job_ok and all(tests.values()) if tests else job_ok
|
||||
if spec.test_framework == TestFramework.BASH and not tests:
|
||||
log.error("No test results recovered (state=%s). Script likely never ran.", job_status)
|
||||
passed = False
|
||||
if not passed:
|
||||
log.error("Job did not complete successfully or tests failed (status=%s)", job_status)
|
||||
|
||||
return JobResult(passed=passed, tests=tests, raw_logs=raw_logs, failure_details=failure_details)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
platform = DEVICE_PLATFORM.get(args.device)
|
||||
if platform is None:
|
||||
log.error(
|
||||
"Unknown device %r. Known: %s",
|
||||
args.device, ", ".join(sorted(DEVICE_PLATFORM.keys())),
|
||||
)
|
||||
return 1
|
||||
spec = PLATFORM_SPECS[platform]
|
||||
|
||||
api_key = os.environ.get("QDC_API_KEY")
|
||||
if not api_key:
|
||||
log.error("QDC_API_KEY environment variable must be set")
|
||||
return 1
|
||||
if not args.pkg_dir.is_dir():
|
||||
log.error("--pkg-dir %s does not exist", args.pkg_dir)
|
||||
return 1
|
||||
|
||||
client = qdc_api.get_public_api_client_using_api_key(
|
||||
api_key_header=api_key,
|
||||
app_name_header="llama-cpp-ci",
|
||||
on_behalf_of_header="llama-cpp-ci",
|
||||
client_type_header="Python",
|
||||
)
|
||||
|
||||
target_id = qdc_api.get_target_id(client, args.device)
|
||||
if target_id is None:
|
||||
log.error("Could not find QDC target for device %r", args.device)
|
||||
return 1
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
log.info("Building %s artifact (test=%s) ...", platform.value, args.test)
|
||||
zip_path = spec.build_artifact(
|
||||
args.pkg_dir, Path(tmpdir), args.test, args.model_url
|
||||
)
|
||||
log.info("Uploading artifact (%d MB) ...", zip_path.stat().st_size // 1_000_000)
|
||||
artifact_id = qdc_api.upload_file(client, str(zip_path), ArtifactType.TESTSCRIPT)
|
||||
|
||||
if artifact_id is None:
|
||||
log.error("Artifact upload failed")
|
||||
return 1
|
||||
|
||||
max_attempts = 1 + args.retries
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
result = _submit_and_run_job(client, args, spec, target_id, artifact_id)
|
||||
break
|
||||
except DeviceUnavailableError as e:
|
||||
if attempt < max_attempts:
|
||||
log.warning(
|
||||
"Attempt %d/%d failed (device unavailable): %s — retrying in %ds",
|
||||
attempt, max_attempts, e, args.retry_delay,
|
||||
)
|
||||
time.sleep(args.retry_delay)
|
||||
else:
|
||||
log.error(
|
||||
"Attempt %d/%d failed (device unavailable): %s — no retries left",
|
||||
attempt, max_attempts, e,
|
||||
)
|
||||
write_summary(
|
||||
JobResult(passed=False, tests={}),
|
||||
title=f"QDC Device Unavailable ({args.device})",
|
||||
)
|
||||
return 1
|
||||
else:
|
||||
return 1
|
||||
|
||||
if args.test == "backend-ops":
|
||||
title = f"Backend Ops — HTP0 ({args.device})"
|
||||
elif args.test == "all":
|
||||
title = f"QDC Tests ({args.device})"
|
||||
else:
|
||||
title = f"QDC Test Results ({args.device})"
|
||||
write_summary(result, title=title)
|
||||
|
||||
return 0 if result.passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Shared pytest fixtures for QDC on-device test runners."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from appium import webdriver
|
||||
|
||||
from utils import options, write_qdc_log
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def driver():
|
||||
return webdriver.Remote(command_executor="http://127.0.0.1:4723/wd/hub", options=options)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
xml_path = getattr(session.config.option, "xmlpath", None) or "results.xml"
|
||||
if os.path.exists(xml_path):
|
||||
with open(xml_path) as f:
|
||||
write_qdc_log("results.xml", f.read())
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/bin/bash
|
||||
# llama.cpp Hexagon test entry script for QDC Linux IoT (BASH framework).
|
||||
#
|
||||
# Placeholders substituted by run_qdc_jobs.py (--platform linux) before upload:
|
||||
# {MODEL_URL} direct URL to a .gguf model file
|
||||
# {TEST_MODE} bench | backend-ops | all
|
||||
#
|
||||
# QDC extracts the artifact zip to /data/local/tmp/TestContent/ and invokes
|
||||
# this script via: /bin/bash /data/local/tmp/TestContent/run_linux.sh
|
||||
# Any files written under /data/local/tmp/QDC_logs/ are auto-uploaded.
|
||||
|
||||
set +e
|
||||
umask 022
|
||||
|
||||
LOG_DIR=/data/local/tmp/QDC_logs
|
||||
BUNDLE_DIR=/data/local/tmp/TestContent/llama_cpp_bundle
|
||||
MODEL_DIR=/data/local/tmp/gguf
|
||||
MODEL_PATH="$MODEL_DIR/model.gguf"
|
||||
RESULTS_XML="$LOG_DIR/results.xml"
|
||||
|
||||
mkdir -p "$LOG_DIR" "$MODEL_DIR"
|
||||
# Redirect all parent-shell output to script.log so QDC auto-uploads it;
|
||||
# per-case runs still capture their own stdout/stderr into dedicated logs.
|
||||
exec > "$LOG_DIR/script.log" 2>&1
|
||||
|
||||
echo "=== env ==="
|
||||
date -u
|
||||
uname -a
|
||||
pwd
|
||||
|
||||
mount -o rw,remount / 2>/dev/null || true
|
||||
|
||||
cd "$BUNDLE_DIR" || { echo "FATAL: bundle missing at $BUNDLE_DIR"; exit 1; }
|
||||
chmod +x bin/* 2>/dev/null
|
||||
export LD_LIBRARY_PATH="$BUNDLE_DIR/lib:$LD_LIBRARY_PATH"
|
||||
export ADSP_LIBRARY_PATH="$BUNDLE_DIR/lib"
|
||||
export GGML_HEXAGON_EXPERIMENTAL=1
|
||||
|
||||
echo "=== download model ==="
|
||||
MODEL_URL="{MODEL_URL}"
|
||||
if [ -z "$MODEL_URL" ]; then
|
||||
echo "No model URL provided, skipping download"
|
||||
elif [ ! -f "$MODEL_PATH" ]; then
|
||||
curl -L -fS --retry 3 --retry-delay 5 -o "$MODEL_PATH" "$MODEL_URL"
|
||||
curl_rc=$?
|
||||
if [ $curl_rc -ne 0 ]; then
|
||||
echo "FATAL: model download failed (rc=$curl_rc)"
|
||||
exit 1
|
||||
fi
|
||||
ls -la "$MODEL_PATH"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JUnit XML helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
xml_open() {
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="utf-8"?>' \
|
||||
"<testsuites>" \
|
||||
"<testsuite name=\"llama_cpp_linux\">" \
|
||||
> "$RESULTS_XML"
|
||||
}
|
||||
|
||||
xml_close() {
|
||||
printf '%s\n' '</testsuite>' '</testsuites>' >> "$RESULTS_XML"
|
||||
}
|
||||
|
||||
xml_case_pass() {
|
||||
local classname=$1 name=$2
|
||||
printf '<testcase classname="%s" name="%s"/>\n' "$classname" "$name" >> "$RESULTS_XML"
|
||||
}
|
||||
|
||||
xml_case_fail() {
|
||||
local classname=$1 name=$2 rc=$3 logfile=$4
|
||||
{
|
||||
printf '<testcase classname="%s" name="%s">\n' "$classname" "$name"
|
||||
printf '<failure message="exit %s"><![CDATA[\n' "$rc"
|
||||
tail -c 4096 "$logfile" 2>/dev/null | sed 's/]]>/]] >/g'
|
||||
printf '\n]]></failure>\n</testcase>\n'
|
||||
} >> "$RESULTS_XML"
|
||||
}
|
||||
|
||||
# Map backend name -> "NDEV --device" pair. "none" means no offload (CPU).
|
||||
backend_env() {
|
||||
case "$1" in
|
||||
cpu) echo "0 none" ;;
|
||||
gpu) echo "0 GPUOpenCL" ;;
|
||||
npu) echo "1 HTP0" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
backend_log_name() {
|
||||
case "$1" in
|
||||
cpu) echo "cpu" ;;
|
||||
gpu) echo "gpu" ;;
|
||||
npu) echo "htp" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
backend_device_name() {
|
||||
case "$1" in
|
||||
cpu) echo "none" ;;
|
||||
gpu) echo "GPUOpenCL" ;;
|
||||
npu) echo "HTP0" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Append a diagnostic block when a per-case `timeout N` fires (rc=124). The
|
||||
# naked log file at that point usually just ends mid-OpenCL-init with no
|
||||
# stderr, which is hard to read in CI summaries.
|
||||
note_timeout_if_triggered() {
|
||||
local rc=$1 budget=$2 log=$3
|
||||
[ "$rc" -eq 124 ] || return 0
|
||||
{
|
||||
printf '\n'
|
||||
printf '=== TIMEOUT after %ss ===\n' "$budget"
|
||||
printf 'uptime: '; uptime 2>/dev/null
|
||||
printf 'free -m:\n'; free -m 2>/dev/null
|
||||
printf 'loadavg: '; cat /proc/loadavg 2>/dev/null
|
||||
} >> "$log"
|
||||
}
|
||||
|
||||
completion_extra_args() {
|
||||
case "$1" in
|
||||
cpu) echo "--device none --ctx-size 128 -no-cnv -n 32 --seed 42 --batch-size 128" ;;
|
||||
gpu) echo "--device GPUOpenCL --ctx-size 128 -no-cnv -n 32 --seed 42 --ubatch-size 512" ;;
|
||||
npu) echo "--device HTP0 --ctx-size 128 -no-cnv -n 32 --seed 42 --ubatch-size 1024" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_completion_case() {
|
||||
local name=$1
|
||||
local parts=($(backend_env "$name"))
|
||||
local ndev=${parts[0]} device=${parts[1]}
|
||||
local device_log_name=$(backend_device_name "$name")
|
||||
local log="$LOG_DIR/llama_completion_${device_log_name}.log"
|
||||
local prompt="$LOG_DIR/bench_prompt.txt"
|
||||
echo 'What is the capital of France?' > "$prompt"
|
||||
local extra
|
||||
extra=$(completion_extra_args "$name")
|
||||
echo "=== [completion:$name] llama-completion --device $device (NDEV=$ndev) ==="
|
||||
timeout 600 env GGML_HEXAGON_NDEV=$ndev ./bin/llama-completion \
|
||||
-m "$MODEL_PATH" \
|
||||
-f "$prompt" \
|
||||
$extra \
|
||||
> "$log" 2>&1 < /dev/null
|
||||
local rc=$?
|
||||
note_timeout_if_triggered "$rc" 600 "$log"
|
||||
if [ $rc -eq 0 ]; then
|
||||
xml_case_pass "tests.test_bench_posix" "test_llama_completion[$name]"
|
||||
else
|
||||
xml_case_fail "tests.test_bench_posix" "test_llama_completion[$name]" "$rc" "$log"
|
||||
fi
|
||||
}
|
||||
|
||||
run_bench_case() {
|
||||
local name=$1
|
||||
local parts=($(backend_env "$name"))
|
||||
local ndev=${parts[0]} device=${parts[1]}
|
||||
local log_suffix=$(backend_log_name "$name")
|
||||
local log="$LOG_DIR/llama_bench_${log_suffix}.log"
|
||||
echo "=== [bench:$name] llama-bench --device $device (NDEV=$ndev) ==="
|
||||
timeout 600 env GGML_HEXAGON_NDEV=$ndev ./bin/llama-bench \
|
||||
-m "$MODEL_PATH" \
|
||||
--device "$device" \
|
||||
-ngl 99 \
|
||||
--batch-size 128 \
|
||||
-t 4 \
|
||||
-p 128 \
|
||||
-n 32 \
|
||||
> "$log" 2>&1
|
||||
local rc=$?
|
||||
note_timeout_if_triggered "$rc" 600 "$log"
|
||||
if [ $rc -eq 0 ]; then
|
||||
xml_case_pass "tests.test_bench_posix" "test_llama_bench[$name]"
|
||||
else
|
||||
xml_case_fail "tests.test_bench_posix" "test_llama_bench[$name]" "$rc" "$log"
|
||||
fi
|
||||
}
|
||||
|
||||
run_backend_ops_case() {
|
||||
local dtype=$1
|
||||
local log="$LOG_DIR/backend_ops_${dtype}.log"
|
||||
local pattern
|
||||
case "$dtype" in
|
||||
q4_0)
|
||||
# Matches Android: exclude a known-broken shape on NPU.
|
||||
pattern='^(?=.*type_a=q4_0)(?!.*type_b=f32,m=576,n=512,k=576).*$'
|
||||
;;
|
||||
*)
|
||||
pattern="type_a=${dtype}"
|
||||
;;
|
||||
esac
|
||||
echo "=== [backend-ops:$dtype] test-backend-ops -b HTP0 -o MUL_MAT ==="
|
||||
timeout 600 env GGML_HEXAGON_NDEV=1 GGML_HEXAGON_HOSTBUF=0 ./bin/test-backend-ops \
|
||||
-b HTP0 -o MUL_MAT -p "$pattern" \
|
||||
> "$log" 2>&1
|
||||
local rc=$?
|
||||
note_timeout_if_triggered "$rc" 600 "$log"
|
||||
if [ $rc -eq 0 ]; then
|
||||
xml_case_pass "tests.test_backend_ops_posix" "test_backend_ops_htp0[$dtype]"
|
||||
else
|
||||
xml_case_fail "tests.test_backend_ops_posix" "test_backend_ops_htp0[$dtype]" "$rc" "$log"
|
||||
fi
|
||||
}
|
||||
|
||||
xml_open
|
||||
|
||||
case "{TEST_MODE}" in
|
||||
bench)
|
||||
for b in cpu gpu npu; do run_completion_case "$b"; done
|
||||
for b in cpu gpu npu; do run_bench_case "$b"; done
|
||||
;;
|
||||
backend-ops)
|
||||
for d in mxfp4 fp16 q4_0; do run_backend_ops_case "$d"; done
|
||||
;;
|
||||
all)
|
||||
for b in cpu gpu npu; do run_completion_case "$b"; done
|
||||
for b in cpu gpu npu; do run_bench_case "$b"; done
|
||||
for d in mxfp4 fp16 q4_0; do run_backend_ops_case "$d"; done
|
||||
;;
|
||||
*)
|
||||
echo "FATAL: unsupported TEST_MODE={TEST_MODE}"
|
||||
;;
|
||||
esac
|
||||
|
||||
xml_close
|
||||
echo "=== done ==="
|
||||
# Host parses results.xml to decide pass/fail.
|
||||
exit 0
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
On-device test-backend-ops runner for llama.cpp (HTP0 backend).
|
||||
|
||||
On Android: executed by QDC's Appium test framework on the QDC runner.
|
||||
The runner has ADB access to the allocated device.
|
||||
On Linux: runs test-backend-ops directly via run_linux.sh (BASH framework).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import (
|
||||
BIN_PATH,
|
||||
push_bundle_if_needed,
|
||||
run_script,
|
||||
write_qdc_log,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def install(driver):
|
||||
push_bundle_if_needed(f"{BIN_PATH}/test-backend-ops")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_a", ["mxfp4", "fp16", "q4_0"])
|
||||
def test_backend_ops_htp0(type_a):
|
||||
if type_a == "q4_0":
|
||||
pattern = r'^(?=.*type_a=q4_0)(?!.*type_b=f32,m=576,n=512,k=576).*$'
|
||||
else:
|
||||
pattern = f"type_a={type_a}"
|
||||
|
||||
quoted_pattern = f'"{pattern}"' if type_a == "q4_0" else pattern
|
||||
result = run_script(
|
||||
"run-tool.sh",
|
||||
extra_env={"HB": "0"},
|
||||
extra_args=["test-backend-ops", "-b", "HTP0", "-o", "MUL_MAT", "-p", quoted_pattern],
|
||||
)
|
||||
write_qdc_log(f"backend_ops_{type_a}.log", result.stdout or "")
|
||||
assert result.returncode == 0, (
|
||||
f"test-backend-ops type_a={type_a} failed (exit {result.returncode})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
|
||||
if os.path.exists("results.xml"):
|
||||
with open("results.xml") as f:
|
||||
write_qdc_log("results.xml", f.read())
|
||||
sys.exit(ret)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
On-device bench and completion test runner for llama.cpp (CPU, GPU, NPU backends).
|
||||
|
||||
On Android: calls upstream run-*.sh scripts from llama.cpp/scripts/snapdragon/adb/
|
||||
on the QDC runner host (scripts wrap commands in ``adb shell`` internally).
|
||||
|
||||
On Linux: runs llama-bench directly via run_linux.sh (BASH framework).
|
||||
|
||||
Placeholders replaced at artifact creation time by run_qdc_jobs.py:
|
||||
<<MODEL_URL>> Direct URL to the GGUF model file (downloaded on-device)
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import (
|
||||
BIN_PATH,
|
||||
MODEL_DEVICE_PATH,
|
||||
MODEL_NAME,
|
||||
PROMPT_DIR,
|
||||
push_bundle_if_needed,
|
||||
run_adb_command,
|
||||
run_script,
|
||||
write_qdc_log,
|
||||
)
|
||||
|
||||
MODEL_URL = "<<MODEL_URL>>"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def install(driver):
|
||||
push_bundle_if_needed(f"{BIN_PATH}/llama-cli")
|
||||
run_adb_command(f"mkdir -p /data/local/tmp/gguf {PROMPT_DIR}")
|
||||
run_adb_command(f"echo 'What is the capital of France?' > {PROMPT_DIR}/bench_prompt.txt")
|
||||
check = subprocess.run(
|
||||
["adb", "shell", f"ls {MODEL_DEVICE_PATH}"],
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
)
|
||||
if check.returncode != 0:
|
||||
run_adb_command(f'curl -L -J --output {MODEL_DEVICE_PATH} "{MODEL_URL}"')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device",
|
||||
[
|
||||
pytest.param("none", id="cpu"),
|
||||
pytest.param("GPUOpenCL", id="gpu"),
|
||||
pytest.param("HTP0", id="npu"),
|
||||
],
|
||||
)
|
||||
def test_llama_completion(device):
|
||||
result = run_script(
|
||||
"run-completion.sh",
|
||||
extra_env={"D": device, "M": MODEL_NAME},
|
||||
extra_args=["--batch-size", "128", "-n", "128", "--seed", "42",
|
||||
"-f", f"{PROMPT_DIR}/bench_prompt.txt"],
|
||||
)
|
||||
write_qdc_log(f"llama_completion_{device}.log", result.stdout or "")
|
||||
assert result.returncode == 0, (
|
||||
f"llama-completion {device} failed (exit {result.returncode})"
|
||||
)
|
||||
|
||||
|
||||
_DEVICE_LOG_NAME = {"none": "cpu", "GPUOpenCL": "gpu", "HTP0": "htp"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device",
|
||||
[
|
||||
pytest.param("none", id="cpu"),
|
||||
pytest.param("GPUOpenCL", id="gpu"),
|
||||
pytest.param("HTP0", id="npu"),
|
||||
],
|
||||
)
|
||||
def test_llama_bench(device):
|
||||
result = run_script(
|
||||
"run-bench.sh",
|
||||
extra_env={"D": device, "M": MODEL_NAME},
|
||||
extra_args=["--batch-size", "128", "-p", "128", "-n", "32"],
|
||||
)
|
||||
write_qdc_log(f"llama_bench_{_DEVICE_LOG_NAME[device]}.log", result.stdout or "")
|
||||
assert result.returncode == 0, (
|
||||
f"llama-bench {device} failed (exit {result.returncode})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
|
||||
if os.path.exists("results.xml"):
|
||||
with open("results.xml") as f:
|
||||
write_qdc_log("results.xml", f.read())
|
||||
sys.exit(ret)
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Shared helpers for QDC on-device test runners."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from appium.options.common import AppiumOptions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# On-device paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BUNDLE_PATH = "/data/local/tmp/llama.cpp"
|
||||
BIN_PATH = f"{BUNDLE_PATH}/bin"
|
||||
LIB_PATH = f"{BUNDLE_PATH}/lib"
|
||||
QDC_LOGS_PATH = "/data/local/tmp/QDC_logs"
|
||||
SCRIPTS_DIR = "/qdc/appium"
|
||||
MODEL_NAME = "model.gguf"
|
||||
MODEL_DEVICE_PATH = "/data/local/tmp/gguf/model.gguf"
|
||||
PROMPT_DIR = "/data/local/tmp/scorecard_prompts"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appium session options
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
options = AppiumOptions()
|
||||
options.set_capability("automationName", "UiAutomator2")
|
||||
options.set_capability("platformName", "Android")
|
||||
options.set_capability("deviceName", os.getenv("ANDROID_DEVICE_VERSION"))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shell / process helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def write_qdc_log(filename: str, content: str) -> None:
|
||||
"""Write content as a log file for QDC log collection."""
|
||||
subprocess.run(
|
||||
["adb", "shell", f"mkdir -p {QDC_LOGS_PATH}"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as f:
|
||||
f.write(content)
|
||||
tmp_path = f.name
|
||||
try:
|
||||
subprocess.run(
|
||||
["adb", "push", tmp_path, f"{QDC_LOGS_PATH}/{filename}"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
|
||||
def ensure_bundle(check_binary: str | None = None) -> None:
|
||||
"""Ensure the llama_cpp_bundle is available on the target device."""
|
||||
push_bundle_if_needed(check_binary or f"{BIN_PATH}/llama-cli")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Android / Linux host helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def run_adb_command(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command on-device via ``adb shell`` with exit-code sentinel."""
|
||||
raw = subprocess.run(
|
||||
["adb", "shell", f"{cmd}; echo __RC__:$?"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
stdout = raw.stdout
|
||||
returncode = raw.returncode
|
||||
if stdout:
|
||||
lines = stdout.rstrip("\n").split("\n")
|
||||
if lines and lines[-1].startswith("__RC__:"):
|
||||
try:
|
||||
returncode = int(lines[-1][7:])
|
||||
stdout = "\n".join(lines[:-1]) + "\n"
|
||||
except ValueError:
|
||||
pass
|
||||
log.info(stdout)
|
||||
result = subprocess.CompletedProcess(raw.args, returncode, stdout=stdout)
|
||||
if check:
|
||||
assert returncode == 0, f"Command failed (exit {returncode})"
|
||||
return result
|
||||
|
||||
|
||||
def run_script(
|
||||
script: str,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
extra_args: list[str] | None = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run an upstream shell script from /qdc/appium/ on the QDC runner host."""
|
||||
env = os.environ.copy()
|
||||
env["GGML_HEXAGON_EXPERIMENTAL"] = "1"
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
cmd = [f"{SCRIPTS_DIR}/{script}"] + (extra_args or [])
|
||||
result = subprocess.run(
|
||||
cmd, env=env,
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
)
|
||||
log.info(result.stdout)
|
||||
return result
|
||||
|
||||
|
||||
def adb_shell(cmd: str) -> None:
|
||||
"""Run a command via adb shell (fire-and-forget, no error check)."""
|
||||
subprocess.run(
|
||||
["adb", "shell", "sh", "-c", cmd],
|
||||
capture_output=True, encoding="utf-8", errors="replace", check=False,
|
||||
)
|
||||
|
||||
|
||||
def push_bundle_if_needed(check_binary: str) -> None:
|
||||
"""Push llama_cpp_bundle to the device if check_binary is not already present."""
|
||||
result = subprocess.run(
|
||||
["adb", "shell", f"ls {check_binary}"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
subprocess.run(
|
||||
["adb", "push", "/qdc/appium/llama_cpp_bundle/", BUNDLE_PATH],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
subprocess.run(
|
||||
["adb", "shell", f"find {BUNDLE_PATH}/bin -type f -exec chmod 755 {{}} +"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Basedir on device
|
||||
$basedir=".\pkg-snapdragon"
|
||||
|
||||
$cli_opts=$args
|
||||
|
||||
$model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
if ($null -ne $env:M) {
|
||||
$model=$env:M
|
||||
}
|
||||
|
||||
$device="HTP0"
|
||||
if ($null -ne $env:D) {
|
||||
$device=$env:D
|
||||
}
|
||||
|
||||
if ($null -ne $env:V) {
|
||||
$env:GGML_HEXAGON_VERBOSE=$env:V
|
||||
}
|
||||
|
||||
if ($null -ne $env:PROF) {
|
||||
$env:GGML_HEXAGON_PROFILE=$env:PROF
|
||||
}
|
||||
|
||||
if ($null -ne $env:OPSTAGE) {
|
||||
$env:GGML_HEXAGON_OPSTAGE=$env:OPSTAGE
|
||||
}
|
||||
|
||||
if ($null -ne $env:NHVX) {
|
||||
$env:GGML_HEXAGON_NHVX=$env:NHVX
|
||||
}
|
||||
|
||||
if ($null -ne $env:NDEV) {
|
||||
$env:GGML_HEXAGON_NDEV=$env:NDEV
|
||||
}
|
||||
|
||||
if ($null -ne $env:HB) {
|
||||
$env:GGML_HEXAGON_HOSTBUF=$env:HB
|
||||
}
|
||||
|
||||
$env:ADSP_LIBRARY_PATH="$basedir\lib"
|
||||
|
||||
& "$basedir\bin\llama-bench.exe" `
|
||||
--mmap 0 -m $basedir\..\..\gguf\$model `
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 `
|
||||
--ubatch-size 1024 -ngl 99 --device $device $cli_opts
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Basedir on device
|
||||
$basedir=".\pkg-snapdragon"
|
||||
|
||||
$cli_opts=$args
|
||||
|
||||
$model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
if ($null -ne $env:M) {
|
||||
$model=$env:M
|
||||
}
|
||||
|
||||
$device="HTP0"
|
||||
if ($null -ne $env:D) {
|
||||
$device=$env:D
|
||||
}
|
||||
|
||||
if ($null -ne $env:V) {
|
||||
$env:GGML_HEXAGON_VERBOSE=$env:V
|
||||
}
|
||||
|
||||
if ($null -ne $env:SCHED) {
|
||||
$env:GGML_SCHED_DEBUG=$env:SCHED; $cli_opts="$cli_opts -v"
|
||||
}
|
||||
|
||||
if ($null -ne $env:PROF) {
|
||||
$env:GGML_HEXAGON_PROFILE=$env:PROF
|
||||
}
|
||||
|
||||
if ($null -ne $env:OPSTAGE) {
|
||||
$env:GGML_HEXAGON_OPSTAGE=$env:OPSTAGE
|
||||
}
|
||||
|
||||
if ($null -ne $env:NHVX) {
|
||||
$env:GGML_HEXAGON_NHVX=$env:NHVX
|
||||
}
|
||||
|
||||
if ($null -ne $env:NDEV) {
|
||||
$env:GGML_HEXAGON_NDEV=$env:NDEV
|
||||
}
|
||||
|
||||
if ($null -ne $env:HB) {
|
||||
$env:GGML_HEXAGON_HOSTBUF=$env:HB
|
||||
}
|
||||
|
||||
$env:ADSP_LIBRARY_PATH="$basedir\lib"
|
||||
|
||||
& "$basedir\bin\llama-cli.exe" `
|
||||
--no-mmap -m $basedir\..\..\gguf\$model `
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 `
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on `
|
||||
-ngl 99 --device $device $cli_opts
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Basedir on device
|
||||
$basedir=".\pkg-snapdragon"
|
||||
|
||||
$cli_opts=$args
|
||||
|
||||
$model="Llama-3.2-3B-Instruct-Q4_0.gguf"
|
||||
if ($null -ne $env:M) {
|
||||
$model=$env:M
|
||||
}
|
||||
|
||||
$device="HTP0"
|
||||
if ($null -ne $env:D) {
|
||||
$device=$env:D
|
||||
}
|
||||
|
||||
if ($null -ne $env:V) {
|
||||
$env:GGML_HEXAGON_VERBOSE=$env:V
|
||||
}
|
||||
|
||||
if ($null -ne $env:SCHED) {
|
||||
$env:GGML_SCHED_DEBUG=$env:SCHED; $cli_opts="$cli_opts -v"
|
||||
}
|
||||
|
||||
if ($null -ne $env:PROF) {
|
||||
$env:GGML_HEXAGON_PROFILE=$env:PROF
|
||||
}
|
||||
|
||||
if ($null -ne $env:OPSTAGE) {
|
||||
$env:GGML_HEXAGON_OPSTAGE=$env:OPSTAGE
|
||||
}
|
||||
|
||||
if ($null -ne $env:NHVX) {
|
||||
$env:GGML_HEXAGON_NHVX=$env:NHVX
|
||||
}
|
||||
|
||||
if ($null -ne $env:NDEV) {
|
||||
$env:GGML_HEXAGON_NDEV=$env:NDEV
|
||||
}
|
||||
|
||||
if ($null -ne $env:HB) {
|
||||
$env:GGML_HEXAGON_HOSTBUF=$env:HB
|
||||
}
|
||||
|
||||
$env:ADSP_LIBRARY_PATH="$basedir\lib"
|
||||
|
||||
& "$basedir\bin\llama-completion.exe" `
|
||||
--no-mmap -m $basedir\..\..\gguf\$model `
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 `
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on `
|
||||
-ngl 99 -no-cnv --device $device $cli_opts
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Basedir on device
|
||||
$basedir=".\pkg-snapdragon"
|
||||
|
||||
$cli_opts=$args
|
||||
|
||||
$model="gemma-3-4b-it-Q4_0.gguf"
|
||||
if ($null -ne $env:M) {
|
||||
$model=$env:M
|
||||
}
|
||||
|
||||
$mmproj="mmproj-F16.gguf"
|
||||
if ($null -ne $env:MMPROJ) {
|
||||
$mmproj=$env:MMPROJ
|
||||
}
|
||||
|
||||
$image=""
|
||||
if ($null -ne $env:IMG) {
|
||||
$image=$env:IMG
|
||||
}
|
||||
|
||||
$device="HTP0"
|
||||
if ($null -ne $env:D) {
|
||||
$device=$env:D
|
||||
}
|
||||
|
||||
if ($null -ne $env:V) {
|
||||
$env:GGML_HEXAGON_VERBOSE=$env:V
|
||||
}
|
||||
|
||||
if ($null -ne $env:SCHED) {
|
||||
$env:GGML_SCHED_DEBUG=$env:SCHED; $cli_opts="$cli_opts -v"
|
||||
}
|
||||
|
||||
if ($null -ne $env:PROF) {
|
||||
$env:GGML_HEXAGON_PROFILE=$env:PROF
|
||||
}
|
||||
|
||||
if ($null -ne $env:OPSTAGE) {
|
||||
$env:GGML_HEXAGON_OPSTAGE=$env:OPSTAGE
|
||||
}
|
||||
|
||||
if ($null -ne $env:NHVX) {
|
||||
$env:GGML_HEXAGON_NHVX=$env:NHVX
|
||||
}
|
||||
|
||||
if ($null -ne $env:NDEV) {
|
||||
$env:GGML_HEXAGON_NDEV=$env:NDEV
|
||||
}
|
||||
|
||||
if ($null -ne $env:HB) {
|
||||
$env:GGML_HEXAGON_HOSTBUF=$env:HB
|
||||
}
|
||||
|
||||
if ($null -ne $env:MTMD_DEVICE) {
|
||||
$env:MTMD_BACKEND_DEVICE=$env:MTMD_DEVICE
|
||||
}
|
||||
|
||||
$env:ADSP_LIBRARY_PATH="$basedir\lib"
|
||||
|
||||
& "$basedir\bin\llama-mtmd-cli.exe" `
|
||||
--no-mmap -m $basedir\..\..\gguf\$model `
|
||||
--mmproj $basedir\..\..\gguf\$mmproj `
|
||||
--image $basedir\..\..\gguf\$image `
|
||||
--poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 `
|
||||
--ctx-size 8192 --ubatch-size 1024 -fa on `
|
||||
-ngl 99 --device $device -v $cli_opts
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Basedir on device
|
||||
$basedir=".\pkg-snapdragon"
|
||||
|
||||
if ($args.Count -eq 0) {
|
||||
Write-Host "No arguments provided.Expected the tool and argument to run."
|
||||
exit -1
|
||||
}
|
||||
|
||||
$tool=$args[0]
|
||||
$cli_opts=@()
|
||||
|
||||
if ($args.Count -gt 1) {
|
||||
$cli_opts=$args[1..($args.Count - 1)]
|
||||
$remainingArgs = $args[1..($args.Count - 1)]
|
||||
}
|
||||
|
||||
$device="HTP0"
|
||||
if ($null -ne $env:D) {
|
||||
$device=$env:D
|
||||
}
|
||||
|
||||
if ($null -ne $env:V) {
|
||||
$env:GGML_HEXAGON_VERBOSE=$env:V
|
||||
}
|
||||
|
||||
if ($null -ne $env:SCHED) {
|
||||
$env:GGML_SCHED_DEBUG=$env:SCHED; $cli_opts="$cli_opts -v"
|
||||
}
|
||||
|
||||
if ($null -ne $env:PROF) {
|
||||
$env:GGML_HEXAGON_PROFILE=$env:PROF
|
||||
}
|
||||
|
||||
if ($null -ne $env:OPSTAGE) {
|
||||
$env:GGML_HEXAGON_OPSTAGE=$env:OPSTAGE
|
||||
}
|
||||
|
||||
if ($null -ne $env:NHVX) {
|
||||
$env:GGML_HEXAGON_NHVX=$env:NHVX
|
||||
}
|
||||
|
||||
if ($null -ne $env:NDEV) {
|
||||
$env:GGML_HEXAGON_NDEV=$env:NDEV
|
||||
}
|
||||
|
||||
if ($null -ne $env:HB) {
|
||||
$env:GGML_HEXAGON_HOSTBUF=$env:HB
|
||||
}
|
||||
|
||||
$env:ADSP_LIBRARY_PATH="$basedir\lib"
|
||||
|
||||
& "$basedir\bin\$tool" `
|
||||
$cli_opts
|
||||
@@ -0,0 +1,105 @@
|
||||
# Requires Run as Administrator is NOT strictly necessary for User-scope env vars,
|
||||
# but recommended for creating directories in C:\ root if permissions are restricted.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Configuration ---
|
||||
$BaseDir = "C:\Qualcomm"
|
||||
|
||||
# SDK 1: Hexagon
|
||||
$HexagonUrl = "https://github.com/snapdragon-toolchain/hexagon-sdk/releases/download/v6.6.0.0/hexagon-sdk-v6.6.0.0-arm64-wos.tar.xz"
|
||||
$HexagonParent = Join-Path $BaseDir "Hexagon_SDK"
|
||||
$HexagonSdkVersion = "6.6.0.0"
|
||||
$HexagonToolsVersion = "19.0.07"
|
||||
$HexagonSdkTarget = Join-Path $HexagonParent $HexagonSdkVersion
|
||||
$HexagonToolsTarget = Join-Path $HexagonSdkTarget "\tools\HEXAGON_Tools\$HexagonToolsVersion"
|
||||
|
||||
# SDK 2: OpenCL
|
||||
$OpenCLUrl = "https://github.com/snapdragon-toolchain/opencl-sdk/releases/download/v2.3.2/adreno-opencl-sdk-v2.3.2-arm64-wos.tar.xz"
|
||||
$OpenCLParent = Join-Path $BaseDir "OpenCL_SDK"
|
||||
$OpenCLVersion = "2.3.2"
|
||||
$OpenCLTarget = Join-Path $OpenCLParent $OpenCLVersion
|
||||
|
||||
# --- Helper Function ---
|
||||
function Install-QualcommSDK {
|
||||
param (
|
||||
[string]$Url,
|
||||
[string]$ParentDir,
|
||||
[string]$TargetDir,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
# 1. Create Parent Directory
|
||||
if (-not (Test-Path -Path $ParentDir)) {
|
||||
Write-Host "Creating directory: $ParentDir" -ForegroundColor Cyan
|
||||
New-Item -Path $ParentDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# 2. Check for Specific Version Directory
|
||||
if (Test-Path -Path $TargetDir) {
|
||||
Write-Host "$Name ($TargetDir) already exists. Skipping download." -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "$Name not found. preparing to download..." -ForegroundColor Yellow
|
||||
|
||||
# Create the target directory to extract into
|
||||
New-Item -Path $TargetDir -ItemType Directory -Force | Out-Null
|
||||
|
||||
# Define temporary archive path
|
||||
$TempFile = Join-Path $ParentDir "temp_sdk.tar.xz"
|
||||
|
||||
try {
|
||||
# Download
|
||||
Write-Host "Downloading from: $Url"
|
||||
Invoke-WebRequest -Uri $Url -OutFile $TempFile
|
||||
|
||||
# Untar
|
||||
# Note: We assume Windows includes tar.exe (Win 10 build 17063+)
|
||||
Write-Host "Extracting archive to $TargetDir..."
|
||||
|
||||
# We use -C to extract contents INTO the target directory created above
|
||||
tar -xJvf $TempFile -C $TargetDir\..
|
||||
|
||||
Write-Host "Extraction complete." -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to download or extract $Name. Error: $_"
|
||||
# Cleanup target dir if failed so script tries again next time
|
||||
Remove-Item -Path $TargetDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
finally {
|
||||
# Cleanup Archive
|
||||
if (Test-Path $TempFile) { Remove-Item $TempFile -Force }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Execution ---
|
||||
|
||||
# 1. Ensure Base C:\Qualcomm exists
|
||||
if (-not (Test-Path $BaseDir)) {
|
||||
New-Item -Path $BaseDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# 2. Run Install Logic
|
||||
Install-QualcommSDK -Url $HexagonUrl -ParentDir $HexagonParent -TargetDir $HexagonSdkTarget -Name "Hexagon SDK"
|
||||
Install-QualcommSDK -Url $OpenCLUrl -ParentDir $OpenCLParent -TargetDir $OpenCLTarget -Name "OpenCL SDK"
|
||||
|
||||
# --- Environment Variables ---
|
||||
|
||||
Write-Host "`nSetting Environment Variables..." -ForegroundColor Cyan
|
||||
|
||||
# Set OPENCL_SDK_ROOT
|
||||
[System.Environment]::SetEnvironmentVariable('OPENCL_SDK_ROOT', $OpenCLTarget, [System.EnvironmentVariableTarget]::User)
|
||||
$env:OPENCL_SDK_ROOT = $OpenCLTarget # Set for current session as well
|
||||
Write-Host "OPENCL_SDK_ROOT set to: $OpenCLTarget"
|
||||
|
||||
# Set HEXAGON_SDK_ROOT
|
||||
[System.Environment]::SetEnvironmentVariable('HEXAGON_SDK_ROOT', $HexagonSdkTarget, [System.EnvironmentVariableTarget]::User)
|
||||
$env:HEXAGON_SDK_ROOT = $HexagonSdkTarget # Set for current session as well
|
||||
Write-Host "HEXAGON_SDK_ROOT set to: $HexagonSdkTarget"
|
||||
|
||||
# Set HEXAGON_SDK_ROOT
|
||||
[System.Environment]::SetEnvironmentVariable('HEXAGON_TOOLS_ROOT', $HexagonToolsTarget, [System.EnvironmentVariableTarget]::User)
|
||||
$env:HEXAGON_TOOLS_ROOT = $HexagonToolsTarget # Set for current session as well
|
||||
Write-Host "HEXAGON_TOOLS_ROOT set to: $HexagonToolsTarget"
|
||||
Reference in New Issue
Block a user