手把手:在阿加犀 X1 上把QWEN开源8B大模型跑起来(NPU 实战笔记)

AKATSUKIKAZE 2026-09-16 20:04:25

这篇文档记录了在一个"AidLux X1 盒子"上部署大模型的完整过程。
目标模型原本定的是 Qwen3.8-27B,中途根据硬件实情转向 NPU + Qwen3-8B
所有命令都是实际敲过、实际跑通的,不是抄文档。
本文由agent辅助完成


0. 先说:这台机器到底能跑什么

在动手之前,先把"能"和"不能"说清楚,省得你白折腾一整天(我替你折腾过了)。

路线模型速度结论
NPU(Hexagon)Qwen3-8B w4a167.4 tok/s就用这个
CPU(llama.cpp)Qwen3.8-27B Q3_K0.10 tok/s⚠️ 能跑,但慢到没法用
GPU(Vulkan/Adreno 740)Qwen3.8-27B Q3_K❌ 驱动直接崩

三个必须记住的硬事实:

  1. NPU 只认"官方预编译"的模型。 它不能吃 GGUF,也不能吃 HuggingFace 的 safetensors。
    你得用阿加犀 Model Farm 上已经转换、量化、编译好的 QNN 格式模型。
  2. Model Farm 上最大的 Qwen 就是 8B。 没有 14B、没有 27B、更没有 Qwen3.8 系列。
  3. 27B 这台机器谁也扛不住。 15.6 GB 统一内存,27B 量化到 4bit 也要 14–16 GB 权重,
    再加上 KV cache 和运行时开销 —— 数学上就不成立。

所以最终方案:NPU 跑 Qwen3-8B,8K 上下文,7.4 tok/s。
下面的文档会告诉你每一步怎么走,以及为什么要这么走。


1. 认识你的机器

1.1 设备名片

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(折仅代表笔者手上的开发版状况)

1.2 第一件事:能用 SSH 连上

听起来是废话,但这一步就有坑。

坑:沙箱里没有 pty。 sshpassexpect 这类靠伪终端的自动登录工具会直接报
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。

1.3 上传脚本:别用 heredoc

一开始我想当然地写了 ssh ... 'cat > f.py' < localfile,结果远端文件是空的 —— 因为
subprocessstdin 被设成了 DEVNULL。改成 input=data 显式喂进去就好了。

小教训:自动化里用 heredoc 传文件,是自找麻烦。用 stdin 管道。

1.4 注意:远端后台任务会把 SSH 挂住

ssh host 'nohup ./long.sh &' 这种写法,SSH 会话不会退出,一直等到子进程结束(或超时)。
正确姿势:

nohup ./long.sh > log.txt 2>&1 < /dev/null &

三个重定向都要:> log 收输出、2>&1 收错误、< /dev/null 断开标准输入。
另外把"启动"和"查看"分成两次 SSH 调用,别在一个会话里等。


2. 场景 A:NPU 路线(最终采用)

2.1 先搞清楚 NPU 的"食物"是什么

这是最容易走弯路的认知点:

  • CPU/GPU 路线吃通用格式(GGUF、safetensors),你自己转换、自己量化,灵活。
  • NPU 路线吃的是厂商用 QNN SDK 编译出来的产物:一堆 .serialized.bin.aidem
    分片文件 + 一个描述文件。你没法自己把 GGUF 塞进去。

所以上 NPU 的前提是:Model Farm 上得有你要的模型的现成包

2.2 查有没有你要的模型

设备自带一个命令行工具 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)

2.3 检查 SDK 是否就绪

好消息: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

2.4 拿模型

两种方式。

方式一:用 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 &

2.5 模型放哪儿?看描述文件

打开那个模型 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 量化格式,解压后会膨胀。

2.6 修配置:剔除"幽灵模型"

看一眼服务配置 /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

2.7 启动服务

这是最关键的一条命令,有两个细节容易漏:

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.jsonhttp_cfg 里把 ip 改成 127.0.0.1,或用防火墙挡掉 8888 端口。

2.8 怎么确认它真的在用 NPU?

别光信 --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.solibQnnHtpV73Stub.so,就说明 NPU 后端已经装载了。

证据二:通过 /dev/adsprpc 与 DSP 通信。 这是高通 DSP 的远程调用通道:

ls -l /proc/$P/fd | grep -c adsprpc

⚠️ 这个数字会变:空闲时常驻 3 个句柄,推理时我见过涨到 100。
所以别拿它当固定基准,> 0 就说明通道开着。
(CPU 版的 llama.cpp 进程则完全没有 libQnnHtp,也没有 adsprpc 句柄 —— 对照很明显。)

2.9 跑一次真实推理

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 里要求不要思考。

2.10 性能基准

用同一个接口,改 max_tokens 测不同负载(脚本:~/qwen38/bench_npu.sh):

场景promptcompletion耗时解码速度
短问答1018424.7 s7.45 tok/s
长文生成2370093.3 s7.51 tok/s
长提示(616 tok)61652170.8 s7.36 tok/s(综合 16.1)

怎么读这组数:

  • 解码稳定在 7.4 tok/s,跟生成长度基本无关 —— 说明是稳态的 NPU 推理,没有内存抖动。
  • 提示处理约 145 tok/s(从第三个场景反推:616 token 的提示只花了很少时间)。
  • 综合吞吐能到 16 tok/s,是因为长提示场景下"预填充"很快,拉高了平均值。

2.11 从你的电脑上调它

两种方式。

方式一:脚本(懒人推荐)

我这里写了个 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 就行。


3. 场景 B:CPU 路线(llama.cpp)—— 能跑,但你要有耐心

这条路是为 27B 准备的。虽然最后没用它上线,但过程值得记下来,因为坑比较典型。

3.1 编译 llama.cpp

设备有 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-clillama-completionllama-benchllama-server

3.2 27B 模型的选型:认准 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_XXS6.9 GB8 GB
UD-Q2_K_XL9.4 GB12 GB
UD-Q3_K_XL12.5 GB16 GB ← 我们选它
UD-Q4_K_M15.7 GB24 GB
UD-Q5_K_M18.9 GB32 GB

UD- 前缀是 Unsloth Dynamic,它会按层动态分配量化精度(重要层用高精度),
同等体积下质量比传统均匀量化更好。在内存受限时优先选 UD 系列。

3.3 三个 CLI 版本的坑

新版 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 "你好,请用一句话介绍你自己。"

3.4 结果:能生成,但慢到不可用

运行时有一堆无害的警告,别被吓到:

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 倍。


4. 场景 C:GPU 路线(Vulkan)—— 编译成功,运行崩溃

这条路技术含量最高,也最"意难平":所有坎都过了,最后被驱动拦住。

4.1 先探测 GPU 到底能不能用

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。

4.2 坎一:apt 状态是坏的

准备装编译依赖时,所有 apt-get install 都失败:

E: Unmet dependencies. Try 'apt --fix-broken install'

根因有两层,挖出来是这样:

  1. kmodmod-blacklist 抢同一个文件 /etc/modprobe.d/blacklist.conf
    mod-blacklist 是 AidLux 平台包,dpkg 不让 kmod 覆盖它。
  2. 结果 libkmod2 卡在 **iU(半装)**状态,libc-binit(半配置),
    于是整个 apt 事务全被阻塞。

修复(注意 --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' 就干净了。

4.3 坎二: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.cppvulkan-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
    llama.cpp 里有 154 个着色器用了 #include,所以这步是必须的。
  • 包含路径:vulkan-shaders-gen 不传 -I,靠 glslc 按"当前工作目录"解析。
    包装器里得自己加上 -I<源文件目录>(注意 glslang 要求 -I 和路径紧贴,不能有空格)。

4.4 坎三:Vulkan 头文件太旧

编译到一半报一堆符号找不到:

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-dev1.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" \
  ...

4.5 结果:编译全过,运行全崩

好消息: 编译完全成功 ——

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 驱动在这个负载下不稳定。 不是参数问题,是驱动本身的问题,
继续调参没有意义。这条路到此为止。


5. 收尾:清理和复盘

5.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 删文件同样能给根分区腾地方。

5.2 复盘:三条经验

① 先探硬件,再选模型。
我最开始是从"用户要 Qwen3.8-27B"出发的,一路做到了 CPU 能跑、GPU 能编译。
但如果第一步就去查 Model Farm 支持什么,会立刻发现 NPU 上根本没有 27B,
可以省下大量时间。硬件约束不是障碍,是需求的一部分。

② 报错要挖到根因,别绕。
aptUnmet dependencies 是表象,真因是 kmod 的文件冲突。
glslc 装不上是表象,真因是 glibc 版本。
lld 找不到符号是表象,真因是头文件版本。
每一个"换个方法试试"之前,先花两分钟看看根因是什么。

③ 分层定位,一次改一个变量。
GPU 崩了之后,我没有瞎调参数,而是先跑 -ngl 0(纯 CPU 回退)。
如果 -ngl 0 也崩 → 问题在设备初始化,与显存无关。
这一个实验就把排查范围缩小了一半。

5.3 最终文件清单

设备上(~/qwen38/):

文件作用
start_npu_api.shNPU 服务启动脚本(含配置自检)
bench_npu.shNPU 性能基准
test_npu_chat.shNPU 对话测试
src/llama.cpp/build-cpu/CPU 版 llama.cpp
src/llama.cpp/build-vk/Vulkan 版(能枚举 Adreno,但崩溃)
bin/glslcglslc→glslangValidator 包装器
models/Qwen3.8-27B GGUF

本机(~/Aidlux/):

文件作用
run_ssh.py / ssh_upload.py / ssh_fire.py免 pty 的 SSH 工具箱
scripts/npu_chat.pyNPU 对话客户端
scripts/glslc_wrapper.sh上面那个包装器(可复用)
scripts/build_vulkan2.shVulkan 构建脚本
README.md部署报告
askpass.sh⚠️ 含明文密码,用完请删

6. 快速命令速查

# ── 连上设备(本机) ──────────────────────────────
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 那条路能走通编译,但在这块板子上跑不动大模型。

...全文
16 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

7,672

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、WoS 、XR、IoT、Auto、生成式AI等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
人工智能物联网机器学习 技术论坛(原bbs) 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧