分类: 技术笔记

记录技术学习与实践的点滴

  • Tesla V100 32G + llama.cpp 本地部署 Qwen3.6 27B:从驱动到 138K 长上下文的完整方案

    Tesla V100 32G + llama.cpp 本地部署 Qwen3.6 27B:从驱动到 138K 长上下文的完整方案

    这篇记录 Qwen3.6 27B 大模型全本地跑通的完整过程。目标一句话:27B 参数模型,全本地,长上下文,暴露 OpenAI 兼容 API,任何前端(包括 DeepSeek Harness 本身)直接对接。

    最终结果:

    • Qwen3.6-27B UD-Q5_K_XL(18.95 GB)+ 138K 上下文 + KV 缓存量化 → 32GB 显存刚好装下
    • 同一条流水线后来延伸到 Qwen3.8-27B:Q4_K_XL + MTP 投机解码 + 256K 上下文,实测占用 26077 / 32768 MiB

    下面就是完整搭建流程。所有参数都来自实际在跑的脚本,不是理论最优值。

    一、硬件与驱动

    实际值
    GPU Tesla V100-PCIE-32GB(TCC 模式)
    驱动 573.96(Data Center Tesla 桌面版)
    CUDA 12.8(cuda_12.8.0_571.96_windows)
    系统 Windows 10/11

    两个坑:

    1. V100 不能用 GeForce 消费级驱动。 要用数据中心分支,安装包文件名里带 data-center-tesla(我本地留了份:573.96-data-center-tesla-desktop-win10-win11-64bit-dch-international.exe)。

    2. TCC 模式没有桌面显示输出。 V100 是纯计算卡,看屏幕靠核显,状态全靠 nvidia-smi。写这篇时的实况(节选):

    | 0  Tesla V100-PCIE-32GB   TCC    75C    51W / 250W
    | 26077MiB / 32768MiB
    | 进程: llama-server.exe  26066MiB
    

    二、编译 CUDA 版 llama.cpp

    我保留两套 llama.cpp:

    • F:\AI\llama.cpp-CUDA\ — CUDA 后端(给 V100)
    • F:\AI\llama.cpp\ — Vulkan 后端(更早的 AMD RX 590 8G 在用)

    CUDA 版用 VS2022(MSVC 19.44)+ CUDA 12.8 工具链编译,当前在产 build:

    version: 0.1.0-dev (build 10415, commit 1d2869c6e)
    built with MSVC 19.44.35228.0 for x64
    

    CMake 关键参数:

    cmake -B build -G "Visual Studio 17 2022" -A x64 ^
      -DGGML_CUDA=ON ^
      -DCMAKE_BUILD_TYPE=Release
    cmake --build build --config Release -j
    

    所有启动脚本都会先检查 Release\llama-server.exe 是否存在,缺了就直接提示先编译,避免"启动失败但不知道为什么"。

    三、模型选择:27B 为什么"装得下"

    手头的模型文件(实测大小):

    模型 大小 备注
    Qwen3.6-27B-Q4_K_M.gguf 15.93 GB 常规 K 量化
    Qwen3.6-27B-UD-Q5_K_XL.gguf 18.95 GB Unsloth 动态量化(本文主角)
    Qwen3.8-27B-UD-Q4_K_XL.gguf 16.69 GB 当前生产主模型
    Qwen3.8-27B-Q5_K_M.gguf 18.47 GB
    Qwen3.8-27B-UD-Q6_K_XL.gguf 23.56 GB 高质量档(偏重)
    mtp-Qwen3.8-27B-Q4_0.gguf 1.28 GB MTP 投机解码草稿

    3.6 为什么选 UD-Q5_K_XL:UD(Unsloth Dynamic)量化用 imatrix 校准数据动态分配 bit,关键张量多给、不重要的少给,同等名义 5 bit 下质量比固定 Q5_K_M 好。imatrix 校准文件(imatrix_unsloth.gguf)就放在模型目录里。

    但 27B 长上下文能上 32G 的关键不是量化,是架构。 我写了个小脚本解析 GGUF v3 元数据(解析器在 novel-project/tools/gguf-meta.py,顺带解决了 v3 格式 KV 区紧跟文件头、字符串长度用 u64 编码的坑),结果有点意外:

    general.architecture = qwen35
    qwen35.block_count = 65
    qwen35.full_attention_interval = 4      # 每 4 层才 1 层全注意力
    qwen35.attention.head_count_kv = 4      # GQA,4 个 KV 头
    qwen35.attention.key_length = 256       # head 维度
    qwen35.context_length = 262144          # 原生 256K
    qwen35.nextn_predict_layers = 1         # 内置 MTP 层
    qwen35.ssm.state_size = 128             # 线性注意力/SSM 状态
    

    也就是说 Qwen3.x-27B 系列是混合架构:65 个 block 里只有 1/4 是全注意力(约 17 层),其余是线性注意力/SSM,状态大小固定、不随上下文增长。KV 缓存只给那 17 层全注意力层分配——这是 256K 上下文能塞进 32GB 的根本原因。

    四、显存预算(全部真实数字)

    KV 缓存容量公式(q4_0 量化约 4.5 bit/元素,0.5625 B/元素):

    KV = 2(K+V) × ctx × 4(KV头) × 256(head维) × 0.5625B × 17(全注意力层)
    

    256K 代入:单层 KV ≈ 288 MiB,17 层合计 ≈ 4.8 GB;若不做 KV 量化(fp16),同样 256K 要 17 GB

    Q3.8 Q4_K_XL @256K(实测) Q3.6 Q5_K_XL @138K(估算)
    模型权重 16.69 GB 18.95 GB
    KV 缓存(q4_0) ≈4.8 GB ≈2.6 GB
    SSM 状态 + 激活 + 计算缓冲 ≈4.0 GB(实测余量) ≈4.0 GB(参照)
    合计 ≈25.5 GB → 实测 26077 MiB ≈25.6 GB,还能开 –parallel 2

    实测与估算吻合,这套预算方法是可信的。两个结论:

    1. --flash-attn on + --cache-type-k q4_0 --cache-type-v q4_0 是长上下文的两个必选项,缺一个 256K 都悬。
    2. Q6 + 256K 不成立:光权重就 23.56 GB,我的 Q6 脚本 batch 只能压到 256。要质量降上下文,要上下文降量化,二选一。

    五、启动脚本(真实演进过程)

    早期是 Vulkan 时代(RX 590 8G),部分层卸载到 GPU:

    :: 历史配置:RX 590 8G Vulkan 版
    llama-server -m Qwen3.6-27B-Q4_0.gguf -ngl 24 --n-cpu-moe 999 ^
      -c 4096 -b 1024 -ub 512 -t 6 --device Vulkan0 --cache-ram 2048 --mlock
    

    -ngl 24 只把 24 层推 GPU,其余吃 CPU;--n-cpu-moe 999 是 35B-A3B MoE 模型时代抄过来的,对稠密 27B 实际不生效——无害但误导,后来删掉了。

    V100 到位后配置重写:99 层全上 GPU,上下文从 4K 拉到 138K

    :: start_Qwen3.6-27b-Q5-MTP_v01.bat(核心部分)
    set MODEL_PATH=F:\AI\models\Qwen3.6-27B-UD-Q5_K_XL.gguf
    set CTX_LEN=141072
    
    Release\llama-server.exe -m "%MODEL_PATH%" ^
      -ngl 99 -c 141072 -b 512 -t 8 ^
      --host 0.0.0.0 --port 8080 ^
      --flash-attn on --cache-type-k q4_0 --cache-type-v q4_0 ^
      --parallel 2 --mlock --metrics
    

    参数解释:

    • -ngl 99:全部层上 GPU(32G 够放,不用抠)
    • -c 141072:算过能装下的上下文长度(138K)。Q5 权重比 Q4 重约 2GB,256K 装不下
    • --parallel 2:2 个并发请求槽位(KV 缓存翻倍,预算表里已留了余量)
    • --mlock:锁定内存防换页
    • -t 8:CPU 线程数,服务残留的 CPU 部分

    注意 141072 不是整数规格——是"算到刚好装下"的结果,不是官方档位。

    当前生产版已升级到 Qwen3.8-27B,并加了 MTP 投机解码

    :: 当前生产配置
    Release\llama-server.exe -m "D:\Models\Qwen3.8-27B\Qwen3.8-27B-UD-Q4_K_XL.gguf" ^
      --model-draft "D:\Models\Qwen3.8-27B\mtp-Qwen3.8-27B-Q4_0.gguf" ^
      -ngl 99 -np 1 -c 262144 -b 512 -t 8 ^
      --host 0.0.0.0 --port 8080 ^
      --flash-attn on --cache-type-k q4_0 --cache-type-v q4_0 ^
      --spec-type draft-mtp --spec-draft-n-max 2
    

    元数据里 nextn_predict_layers = 1 说明模型自带 1 层 MTP(Multi-Token Prediction),3.8 配套了独立的 1.28GB Q4_0 草稿文件。--spec-draft-n-max 2 让草稿层一次多预测 2 个 token、主模型一次性验证,中文文本接受率不低,白捡的速度。注意 Q3.6 配置里没有这一项(3.6 没有独立 MTP 草稿文件),别照抄。

    六、前端接入

    llama-server 原生 OpenAI 兼容:

    • API:http://127.0.0.1:8080/v1(chat/completions、models 都通)
    • Web:http://127.0.0.1:8080 自带 playground

    对 DeepSeek Harness(运行本文的工具),把模型服务指向 8080 即可:

    :: D:\Dev\start-harness.bat(简化)
    dsh web --port 3080
    

    然后打开 http://127.0.0.1:3080。现在正在服务这段对话的模型,就是本文这台 V100 上跑的——某种意义上是自我验证。

    服务监听 0.0.0.0,内网其他机器也能用;如果机器走 VPN 或暴露公网,记得加认证和限流(llama-server 没有内置鉴权,建议套一层反代)。

    七、踩坑清单

    1. V100 要 Data Center 驱动,GeForce 驱动装不上/不识别。
    2. Q6 + 256K 直接别想,光权重 23.56GB。
    3. KV 缓存量化是长上下文的必选项:q4_0 把 256K 的 KV 从 17GB 压到约 4.8GB。
    4. --n-cpu-moe 对稠密模型无效,MoE 时代的残留,删掉免得误导。
    5. MTP 要配套草稿文件:3.6 没有 mtp 文件,--spec-type draft-mtp 加了也白加。
    6. 别重复起服务:8080 端口被 llama-server 占着,脚本里有存在性检查,手动启动前先 nvidia-smi 看一眼。

    八、速查表

    场景 推荐组合
    质量优先 Q3.8 UD-Q6_K_XL + ctx 32K~64K
    均衡(3.6 主选) Q3.6 UD-Q5_K_XL + ctx 138K + –parallel 2
    长上下文优先 Q3.8 UD-Q4_K_XL + ctx 256K + MTP 投机解码
    小模型快速验证 Qwen3.5-9B-Q4_K_M(5.29 GB,8G 显存即可)

    脚本、模型都在这台机器上,GGUF 元数据解析器在 novel-project/tools/gguf-meta.py。下一篇想写 imatrix 校准与 UD 量化的制作流程。

  • 三种方式登录 WordPress REST API:Basic Auth、Application Password 与 Cookie Nonce

    三种方式登录 WordPress REST API:Basic Auth、Application Password 与 Cookie Nonce

    上周我想写个脚本把小说章节批量发布到博客,第一道坎就是认证:怎么让脚本以「我」的身份登录?WordPress REST API 官方支持好几种认证方式,但在我的站点(WordPress 7.1 + OpenResty + PHP 8.3,纯 HTTP 无 HTTPS)上实测,只有一种走得通。这篇文章记录完整过程、对比和最终方案。

    一、Basic Auth:看起来最省事,实测 401

    最直观的方式:把用户名密码放进 HTTP 的 Authorization 头。

    curl -u 用户名:密码 \
      http://你的站点/wp-json/wp/v2/users/me
    

    很多教程说这种方式在非 SSL 站点可用,于是我先试了它。结果是 401:

    {
      "code": "rest_not_logged_in",
      "message": "您目前没有登录。",
      "data": { "status": 401 }
    }
    

    我排除了「密码里特殊字符」这个变量——手工把 Base64 头拼出来再发一次,还是 401。这个错误是 WordPress REST 的标准错误结构,说明请求确实到了 WordPress 核心,只是认证没被接受。结论:我的站点当前环境不接受这种认证方式,可能是宿主反向代理或安全策略拦了 Authorization 头,也可能是这套核心/插件组合没有启用它。原因不重要,重要的是生产流程不要依赖它。

    二、Application Password:官方推荐,但要求 HTTPS

    这是 WordPress 团队最希望你用的方式:

    1. 后台 → 用户 → 个人资料
    2. 拉到最下面「应用程序密码」一节
    3. 输入一个名字(比如 publish-script),点创建,会得到一串 xxxx xxxx xxxx xxxx xxxx xxxx 格式的密码

    用法和 Basic Auth 一样,只是密码换成它:

    curl -u 用户名:xxxx-xxxx-xxxx-xxxx-xxxx \
      https://你的站点/wp-json/wp/v2/posts
    

    好处很实在:它是独立凭证,可随时吊销,不会暴露你的登录密码,官方文档也把它作为首选。但有一个硬条件:只在 HTTPS 下生效。我的博客目前是纯 IP + HTTP 访问,建了也用不了。所以这一条被我记进了待办:等域名和证书下来,认证整体迁过去。

    三、Cookie + X-WP-Nonce:最终采用的方案

    第三种方式的原理和浏览器一致:先用登录表单拿到会话 Cookie,之后每次 REST 请求带上 Cookie 加一个时效性 nonce。完整流程四步:

    1. GET /wp-login.php —— 这一步最容易被忽略。WordPress 在这个页面会种一个 test cookie,跳过它直接 POST,会收到「Cookies 被阻止或者您的浏览器不支持」
    2. POST /wp-login.php(提交 log、pwd、testcookie)——成功后拿到 wordpress_logged_in_* 会话 Cookie
    3. GET /wp-admin/post-new.php(任意加载了区块编辑器的后台页面)——页面源码里有一个 wpApiSettings JS 对象,其中的 "nonce" 字段就是需要的 X-WP-Nonce
    4. 每次 REST 请求带上 Cookie + X-WP-Nonce

    PowerShell 核心实现(.NET HttpClient 会自动管理 Cookie 容器):

    $handler = New-Object System.Net.Http.HttpClientHandler
    $client  = New-Object System.Net.Http.HttpClient($handler)
    
    # 1) 先 GET 登录页(拿 test cookie)
    $null = $client.GetAsync('http://site/wp-login.php').GetAwaiter().GetResult()
    
    # 2) POST 登录
    $body = 'log=user&pwd=' + [uri]::EscapeDataString($pass) + '&testcookie=1'
    $content = New-Object System.Net.Http.StringContent(
        $body, [System.Text.Encoding]::UTF8, 'application/x-www-form-urlencoded')
    $null = $client.PostAsync('http://site/wp-login.php', $content).GetAwaiter().GetResult()
    
    # 3) 从后台页面取 nonce
    $admin = $client.GetAsync('http://site/wp-admin/post-new.php').GetAwaiter().GetResult()
    $html  = $admin.Content.ReadAsStringAsync().GetAwaiter().GetResult()
    $nonce = ([regex]::Match($html, '"nonce"\s*:\s*"([a-f0-9]+)"')).Groups[1].Value
    
    # 4) 调 API
    $req = [System.Net.Http.HttpRequestMessage]::new('GET', 'http://site/wp-json/wp/v2/users/me')
    $req.Headers.Add('X-WP-Nonce', $nonce)
    $me = $client.SendAsync($req).GetAwaiter().GetResult()
    

    两个注意点:

    • nonce 有时效窗口。默认 tick 是 15 分钟,一个 nonce 在「当前 + 上一」两个窗口内(约 30 分钟)可重复使用。批量任务很快跑完,一次 nonce 够用;长任务要在过期前重新取一次
    • 如果用 PowerShell 的 Invoke-WebRequest -WebSession,注意 Cookie 集合解析可能不稳定(我实测拿到过名字、值全空的 Cookie)。换成 .NET HttpClient 自带的 Cookie 容器更稳

    四、三种方式对比

    方式 需要 HTTPS 脚本友好度 凭证生命周期 在我站的结论
    Application Password 可长期,随时吊销 首选(等证书)
    Cookie + Nonce nonce 约 30 分钟,cookie 按设置 当前在用
    Basic Auth 理论不需要 实测 401,不可用

    五、结论

    • 站点有 HTTPS:直接 Application Password。凭证可吊销、不暴露登录密码,官方推荐
    • 像我这样纯 HTTP、本机自动化:Cookie + Nonce 可行。本质是把浏览器登录过程脚本化
    • Basic Auth:当调试时的快捷手段用,别在正式流程里赌它

    完整的发布脚本在下一篇(开发实战分类)讲:内容清洗、幂等设计,以及路上踩的坑。

  • 用 AI 写作的半年:几个真实的变化

    用 AI 写作的半年:几个真实的变化

    过去半年,我把 AI 当成了写作搭子:先让它帮我列提纲,再让它起草稿,最后再自己改。用了这么久,还是想认真聊聊几个真实的感受。

    一、空白页恐惧症,治好了大半
    以前坐下来写东西,最难受的就是开头。现在我会先让 AI 列 5 到 10 个角度,里面只要有一个打动我,我就顺着写。写文章最难的那一步——想清楚从哪下笔——五分钟就解决了。

    二、初稿变快了,但我的声音在消失
    用得越多,越发现自己说话开始”模板化”。每一段都讲究结构,每一个过渡都很丝滑,可读完之后,找不到一句只有我才会写的话。所以我现在给自己定了个规矩:初稿改完,必须把最关键的两三句用完全自己的话重写一遍,哪怕写得更”笨”一点。

    三、AI 擅长”结构”,不擅长”观点”
    一篇真正有价值的文章,核心还是我自己的经历、自己的判断。AI 能做的是帮我把这些想法整理清楚、表达完整。它像一个很厉害的编辑,能帮你调骨架,但给不了你灵魂。

    半年的结论就一句话:AI 是很好的工具,但不能当作者。工具只会越来越强,但敢不敢表达、有没有自己的观察,这件事永远只能自己来。

  • Python 异步编程入门:从 async/await 说起

    Python 异步编程入门:从 async/await 说起

    异步编程是现代编程中不可或缺的一部分。Python 3.5 引入的 async/await 语法让异步代码写起来像同步代码一样直观。

    (更多…)

  • 用 WordPress 搭建个人博客的完整指南

    用 WordPress 搭建个人博客的完整指南

    搭建一个个人博客从来没有这么简单过。本文将分享使用 WordPress 从零开始搭建个人博客的完整流程。

    (更多…)