7,672
社区成员
发帖
与我相关
我的任务
分享这篇文档记录了在一个"AidLux X1 盒子"上部署大模型的完整过程。
目标模型原本定的是 Qwen3.8-27B,中途根据硬件实情转向 NPU + Qwen3-8B。
所有命令都是实际敲过、实际跑通的,不是抄文档。
本文由agent辅助完成
在动手之前,先把"能"和"不能"说清楚,省得你白折腾一整天(我替你折腾过了)。
| 路线 | 模型 | 速度 | 结论 |
|---|---|---|---|
| NPU(Hexagon) | Qwen3-8B w4a16 | 7.4 tok/s | ✅ 就用这个 |
| CPU(llama.cpp) | Qwen3.8-27B Q3_K | 0.10 tok/s | ⚠️ 能跑,但慢到没法用 |
| GPU(Vulkan/Adreno 740) | Qwen3.8-27B Q3_K | — | ❌ 驱动直接崩 |
三个必须记住的硬事实:
所以最终方案:NPU 跑 Qwen3-8B,8K 上下文,7.4 tok/s。
下面的文档会告诉你每一步怎么走,以及为什么要这么走。
SSH : aidlux@10.174.211.94 (默认密码 aidlux)(根据实际网络状况决定)
系统 : Android 13 上的 AidLux 容器,Ubuntu 22.04.2 LTS
内核 : 5.15.178-android13 aarch64
SoC : Qualcomm QCS8550 (/sys/devices/soc0/machine = QCS_KALAMAP)
CPU : 6 核 Kryo
GPU : Adreno 740v2 (Vulkan 可用, 但大模型会崩)
NPU : Hexagon (通过 QNN / AidGen 访问)
内存 : 15.6 GB 统一内存
磁盘 : 79 GB,可用约 36 GB(折仅代表笔者手上的开发版状况)
听起来是废话,但这一步就有坑。
坑:沙箱里没有 pty。 sshpass、expect 这类靠伪终端的自动登录工具会直接报The system has no more ptys —— 因为运行环境禁止 open("/dev/ptmx")。
绕法:用 SSH_ASKPASS。 这是 OpenSSH 自带的功能,原本给图形界面弹密码框用的,
但它同样能在没有终端的情况下喂密码,而且完全不需要 pty:
import os, subprocess
# 1) 写一个只会 echo 密码的脚本
askpass = os.path.abspath("askpass.sh")
open(askpass, "w").write("#!/bin/sh\necho 'aidlux'\n")
os.chmod(askpass, 0o700)
# 2) 环境变量告诉 ssh 用它
env = dict(os.environ)
env.update({"SSH_ASKPASS": askpass, "SSH_ASKPASS_REQUIRE": "force", "DISPLAY": ":0"})
# 3) 关键:用 setsid 脱离当前会话,preexec_fn 里调用
subprocess.run(
["ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
"-o", "PubkeyAuthentication=no", "-o", "PreferredAuthentications=password",
"-o", "NumberOfPasswordPrompts=1", "-o", "LogLevel=ERROR",
"aidlux@10.174.211.94", "uname -a"],
env=env, stdin=subprocess.DEVNULL, preexec_fn=os.setsid,
)
这三个要点缺一不可:SSH_ASKPASS_REQUIRE=force(强制走 askpass)、preexec_fn=os.setsid(脱离控制终端)、stdin=DEVNULL(免得 ssh 去读标准输入)。
我封了三个小工具在 ~/Aidlux/:run_ssh.py(跑命令)、ssh_upload.py(传文件)、ssh_fire.py(后台放长任务)。
💡
ssh_upload.py的写法很实用:把本地文件通过 stdin 管道喂给远端的cat > 文件,
一行搞定上传,不依赖 scp/sftp。
一开始我想当然地写了 ssh ... 'cat > f.py' < localfile,结果远端文件是空的 —— 因为subprocess 的 stdin 被设成了 DEVNULL。改成 input=data 显式喂进去就好了。
小教训:自动化里用 heredoc 传文件,是自找麻烦。用 stdin 管道。
ssh host 'nohup ./long.sh &' 这种写法,SSH 会话不会退出,一直等到子进程结束(或超时)。
正确姿势:
nohup ./long.sh > log.txt 2>&1 < /dev/null &
三个重定向都要:> log 收输出、2>&1 收错误、< /dev/null 断开标准输入。
另外把"启动"和"查看"分成两次 SSH 调用,别在一个会话里等。
这是最容易走弯路的认知点:
.serialized.bin.aidem所以上 NPU 的前提是:Model Farm 上得有你要的模型的现成包。
设备自带一个命令行工具 aidllm,直接列出该 SoC(8550)可用的模型:
aidllm remote-list api
输出长这样(节选):
socfg: { qc8550 enterprise}
Current Soc : 8550
Name Url
qwen3-0.6b-qnn2.36-w4a16-qcs8550 aplux/qwen3-0.6b-qnn2.36-w4a16-qcs8550
qwen3-1.7b-qnn2.36-w4a16-qcs8550 aplux/qwen3-1.7b-qnn2.36-w4a16-qcs8550
qwen3-4b-instruct-2507-qnn2.36-w4a16-qcs8550 aplux/qwen3-4b-instruct-2507-qnn2.36-w4a16-qcs8550
qwen3-8b-qnn2.36-w4a16-qcs8550 aplux/qwen3-8b-qnn2.36-w4a16-qcs8550
qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550 aplux/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550
qwen3-vl-8b-instruct-qnn2.48-w4a16-qcs8550 aplux/qwen3-vl-8b-instruct-qnn2.48-w4a16-qcs8550
...
这一刻你应该意识到:列表里最大就是 8B,Qwen3.8-27B 不在上面。 后面的故事就是从这来的。
命名规则很好懂:
qwen3-8b - cl8192 - qnn2.36 - w4a16 - qcs8550
↑ ↑ ↑ ↑ ↑
模型 上下文长度 QNN版本 量化方式 目标芯片
(决定用哪个
qnn库: 236/240)
好消息:AidLux 镜像里通常已经装好了 AidGen SDK。检查一下:
ls /usr/local/lib/libaidgen*
# libaidgen.so libaidgen.so.2 libaidgen.so.2.1.0
# libaidgen_qnn236.so libaidgen_qnn240.so ...
ls /usr/local/share/aidgen/
# docs examples
# 用包管理器确认版本
aid-pkg list | grep aidgen
# aidgen-qnn236 2.1.0.120
# aidgen-qnn240 2.1.0.120
# aidgen-sdk 2.1.0.120
如果没有,官方文档的装法是:
sudo aid-pkg update
sudo aid-pkg -i aidgen-sdk
sudo aid-pkg -i aidgen-qnn236
sudo aid-pkg -i aidgen-qnn240
两种方式。
方式一:用 aidllm pull(推荐)
aidllm pull api aplux/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550
方式二:模型包已经躺在磁盘上了
我这次的情况就是:设备 /sdcard 上已经有一个 4.8 GB 的包:
ls -lh /sdcard/*.tar.gz
# -rw-rw----. 4.5G qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.tar.gz
解压看看里面有啥:
cd /sdcard && tar -tzf qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.tar.gz
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/kv-cache.primary.qnn-htp ← KV cache
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/htp_backend_ext_config.json ← NPU 性能配置
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/qwen3-8b-...-qcs8550.json ← 模型描述文件
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/qwen3-8b_qnn236_qcs8550_cl8192_1_of_5.serialized.bin.aidem
./qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/qwen3-8b_qnn236_qcs8550_cl8192_2_of_5.serialized.bin.aidem
...(共 5 个分片,合计约 5.1 GB)
解压(4.8 GB 的包,给它一两分钟,放后台跑):
cd /sdcard && nohup tar -xzf qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.tar.gz \
> extract.log 2>&1 < /dev/null &
打开那个模型 JSON —— 它是唯一权威的路径说明:
cat /sdcard/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.json
{
"backend_type": "genie",
"prefix_path": "/opt/aidlux/app/aid-openai-api/res/models/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/kv-cache.primary.qnn-htp",
"model": {
"path": [
"/opt/aidlux/app/aid-openai-api/res/models/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/qwen3-8b_qnn236_qcs8550_cl8192_1_of_5.serialized.bin.aidem",
... (共 5 条)
]
}
}
所以模型必须放在 /opt/aidlux/app/aid-openai-api/res/models/<model_id>/。
好在我这台设备上已经放好了,只是 /sdcard 留了一份冗余:
ls -la /opt/aidlux/app/aid-openai-api/res/models/
# drwxrwxr-x. qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550/ ← 7.7 GB,完整
# -rw-r-----. qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.tar.gz ← 4.8 GB,原始备份(留着)
💡
du -sh显示 7.7 GB 而压缩包只有 4.8 GB,是正常的 —— 权重是 w4a16 量化格式,解压后会膨胀。
看一眼服务配置 /opt/aidlux/app/aid-openai-api/api_cfg.json:
python3 -c "
import json
d = json.load(open('/opt/aidlux/app/aid-openai-api/api_cfg.json'))
print('已注册模型:')
for m in d['model_cfg_list']:
print(' -', m['model_id'])
print('默认模型:', d['default_model_id'])
"
输出:
已注册模型:
- qwen3-8b-cl16384-qnn2.36-w4a16-qcs8550 ← 盘上根本没有!
- qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550 ← 这个才是真的
默认模型: qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550
这是个会咬人的坑: 配置里注册了一个磁盘上不存在的 cl16384,服务加载时会去找它、然后报错。
所以启动前先自检、把不存在的条目剔掉。
永远先备份再改配置:
A=/opt/aidlux/app/aid-openai-api
cp -a $A/api_cfg.json $A/api_cfg.json.bak-$(date +%s)
python3 - <<'PY'
import json, os
p = "/opt/aidlux/app/aid-openai-api/api_cfg.json"
M = "/opt/aidlux/app/aid-openai-api/res/models"
d = json.load(open(p))
kept = []
for m in d.get("model_cfg_list", []):
if os.path.isdir(os.path.join(M, m["model_id"])):
kept.append(m); print("KEEP", m["model_id"])
else:
print("DROP", m["model_id"], "(not on disk)")
d["model_cfg_list"] = kept
if kept and d.get("default_model_id") not in [k["model_id"] for k in kept]:
d["default_model_id"] = kept[0]["model_id"]
json.dump(d, open(p, "w"), indent=2, ensure_ascii=False)
PY
这是最关键的一条命令,有两个细节容易漏:
cd /opt/aidlux/app/aid-openai-api
nohup env LD_LIBRARY_PATH=./lib ./api \
--device dsp \
--aidgen_qnn_ver 240 \
--config ./api_cfg.json \
> /dev/null 2>&1 < /dev/null &
LD_LIBRARY_PATH=./lib —— 不加就直接报 libaidgense.so: cannot open shared object file,lib/ 子目录里,不在系统库路径。--device dsp —— 明确指定跑在 DSP/Hexagon,也就是 NPU 上。这是"用没用 NPU"的开关。服务大约 12 秒后可用。验证:
curl -s http://127.0.0.1:8888/v1/models
{
"object": "list",
"loaded_id": "qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550",
"model_type": "llm",
"data": [{"id": "qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550", "owned_by": "aplux", ...}]
}
看到 loaded_id 有值,就说明模型已经常驻 NPU 内存,可以开聊了。
也可以用官方 CLI 管理:
aidllm start api # 启动
aidllm status api # 状态 → "Api server status: Running"
aidllm stop api # 停止
⚠️ 安全提醒: 服务默认监听
0.0.0.0:8888,整个局域网都能访问,不是只有本机。
在api_cfg.json的http_cfg里把ip改成127.0.0.1,或用防火墙挡掉 8888 端口。
别光信 --device dsp 这个参数,用证据说话。有两个层次的证据。
证据一(最硬):进程加载了 QNN HTP 的库。 HTP 就是 Hexagon Tensor Processor:
P=$(pgrep -f "\./api" | head -1)
grep -aoE "libQnn[A-Za-z0-9]*\.so|libaidgen[a-z0-9]*\.so" /proc/$P/maps | sort -u
libQnnHtpNetRunExtensions.so
libQnnHtp.so ← HTP = Hexagon Tensor Processor
libQnnHtpV73Stub.so ← V73 = Hexagon DSP 架构代号
libQnnSystem.so
libaidgen.so
libaidgense.so
看到 libQnnHtp.so 和 libQnnHtpV73Stub.so,就说明 NPU 后端已经装载了。
证据二:通过 /dev/adsprpc 与 DSP 通信。 这是高通 DSP 的远程调用通道:
ls -l /proc/$P/fd | grep -c adsprpc
⚠️ 这个数字会变:空闲时常驻 3 个句柄,推理时我见过涨到 100。
所以别拿它当固定基准,> 0就说明通道开着。
(CPU 版的 llama.cpp 进程则完全没有libQnnHtp,也没有 adsprpc 句柄 —— 对照很明显。)
curl -s http://127.0.0.1:8888/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550",
"messages": [{"role": "user", "content": "你好,请用一句话介绍你自己。"}],
"stream": false,
"max_tokens": 256
}'
返回(节选):
<think>
好的,用户让我用一句话介绍自己。首先,我需要确定用户的需求是什么……
</think>
我是通义千问,由通义实验室研发的超大规模语言模型,擅长多语言理解和生成,
能够完成对话、创作、推理等多种任务。
usage 显示 prompt_tokens: 15, completion_tokens: 199,耗时 26.7 秒。
💡 Qwen3 是混合推理模型,默认会先输出
<think>...</think>思考过程再给答案。
如果只想看结论,可以用配置里的qwen3-no-think模板,或在 prompt 里要求不要思考。
用同一个接口,改 max_tokens 测不同负载(脚本:~/qwen38/bench_npu.sh):
| 场景 | prompt | completion | 耗时 | 解码速度 |
|---|---|---|---|---|
| 短问答 | 10 | 184 | 24.7 s | 7.45 tok/s |
| 长文生成 | 23 | 700 | 93.3 s | 7.51 tok/s |
| 长提示(616 tok) | 616 | 521 | 70.8 s | 7.36 tok/s(综合 16.1) |
怎么读这组数:
两种方式。
方式一:脚本(懒人推荐)
我这里写了个 scripts/npu_chat.py,通过 SSH 隧道调回环接口,不额外暴露端口:
python3 scripts/npu_chat.py -n 256 "用一句话介绍你自己"
python3 scripts/npu_chat.py --no-stream -p "写一首关于西湖的诗"
方式二:直接打(设备在局域网里就能连)
curl -s http://10.174.211.94:8888/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550",
"messages":[{"role":"user","content":"你好"}],
"max_tokens":128}'
Python 客户端(OpenAI 兼容,流式):
import json, requests
def chat(messages, model="qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550"):
r = requests.post(
"http://127.0.0.1:8888/v1/chat/completions",
json={"model": model, "messages": messages, "stream": True},
stream=True,
)
r.raise_for_status()
for line in r.iter_lines():
if not line:
continue
text = line.decode()
if not text.startswith("data: "):
continue
body = text[6:]
if body.strip() == "[DONE]":
break
chunk = json.loads(body)
piece = chunk["choices"][0]["delta"].get("content")
if piece:
print(piece, end="", flush=True)
chat([{"role": "user", "content": "Give me a short intro to LLMs."}])
因为它是 OpenAI 兼容接口,所以 LangChain、 LlamaIndex、各种 Chat UI 基本都能直接接,
把 base_url 指向 http://<设备IP>:8888/v1 就行。
这条路是为 27B 准备的。虽然最后没用它上线,但过程值得记下来,因为坑比较典型。
设备有 gcc/cmake,直接源码编译:
git clone --depth 1 https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=OFF -DGGML_OPENCL=OFF \
-DLLAMA_CURL=OFF -DGGML_NATIVE=ON \
-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_SERVER=ON -DLLAMA_BUILD_TOOLS=ON
cmake --build build-cpu --config Release -j6
编译很顺利,得到 llama-cli、llama-completion、llama-bench、llama-server。
UD- 量化从 hf-mirror 下载(国内直连 huggingface.co 不通,hf-mirror.com 通):
BASE=https://hf-mirror.com/unsloth/Qwen3.8-27B-GGUF/resolve/main
curl -L -C - -o Qwen3.8-27B-UD-Q3_K_XL.gguf \
"$BASE/Qwen3.8-27B-UD-Q3_K_XL.gguf" # 12.5 GB
Unsloth 的量化矩阵(节选)和选择理由:
| 量化 | 大小 | 适合内存 |
|---|---|---|
| UD-IQ2_XXS | 6.9 GB | 8 GB |
| UD-Q2_K_XL | 9.4 GB | 12 GB |
| UD-Q3_K_XL | 12.5 GB | 16 GB ← 我们选它 |
| UD-Q4_K_M | 15.7 GB | 24 GB |
| UD-Q5_K_M | 18.9 GB | 32 GB |
UD- 前缀是 Unsloth Dynamic,它会按层动态分配量化精度(重要层用高精度),
同等体积下质量比传统均匀量化更好。在内存受限时优先选 UD 系列。
新版 llama.cpp(v0.4.1)改了一堆参数名,踩了个遍:
| 旧写法 | 新写法 | 说明 |
|---|---|---|
-no-cnv | -no-cnv 只在 llama-completion 里有效 | llama-cli 里已移除,会报 invalid argument |
--no-mmap | --load-mode none | 改成了枚举值:auto/mmap/none/mlock/dio |
-st | -st(单轮) | 但配合 -p 时行为有变化,容易卡在交互提示符 |
结论:要做纯文本补全,用 llama-completion,不要用 llama-cli。
BIN=~/qwen38/src/llama.cpp/build-cpu/bin
$BIN/llama-completion \
-m ~/qwen38/models/Qwen3.8-27B-UD-Q3_K_XL.gguf \
-c 2048 -n 64 -t 6 --no-warmup -no-cnv \
--temp 0.7 --top-p 0.8 --top-k 20 --min-p 0 \
-p "你好,请用一句话介绍你自己。"
运行时有一堆无害的警告,别被吓到:
W model has unused tensor blk.64.ffn_gate.weight (size = 73113600 bytes) -- ignoring
这是模型的 MTP(多 token 预测)层没被加载,正常现象。
它确实生成了正确内容(能看到它输出 <think> 推理):
你好,请用一句话介绍你自己。
<think>
用户要求我用一句话介绍自己。我应该简洁、……
但权威数字来自 llama-bench:
$BIN/llama-bench -m ~/qwen38/models/Qwen3.8-27B-UD-Q3_K_XL.gguf -p 32 -n 16 -t 6
| model | size | params | backend | threads | test | t/s |
| qwen35 27B Q3_K - Large | 12.23 GiB| 27.32 B | CPU | 6 | pp32 | 0.45 ± 0.01 |
pp32(处理 32 个 token 的提示)= 0.45 tok/s。 也就是说,处理一句话的开场白要 71 秒。
生成速度实测 0.10 tok/s —— 出一个字要 10 秒。
原因: 12.2 GiB 权重压在 15.6 GB 内存上,几乎没有余量给 KV cache 和计算缓冲,
内存带宽成为绝对瓶颈(系统负载跑到 10.8,而只有 6 个核)。
和 NPU 对比:NPU 是 7.4 tok/s,快了约 74 倍。
这条路技术含量最高,也最"意难平":所有坎都过了,最后被驱动拦住。
Adreno 740 有 Mesa 的 Turnip 开源驱动,用 Python ctypes 直接调 Vulkan loader 就能探测
(不需要装任何开发头文件):
import ctypes
vk = ctypes.CDLL("libvulkan.so.1")
# ... 构造 VkInstanceCreateInfo,调 vkCreateInstance
# ... 调 vkEnumeratePhysicalDevices + vkGetPhysicalDeviceProperties
结果:
vkCreateInstance rc = 0 (0=OK)
physical device count = 2
[0] Adreno (TM) 740 type=1 vendor=0x5143 api=1.1.128
[1] llvmpipe (LLVM 15.0.7, 128bit) type=4 vendor=0x10005
Adreno 740 被成功枚举(type=1 是独显,虽然是集成的)。
GPU 设备节点也都能打开:/dev/kgsl-3d0、/dev/dri/renderD128 权限都是 666。
准备装编译依赖时,所有 apt-get install 都失败:
E: Unmet dependencies. Try 'apt --fix-broken install'
根因有两层,挖出来是这样:
kmod 和 mod-blacklist 抢同一个文件 /etc/modprobe.d/blacklist.conf。mod-blacklist 是 AidLux 平台包,dpkg 不让 kmod 覆盖它。libkmod2 卡在 **iU(半装)**状态,libc-bin 也 it(半配置),修复(注意 --force-overwrite 是必需的):
S() { echo aidlux | sudo -S "$@"; } # 免交互 sudo
# 1) 备份冲突文件
S cp -a /etc/modprobe.d/blacklist.conf /etc/modprobe.d/blacklist.conf.aidlux-bak
# 2) 强制覆盖安装 kmod
S dpkg --force-overwrite -i /var/cache/apt/archives/kmod_29-1ubuntu1.1_arm64.deb
# 3) 配置所有半装包(conffile 提示选"保留旧文件")
S dpkg --configure -a --force-confold
# 4) 修依赖
S apt-get --fix-broken install -y
修完之后 dpkg -l | grep -v '^ii' 就干净了。
glslc 装不上llama.cpp 的 Vulkan 后端需要 glslc(shaderc 的着色器编译器)把 GLSL 编成 SPIR-V。
Ubuntu 的 universe 仓库里确实有 glslc,但 jammy 的索引里没有(universe 没启用)。
去华为云镜像的 pool 里能翻到 deb,然而:
glslc: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found
glslc: /lib/aarch64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.32' not found
镜像上只有 2024.4 版本,它需要 glibc 2.38;而系统是 glibc 2.35。死路。
解法:自己写一个 glslc 包装脚本,把参数翻译给 glslangValidator
(这个工具系统里有,jammy 自带)。核心是三个转换:
#!/bin/bash
# glslc → glslangValidator 参数翻译器
# 1) -fshader-stage=compute → glslang 靠文件扩展名判断阶段,复制成 shader.comp
# 2) --target-env=vulkan1.2 → glslang 用空格分隔: --target-env vulkan1.2
# 3) -O / -MD -MF → glslang 不认,忽略/自己生成 .d 文件
还有个隐蔽的坑:llama.cpp 的 vulkan-shaders-gen 把"stderr 非空"当作编译失败。
而 glslangValidator 在成功时也会往 stdout 打印输入文件路径(被合并到 stderr)。
所以包装器必须"成功时保持安静":
OUTPUT="$(glslangValidator "${ARGS[@]}" "$WORK/shader.$SUF" 2>&1)"
RC=$?
if [ $RC -ne 0 ] || [ ! -s "$OUT" ]; then
echo "$OUTPUT" >&2 # 真失败才说话
exit $RC
fi
# 成功:只透出真实警告,过滤掉回显的路径行
另外两个必须处理的细节:
#include 支持:glslc 默认开 GL_GOOGLE_include_directive,glslangValidator 不开。-E 参数不能和链接模式同时用(会报 can't use -E when linking is selected)。#version 后面注入一行#extension GL_GOOGLE_include_directive : require。#include,所以这步是必须的。vulkan-shaders-gen 不传 -I,靠 glslc 按"当前工作目录"解析。-I<源文件目录>(注意 glslang 要求 -I 和路径紧贴,不能有空格)。编译到一半报一堆符号找不到:
error: 'PFN_vkGetDeviceFaultInfoEXT' does not name a type
error: 'LayerSettingEXT' is not a member of 'vk'
error: 'eMesaDozen' is not a member of 'vk::DriverId'
系统 libvulkan-dev 是 1.3.204(2022 年的),llama.cpp 需要更新的头文件。
解法:克隆上游头文件,用 CMake 变量覆盖:
git clone --depth 1 https://github.com/KhronosGroup/Vulkan-Headers.git
cmake -S llama.cpp -B build-vk -DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=ON \
-DVulkan_GLSLC_EXECUTABLE="$HOME/qwen38/bin/glslc" \
-DVulkan_INCLUDE_DIR="$HOME/qwen38/src/Vulkan-Headers/include" \
-DCMAKE_CXX_FLAGS="-I$HOME/qwen38/src/Vulkan-Headers/include" \
...
好消息: 编译完全成功 ——
Generate vulkan shaders for xxx.comp × 153
cannot compile count: 0
error count: 0
而且识别到了 GPU:
Available devices:
Vulkan0: Adreno (TM) 740 (15514 MiB, 15514 MiB free)
坏消息: 一加载 12.5 GB 权重就 SIGSEGV:
Loading model... |/-\|/
Segmentation fault
连 -ngl 0(完全不用 GPU,纯 CPU 回退)都崩 —— 说明问题出在
Vulkan 后端初始化阶段,而不是"显存不够"。
结论:Turnip 23.2.1 驱动在这个负载下不稳定。 不是参数问题,是驱动本身的问题,
继续调参没有意义。这条路到此为止。
折腾一圈,磁盘会悄悄鼓起来。我这次:
# 删掉 /sdcard 上的冗余副本(解压目录 + tar 包,约 9.6 GB)
rm -rf /sdcard/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550
rm -f /sdcard/qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550.tar.gz
清理前: 61G 已用 / 18G 可用 (78%)
清理后: 51G 已用 / 28G 可用 (65%)
清理原则:保留"能重建运行环境的最小集合"。 我保留了:
/opt/.../res/models/qwen3-8b-...-qcs8550/ —— 运行中的模型(7.7 GB)/opt/.../res/models/qwen3-8b-...-qcs8550.tar.gz —— 原始备份(4.8 GB,万一解压坏了能重来)~/qwen38/models/Qwen3.8-27B-UD-Q3_K_XL.gguf —— CPU 版 27B(12.5 GB)如果确认不再需要 27B 的 CPU 实验,删掉
~/qwen38/models/能再省 12.5 GB。
注意一个反直觉的点: /sdcard 和 / 是同一个分区(都是 userdata,通过 /dev/fuse 挂载)。
所以 df 看哪个都一样,在 /sdcard 删文件同样能给根分区腾地方。
① 先探硬件,再选模型。
我最开始是从"用户要 Qwen3.8-27B"出发的,一路做到了 CPU 能跑、GPU 能编译。
但如果第一步就去查 Model Farm 支持什么,会立刻发现 NPU 上根本没有 27B,
可以省下大量时间。硬件约束不是障碍,是需求的一部分。
② 报错要挖到根因,别绕。apt 报 Unmet dependencies 是表象,真因是 kmod 的文件冲突。glslc 装不上是表象,真因是 glibc 版本。lld 找不到符号是表象,真因是头文件版本。
每一个"换个方法试试"之前,先花两分钟看看根因是什么。
③ 分层定位,一次改一个变量。
GPU 崩了之后,我没有瞎调参数,而是先跑 -ngl 0(纯 CPU 回退)。
如果 -ngl 0 也崩 → 问题在设备初始化,与显存无关。
这一个实验就把排查范围缩小了一半。
设备上(~/qwen38/):
| 文件 | 作用 |
|---|---|
start_npu_api.sh | NPU 服务启动脚本(含配置自检) |
bench_npu.sh | NPU 性能基准 |
test_npu_chat.sh | NPU 对话测试 |
src/llama.cpp/build-cpu/ | CPU 版 llama.cpp |
src/llama.cpp/build-vk/ | Vulkan 版(能枚举 Adreno,但崩溃) |
bin/glslc | glslc→glslangValidator 包装器 |
models/ | Qwen3.8-27B GGUF |
本机(~/Aidlux/):
| 文件 | 作用 |
|---|---|
run_ssh.py / ssh_upload.py / ssh_fire.py | 免 pty 的 SSH 工具箱 |
scripts/npu_chat.py | NPU 对话客户端 |
scripts/glslc_wrapper.sh | 上面那个包装器(可复用) |
scripts/build_vulkan2.sh | Vulkan 构建脚本 |
README.md | 部署报告 |
askpass.sh | ⚠️ 含明文密码,用完请删 |
# ── 连上设备(本机) ──────────────────────────────
python3 run_ssh.py 10.174.211.94 aidlux aidlux '命令'
# ── NPU 服务 ────────────────────────────────────
aidllm start api # 启动
aidllm status api # 状态
aidllm stop api # 停止
~/qwen38/start_npu_api.sh # 一键启动(带配置自检)
# ── 健康检查 ────────────────────────────────────
curl -s http://127.0.0.1:8888/v1/models
# ── 对话 ────────────────────────────────────────
curl -s http://127.0.0.1:8888/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3-8b-cl8192-qnn2.36-w4a16-qcs8550",
"messages":[{"role":"user","content":"你好"}],"max_tokens":128}'
# ── 确认在用 NPU ────────────────────────────────
P=$(pgrep -f "\./api" | head -1)
grep -aoE "libQnn[A-Za-z0-9]*\.so" /proc/$P/maps | sort -u # 看到 libQnnHtp.so 就对了
ls -l /proc/$P/fd | grep -c adsprpc # >0 即通道已开
# ── 查看可用 NPU 模型 ───────────────────────────
aidllm remote-list api
aidllm list api # 本地已装的
aidllm pull api aplux/<模型名>
# ── 性能基准 ────────────────────────────────────
~/qwen38/bench_npu.sh
# ── 磁盘 ────────────────────────────────────────
df -h / # / 和 /sdcard 是同一分区
du -sh /opt/aidlux/app/aid-openai-api/res/models/*
在阿加犀 X1 上,别跟 27B 较劲。
用aidllm pull拉一个官方 QNN 模型,把--device dsp打开,
7.4 tok/s 的 NPU 推理就到手了 —— 比 CPU 快 74 倍。
GGUF 和 Vulkan 那条路能走通编译,但在这块板子上跑不动大模型。