<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>陈鑫宇</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://u2tt.github.io/img/avatar.jpg</icon>
  <id>https://u2tt.github.io/</id>
  <link href="https://u2tt.github.io/" rel="alternate"/>
  <link href="https://u2tt.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 陈鑫宇</rights>
  <subtitle>Vibe coding/Python/日常记录/生活</subtitle>
  <title>陈鑫宇</title>
  <updated>2026-10-20T07:40:00.000Z</updated>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="blog" scheme="https://u2tt.github.io/tags/blog/"/>
    <category term="随笔" scheme="https://u2tt.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
    <category term="LLM" scheme="https://u2tt.github.io/tags/LLM/"/>
    <content>
      <![CDATA[<p><a href="https://4choor3.github.io/2026/10/15/myself-quantized" title="《量化自己》">上一篇</a>的续作。</p><p>上一篇写的用模型量化自己的教程一塌糊涂，而且量化出来的也不太能够使用，会有严重的胡说八道的现象。<br>大概是处于一个既不是我，又不是Qwen的状态。——反正就是离人很远</p><blockquote><p>Q: 为什么你喜欢我<br>A: 因为我们有共同的语言，能分享生活中的点点滴滴，还有你的支持与陪伴让我感到很温暖。</p><p>Q: 你在做什么<br>A: 我在聊天呢<br>Q: 聊什么<br>A: 没什么，就是随意聊聊<br>Q: 和谁聊<br>A: 你呀</p></blockquote><p>所以又开了一个新的GPU实例，配置了48G显存的RTX4090，这次可以量化的模型参数量可以配置的高一些了。<br>希望能量化出可以使用的模型。</p><table><thead><tr><th style="text-align:center">项目</th><th style="text-align:center">规格详情</th></tr></thead><tbody><tr><td style="text-align:center">GPU</td><td style="text-align:center">48G RTX4090 *1</td></tr><tr><td style="text-align:center">可用区</td><td style="text-align:center">华北二A</td></tr><tr><td style="text-align:center">镜像</td><td style="text-align:center">Ubuntu-nvidia 24.04</td></tr><tr><td style="text-align:center">CPU</td><td style="text-align:center">16核</td></tr><tr><td style="text-align:center">内存</td><td style="text-align:center">96 GB</td></tr><tr><td style="text-align:center">系统盘</td><td style="text-align:center">200 GB</td></tr></tbody></table><p>拿到新系统的第一刻，还是无卡启动进行配置更新，安装conda，git，fnm，安装pytorch，node，cmake……<br>让这个系统到一个可以被使用的地步。</p><h2 id="环境配置"><a class="headerlink" href="#环境配置"></a>环境配置</h2><h3 id="更新系统软件源"><a class="headerlink" href="#更新系统软件源"></a>更新系统软件源</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt upgrade -y</span><br></pre></td></tr></table></figure><h3 id="安装Git"><a class="headerlink" href="#安装Git"></a>安装Git</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install git -y</span><br><span class="line">git --version</span><br></pre></td></tr></table></figure><h3 id="使用Miniforge配置Python-3-11环境"><a class="headerlink" href="#使用Miniforge配置Python-3-11环境"></a>使用Miniforge配置Python 3.11环境</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh</span><br><span class="line">bash Miniforge3-Linux-x86_64.sh</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br><span class="line">conda config --<span class="built_in">set</span> auto_activate_base <span class="literal">false</span></span><br><span class="line">conda create -n llama python=3.11 -y</span><br><span class="line">conda activate llama</span><br><span class="line">python --version</span><br></pre></td></tr></table></figure><p>执行最后一项验证后，终端输出将为<code>Python 3.11.x</code>。Miniforge默认配置<code>conda-forge</code>源并集成<code>mamba</code>，后续进入该环境执行<code>conda activate llama</code>即可。</p><h3 id="安装Node-js"><a class="headerlink" href="#安装Node-js"></a>安装Node.js</h3><p>使用<code>fnm</code>安装并配置Node.js 24</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://fnm.vercel.app/install | bash</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br><span class="line">fnm install 24</span><br><span class="line">fnm use 24</span><br><span class="line">fnm default 24</span><br><span class="line">node --version</span><br><span class="line">npm --version</span><br></pre></td></tr></table></figure><h2 id="安装训练框架LLaMA-Factory"><a class="headerlink" href="#安装训练框架LLaMA-Factory"></a>安装训练框架LLaMA-Factory</h2><p>LLaMA-Factory是一站式微调平台，封装了数据处理、LoRA训练、权重合并与导出全流程。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 激活训练环境</span></span><br><span class="line">conda activate llama</span><br><span class="line"></span><br><span class="line"><span class="comment"># 克隆仓库</span></span><br><span class="line">git <span class="built_in">clone</span> --depth 1 https://github.com/hiyouga/LLaMA-Factory.git</span><br><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">pip install -e <span class="string">&quot;.[torch,metrics]&quot;</span></span><br><span class="line">pip install flash-attn --no-build-isolation</span><br></pre></td></tr></table></figure><p>锁定与框架兼容的依赖版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pip install <span class="string">&quot;huggingface_hub&lt;2.0,&gt;=1.5.0&quot;</span></span><br><span class="line">pip check</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：<code>pip check</code>无冲突输出；执行<code>llamafactory-cli --help</code>能正常显示帮助信息。</p><blockquote><p>说明：huggingface-hub 2.x版本与transformers、gradio、tokenizers存在依赖冲突，锁定在1.x可保证WebUI与训练流程正常。</p></blockquote><h2 id="下载基座模型"><a class="headerlink" href="#下载基座模型"></a>下载基座模型</h2><table><thead><tr><th>维度</th><th>7B</th><th><strong>14B（推荐）</strong></th><th>32B</th></tr></thead><tbody><tr><td>语言风格复刻</td><td>良好</td><td><strong>极佳</strong></td><td>极致</td></tr><tr><td>接梗与潜台词理解</td><td>一般</td><td><strong>敏锐</strong></td><td>极高</td></tr><tr><td>抗「AI腔」漂移</td><td>中等</td><td><strong>强</strong></td><td>极强</td></tr><tr><td>48G训练开销</td><td>约15GB</td><td><strong>约28GB</strong></td><td>需QLoRA</td></tr><tr><td>推理速度</td><td>80+ tok/s</td><td><strong>50~60 tok/s</strong></td><td>20~30 tok/s</td></tr></tbody></table><p><strong>推荐基座：<a href="https://huggingface.co/OpenPipe/Qwen3-14B-Instruct"><code>OpenPipe/Qwen3-14B-Instruct</code></a></strong></p><p><img src="/img/myself-quantized-new/modelshow.png" alt="modelshow"></p><p>该版本已移除Qwen3默认的<code>&lt;think&gt;</code>思维链输出，回归直接问答模式，对微调友好，契合微信聊天中碎片化、短句、快速接梗的表达特征。</p><p><strong>版本选择三条准则</strong></p><ul><li>选择带<code>-Instruct</code>后缀的指令对齐版，不使用<code>-Base</code>预训练底模（<code>Base</code>未做对话对齐，只会续写不会问答）。</li><li>不使用带<code>-Thinking</code>的版本（会先输出大段思维链，破坏随性对话节奏）。</li><li>不使用带<code>-AWQ</code>/<code>-GPTQ</code>的版本（这类是量化推理专用包，不用于训练）。</li></ul><p>模型完整权重约29GB，下载到本地目录，训练时直接指定本地路径。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 国内网络建议启用镜像加速</span></span><br><span class="line"><span class="built_in">export</span> HF_ENDPOINT=https://hf-mirror.com</span><br><span class="line"></span><br><span class="line">hf download OpenPipe/Qwen3-14B-Instruct \</span><br><span class="line">    --local-dir ./models/Qwen3-14B-Instruct</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：进入<code>./models/Qwen3-14B-Instruct</code>目录，确认存在以下文件：</p><ul><li><code>config.json</code></li><li><code>tokenizer.json</code>、<code>tokenizer_config.json</code>、<code>vocab.json</code>、<code>merges.txt</code></li><li><code>model-00001-of-00006.safetensors</code>等多个权重分卷</li><li><code>model.safetensors.index.json</code></li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">(base) ubuntu@10-60-22-134:~/models$ <span class="built_in">ls</span> -alh</span><br><span class="line">total 25G</span><br><span class="line">drwxrwxr-x  2 ubuntu ubuntu 4.0K Oct 17 20:03 .</span><br><span class="line">drwxr-x--- 17 ubuntu ubuntu 4.0K Oct 17 20:06 ..</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu  381 Oct 17 20:03 Modelfile</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu 2.7K Oct 17 20:03 chat_template.jinja</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu 1.7K Oct 17 20:01 config.json</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu  213 Oct 17 20:01 generation_config.json</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.7G Oct 17 20:02 model-00001-of-00006.safetensors</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.6G Oct 17 20:02 model-00002-of-00006.safetensors</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.7G Oct 17 20:02 model-00003-of-00006.safetensors</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.6G Oct 17 20:02 model-00004-of-00006.safetensors</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.7G Oct 17 20:03 model-00005-of-00006.safetensors</span><br><span class="line">-rw-------  1 ubuntu ubuntu 4.5G Oct 17 20:03 model-00006-of-00006.safetensors</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu  36K Oct 17 20:03 model.safetensors.index.json</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu  11M Oct 17 20:03 tokenizer.json</span><br><span class="line">-rw-rw-r--  1 ubuntu ubuntu  719 Oct 17 20:03 tokenizer_config.json</span><br></pre></td></tr></table></figure><blockquote><p>说明：上文4个分词相关文件在后续转换GGUF时必需，请确认目录完整。</p></blockquote><h2 id="微信聊天记录处理"><a class="headerlink" href="#微信聊天记录处理"></a>微信聊天记录处理</h2><h3 id="导出聊天记录"><a class="headerlink" href="#导出聊天记录"></a>导出聊天记录</h3><p>使用<code>WeFlow</code>导出微信私聊记录，得到<code>JSON</code>文件。导出格式说明：</p><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>isSend: 1</code></td><td>你本人发送的消息</td></tr><tr><td><code>isSend: 0</code></td><td>对方发送的消息</td></tr><tr><td><code>type: &quot;文本消息&quot;</code></td><td>纯文本内容</td></tr><tr><td><code>type: &quot;引用消息&quot;</code></td><td>引用他人消息后的回复</td></tr><tr><td><code>createTime</code></td><td>消息时间戳</td></tr></tbody></table><blockquote><p>说明：私聊场景中<code>isSend: 1</code>恒为你本人，与聊天对象无关，因此批量处理多个联系人的逻辑完全通用。</p></blockquote><p>建议导出多个联系人的私聊记录，合并为统一训练集，样本越丰富风格覆盖越完整。</p><h3 id="语料规模建议"><a class="headerlink" href="#语料规模建议"></a>语料规模建议</h3><p>1,000 ~ 5,000条高质量多轮 / 单轮样本，足以让LoRA捕捉稳定的语言偏置。</p><h3 id="转换脚本"><a class="headerlink" href="#转换脚本"></a>转换脚本</h3><p><strong>目的</strong>：把原始导出文件转换为ShareGPT格式的多轮训练数据。核心处理是<strong>时间窗口合并</strong>——把你在连续几分钟内发出的多条短句用换行符拼接成一条完整回复，保留真实的断句节奏。</p><p>创建<code>process_weflow.py</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置待处理的文件列表（WeFlow 导出的各联系人 JSON）</span></span><br><span class="line">INPUT_FILES = [</span><br><span class="line">    <span class="string">&quot;私聊_***-7a31c*****.json&quot;</span>,  <span class="comment"># 示范文件</span></span><br><span class="line">]</span><br><span class="line">OUTPUT_FILE = <span class="string">&quot;persona_sft_dataset_all.json&quot;</span></span><br><span class="line"></span><br><span class="line">TIME_THRESHOLD = <span class="number">180</span>   <span class="comment"># 3 分钟（180 秒）内的连续发言合并为一轮</span></span><br><span class="line">MAX_TURNS = <span class="number">6</span>          <span class="comment"># 每个多轮对话样本包含 6 轮问答（12 条消息）</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">clean_content</span>(<span class="params">msg: <span class="built_in">dict</span></span>) -&gt; <span class="built_in">str</span> | <span class="literal">None</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;清洗单条消息，返回纯文本；无效消息返回 None&quot;&quot;&quot;</span></span><br><span class="line">    msg_type = msg.get(<span class="string">&quot;type&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">    content = msg.get(<span class="string">&quot;content&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 只保留文本消息与引用消息</span></span><br><span class="line">    <span class="keyword">if</span> msg_type <span class="keyword">not</span> <span class="keyword">in</span> [<span class="string">&quot;文本消息&quot;</span>, <span class="string">&quot;引用消息&quot;</span>]:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> content:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 过滤纯占位符</span></span><br><span class="line">    ignore_list = [<span class="string">&quot;[图片]&quot;</span>, <span class="string">&quot;[表情包]&quot;</span>, <span class="string">&quot;[视频]&quot;</span>, <span class="string">&quot;[语音通话]&quot;</span>, <span class="string">&quot;[视频通话]&quot;</span>,</span><br><span class="line">                   <span class="string">&quot;[位置]&quot;</span>, <span class="string">&quot;[名片]&quot;</span>, <span class="string">&quot;[炸弹]&quot;</span>, <span class="string">&quot;[OK]&quot;</span>]</span><br><span class="line">    <span class="keyword">if</span> content.strip() <span class="keyword">in</span> ignore_list:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 剥离引用消息自动附加的后缀标签</span></span><br><span class="line">    content = re.sub(<span class="string">r&quot;\[\s*引用[^\]]*\]&quot;</span>, <span class="string">&quot;&quot;</span>, content)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 清理未配对的右中括号</span></span><br><span class="line">    content = re.sub(<span class="string">r&quot;(?&lt;!\[)\]&quot;</span>, <span class="string">&quot;&quot;</span>, content)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 手机号脱敏</span></span><br><span class="line">    content = re.sub(<span class="string">r&quot;1\d&#123;10&#125;&quot;</span>, <span class="string">&quot;13800000000&quot;</span>, content)</span><br><span class="line"></span><br><span class="line">    content = content.strip()</span><br><span class="line">    <span class="keyword">return</span> content <span class="keyword">if</span> content <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">process_single_file</span>(<span class="params">file_path: <span class="built_in">str</span></span>) -&gt; <span class="built_in">list</span>[<span class="built_in">dict</span>]:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;处理单个聊天文件，返回样本列表&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> Path(file_path).exists():</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;未找到文件: <span class="subst">&#123;file_path&#125;</span>，跳过&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line"></span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(file_path, <span class="string">&quot;r&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">        data = json.load(f)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 步骤 1：筛选清洗出有效文本序列</span></span><br><span class="line">    clean_msgs = []</span><br><span class="line">    <span class="keyword">for</span> m <span class="keyword">in</span> data.get(<span class="string">&quot;messages&quot;</span>, []):</span><br><span class="line">        text = clean_content(m)</span><br><span class="line">        <span class="keyword">if</span> text:</span><br><span class="line">            clean_msgs.append(&#123;</span><br><span class="line">                <span class="string">&quot;isSend&quot;</span>: m.get(<span class="string">&quot;isSend&quot;</span>),</span><br><span class="line">                <span class="string">&quot;time&quot;</span>: m.get(<span class="string">&quot;createTime&quot;</span>, <span class="number">0</span>),</span><br><span class="line">                <span class="string">&quot;text&quot;</span>: text,</span><br><span class="line">            &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> clean_msgs:</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 步骤 2：滑动时间窗口，合并同一角色的连续发言</span></span><br><span class="line">    merged_blocks = []</span><br><span class="line">    current_block = &#123;</span><br><span class="line">        <span class="string">&quot;role&quot;</span>: <span class="string">&quot;gpt&quot;</span> <span class="keyword">if</span> clean_msgs[<span class="number">0</span>][<span class="string">&quot;isSend&quot;</span>] == <span class="number">1</span> <span class="keyword">else</span> <span class="string">&quot;human&quot;</span>,</span><br><span class="line">        <span class="string">&quot;texts&quot;</span>: [clean_msgs[<span class="number">0</span>][<span class="string">&quot;text&quot;</span>]],</span><br><span class="line">        <span class="string">&quot;last_time&quot;</span>: clean_msgs[<span class="number">0</span>][<span class="string">&quot;time&quot;</span>],</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> m <span class="keyword">in</span> clean_msgs[<span class="number">1</span>:]:</span><br><span class="line">        role = <span class="string">&quot;gpt&quot;</span> <span class="keyword">if</span> m[<span class="string">&quot;isSend&quot;</span>] == <span class="number">1</span> <span class="keyword">else</span> <span class="string">&quot;human&quot;</span></span><br><span class="line">        <span class="keyword">if</span> role == current_block[<span class="string">&quot;role&quot;</span>] <span class="keyword">and</span> (m[<span class="string">&quot;time&quot;</span>] - current_block[<span class="string">&quot;last_time&quot;</span>] &lt;= TIME_THRESHOLD):</span><br><span class="line">            current_block[<span class="string">&quot;texts&quot;</span>].append(m[<span class="string">&quot;text&quot;</span>])</span><br><span class="line">            current_block[<span class="string">&quot;last_time&quot;</span>] = m[<span class="string">&quot;time&quot;</span>]</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            merged_blocks.append(current_block)</span><br><span class="line">            current_block = &#123;</span><br><span class="line">                <span class="string">&quot;role&quot;</span>: role,</span><br><span class="line">                <span class="string">&quot;texts&quot;</span>: [m[<span class="string">&quot;text&quot;</span>]],</span><br><span class="line">                <span class="string">&quot;last_time&quot;</span>: m[<span class="string">&quot;time&quot;</span>],</span><br><span class="line">            &#125;</span><br><span class="line">    merged_blocks.append(current_block)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 步骤 3：规范为 human -&gt; gpt 严格交替</span></span><br><span class="line">    dialogue = []</span><br><span class="line">    <span class="keyword">for</span> block <span class="keyword">in</span> merged_blocks:</span><br><span class="line">        content = <span class="string">&quot;\n&quot;</span>.join(block[<span class="string">&quot;texts&quot;</span>])</span><br><span class="line">        <span class="keyword">if</span> block[<span class="string">&quot;role&quot;</span>] == <span class="string">&quot;human&quot;</span>:</span><br><span class="line">            dialogue.append(&#123;<span class="string">&quot;from&quot;</span>: <span class="string">&quot;human&quot;</span>, <span class="string">&quot;value&quot;</span>: content&#125;)</span><br><span class="line">        <span class="keyword">elif</span> block[<span class="string">&quot;role&quot;</span>] == <span class="string">&quot;gpt&quot;</span>:</span><br><span class="line">            <span class="keyword">if</span> dialogue <span class="keyword">and</span> dialogue[-<span class="number">1</span>][<span class="string">&quot;from&quot;</span>] == <span class="string">&quot;human&quot;</span>:</span><br><span class="line">                dialogue.append(&#123;<span class="string">&quot;from&quot;</span>: <span class="string">&quot;gpt&quot;</span>, <span class="string">&quot;value&quot;</span>: content&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 步骤 4：按固定轮次切分为多个样本</span></span><br><span class="line">    samples = []</span><br><span class="line">    temp = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, <span class="built_in">len</span>(dialogue) - <span class="number">1</span>, <span class="number">2</span>):</span><br><span class="line">        <span class="keyword">if</span> dialogue[i][<span class="string">&quot;from&quot;</span>] == <span class="string">&quot;human&quot;</span> <span class="keyword">and</span> dialogue[i + <span class="number">1</span>][<span class="string">&quot;from&quot;</span>] == <span class="string">&quot;gpt&quot;</span>:</span><br><span class="line">            temp.extend([dialogue[i], dialogue[i + <span class="number">1</span>]])</span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">len</span>(temp) &gt;= MAX_TURNS * <span class="number">2</span>:</span><br><span class="line">                samples.append(&#123;<span class="string">&quot;conversations&quot;</span>: temp&#125;)</span><br><span class="line">                temp = []</span><br><span class="line">    <span class="keyword">if</span> temp:</span><br><span class="line">        samples.append(&#123;<span class="string">&quot;conversations&quot;</span>: temp&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> samples</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    total_dataset = []</span><br><span class="line">    <span class="keyword">for</span> f <span class="keyword">in</span> INPUT_FILES:</span><br><span class="line">        samples = process_single_file(f)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;文件 <span class="subst">&#123;f&#125;</span> 生成有效样本: <span class="subst">&#123;<span class="built_in">len</span>(samples)&#125;</span> 组&quot;</span>)</span><br><span class="line">        total_dataset.extend(samples)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(OUTPUT_FILE, <span class="string">&quot;w&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> out:</span><br><span class="line">        json.dump(total_dataset, out, ensure_ascii=<span class="literal">False</span>, indent=<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;\n全部合并完成，总计生成 <span class="subst">&#123;<span class="built_in">len</span>(total_dataset)&#125;</span> 组微调样本，已保存至 <span class="subst">&#123;OUTPUT_FILE&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><h3 id="运行转换"><a class="headerlink" href="#运行转换"></a>运行转换</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python process_weflow.py</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：生成<code>persona_sft_dataset_all.json</code>，结构如下。注意<code>value</code>中的<code>\n</code>完整保留了连发消息的节奏感：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;conversations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;from&quot;</span><span class="punctuation">:</span> <span class="string">&quot;human&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;我早饭是不吃的 午饭是没有的 晚饭是现做的\n能熬到天亮&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;from&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt&quot;</span><span class="punctuation">,</span>   <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;我还好，只有早饭不吃\n但我要89点就起来&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;from&quot;</span><span class="punctuation">:</span> <span class="string">&quot;human&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;毕竟人还没睡醒 就来房间通知我她要出去打麻将了\n午饭是根本不用烧的&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;from&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt&quot;</span><span class="punctuation">,</span>   <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;她这何尝不是一种自律呢？\n那你吃什么？&quot;</span><span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><blockquote><p>说明：原始导出文件通常有几十MB，转换后仅几百KB到几MB，属于正常现象。原始文件包含大量渲染用元数据（内部ID、XML签名、头像CDN链接、表情下载URL等），且大量表情包与图片消息已被过滤，连续短句也被合并压缩。</p></blockquote><h3 id="清洗与隐私脱敏"><a class="headerlink" href="#清洗与隐私脱敏"></a>清洗与隐私脱敏</h3><p><strong>目的</strong>：剔除噪音与敏感信息。训练数据中的隐私会被模型记忆并在外部对话中复述，脱敏是必经步骤。</p><p>需处理的内容：</p><ul><li>表情标签未闭合（如<code>[捂脸</code>缺少右括号）</li><li>引用消息残留的孤立右中括号</li><li>真实姓名、私密信息、手机号、登录密码</li></ul><p>创建<code>clean_dataset_all.py</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"></span><br><span class="line">INPUT_FILE = <span class="string">&quot;persona_sft_dataset_all.json&quot;</span></span><br><span class="line">OUTPUT_FILE = <span class="string">&quot;persona_sft_dataset_all_clean.json&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">sanitize_text</span>(<span class="params">text: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="comment"># 1. 敏感信息脱敏（按你自己的实际情况调整替换规则）</span></span><br><span class="line">    text = re.sub(re.escape(<span class="string">&quot;1112***&quot;</span>), <span class="string">&quot;11125000&quot;</span>, text)          <span class="comment"># 学号</span></span><br><span class="line">    text = re.sub(re.escape(<span class="string">&quot;你的真实姓名&quot;</span>), <span class="string">&quot;***&quot;</span>, text)          <span class="comment"># 真实姓名</span></span><br><span class="line">    text = re.sub(re.escape(<span class="string">&quot;南苑******&quot;</span>), <span class="string">&quot;某宿舍楼101&quot;</span>, text)    <span class="comment"># 宿舍地址</span></span><br><span class="line">    text = re.sub(re.escape(<span class="string">&quot;密码abc123456&quot;</span>), <span class="string">&quot;密码******&quot;</span>, text)  <span class="comment"># 明文密码</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 2. 手机号统一脱敏</span></span><br><span class="line">    text = re.sub(<span class="string">r&quot;1\d&#123;10&#125;&quot;</span>, <span class="string">&quot;13800000000&quot;</span>, text)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 3. 补全未闭合的微信表情标签</span></span><br><span class="line">    emoji_names = [</span><br><span class="line">        <span class="string">&quot;坏笑&quot;</span>, <span class="string">&quot;捂脸&quot;</span>, <span class="string">&quot;流汗&quot;</span>, <span class="string">&quot;抱拳&quot;</span>, <span class="string">&quot;恐惧&quot;</span>, <span class="string">&quot;OK&quot;</span>, <span class="string">&quot;握手&quot;</span>, <span class="string">&quot;色&quot;</span>, <span class="string">&quot;大哭&quot;</span>, <span class="string">&quot;玫瑰&quot;</span>,</span><br><span class="line">        <span class="string">&quot;害羞&quot;</span>, <span class="string">&quot;裂开&quot;</span>, <span class="string">&quot;发抖&quot;</span>, <span class="string">&quot;发呆&quot;</span>, <span class="string">&quot;得意&quot;</span>, <span class="string">&quot;调皮&quot;</span>, <span class="string">&quot;让我看看&quot;</span>, <span class="string">&quot;胜利&quot;</span>, <span class="string">&quot;骷髅&quot;</span>,</span><br><span class="line">        <span class="string">&quot;转圈&quot;</span>, <span class="string">&quot;奸笑&quot;</span>, <span class="string">&quot;惊讶&quot;</span>, <span class="string">&quot;强&quot;</span>, <span class="string">&quot;打脸&quot;</span>, <span class="string">&quot;破涕为笑&quot;</span>, <span class="string">&quot;憨笑&quot;</span>, <span class="string">&quot;合十&quot;</span>, <span class="string">&quot;傲慢&quot;</span>,</span><br><span class="line">        <span class="string">&quot;凋谢&quot;</span>, <span class="string">&quot;生病&quot;</span>, <span class="string">&quot;白眼&quot;</span>, <span class="string">&quot;快哭了&quot;</span>, <span class="string">&quot;微笑&quot;</span>, <span class="string">&quot;机智&quot;</span>, <span class="string">&quot;偷笑&quot;</span>, <span class="string">&quot;困&quot;</span>, <span class="string">&quot;衰&quot;</span>,</span><br><span class="line">    ]</span><br><span class="line">    <span class="keyword">for</span> name <span class="keyword">in</span> emoji_names:</span><br><span class="line">        text = re.sub(<span class="string">rf&quot;\[<span class="subst">&#123;name&#125;</span>(?!\])&quot;</span>, <span class="string">f&quot;[<span class="subst">&#123;name&#125;</span>]&quot;</span>, text)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 4. 清理多余空行</span></span><br><span class="line">    text = re.sub(<span class="string">r&quot;\n&#123;3,&#125;&quot;</span>, <span class="string">&quot;\n\n&quot;</span>, text)</span><br><span class="line">    <span class="keyword">return</span> text.strip()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(INPUT_FILE, <span class="string">&quot;r&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    data = json.load(f)</span><br><span class="line"></span><br><span class="line">cleaned_data = []</span><br><span class="line"><span class="keyword">for</span> item <span class="keyword">in</span> data:</span><br><span class="line">    new_conversations = []</span><br><span class="line">    skip_item = <span class="literal">False</span></span><br><span class="line">    <span class="keyword">for</span> conv <span class="keyword">in</span> item.get(<span class="string">&quot;conversations&quot;</span>, []):</span><br><span class="line">        val = conv.get(<span class="string">&quot;value&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line"></span><br><span class="line">        cleaned_val = sanitize_text(val)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> cleaned_val:</span><br><span class="line">            skip_item = <span class="literal">True</span></span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        new_conversations.append(&#123;<span class="string">&quot;from&quot;</span>: conv[<span class="string">&quot;from&quot;</span>], <span class="string">&quot;value&quot;</span>: cleaned_val&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> skip_item <span class="keyword">and</span> <span class="built_in">len</span>(new_conversations) &gt;= <span class="number">2</span>:</span><br><span class="line">        cleaned_data.append(&#123;<span class="string">&quot;conversations&quot;</span>: new_conversations&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(OUTPUT_FILE, <span class="string">&quot;w&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    json.dump(cleaned_data, f, ensure_ascii=<span class="literal">False</span>, indent=<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;清洗完成！共处理样本 <span class="subst">&#123;<span class="built_in">len</span>(cleaned_data)&#125;</span> 组，已保存至 <span class="subst">&#123;OUTPUT_FILE&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><p>运行清洗：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python clean_dataset_all.py</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：统计有效回复轮数。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep -c <span class="string">&#x27;&quot;from&quot;: &quot;gpt&quot;&#x27;</span> persona_sft_dataset_all_clean.json</span><br></pre></td></tr></table></figure><p>输出 ≥ 1000即数据量充裕，可直接进入训练。</p><h2 id="注册数据集"><a class="headerlink" href="#注册数据集"></a>注册数据集</h2><h3 id="放置文件"><a class="headerlink" href="#放置文件"></a>放置文件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> persona_sft_dataset_all_clean.json LLaMA-Factory/data/</span><br></pre></td></tr></table></figure><h3 id="注册配置"><a class="headerlink" href="#注册配置"></a>注册配置</h3><p>编辑<code>LLaMA-Factory/data/dataset_info.json</code>，在最外层<code>{</code>内添加：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;my_persona&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;file_name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;persona_sft_dataset_all_clean.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;formatting&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sharegpt&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;columns&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;messages&quot;</span><span class="punctuation">:</span> <span class="string">&quot;conversations&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tags&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;from&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;value&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;user_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;human&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;assistant_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><h3 id="校验配置"><a class="headerlink" href="#校验配置"></a>校验配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line">python -c <span class="string">&quot;import json; json.load(open(&#x27;data/dataset_info.json&#x27;))&quot;</span></span><br></pre></td></tr></table></figure><p>无输出即JSON语法正确。</p><h3 id="在界面中确认（使用WebUI时）"><a class="headerlink" href="#在界面中确认（使用WebUI时）"></a>在界面中确认（使用WebUI时）</h3><p>在<code>LLaMA-Factory</code>仓库根目录启动界面：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line"><span class="built_in">nohup</span> llamafactory-cli webui &gt; webui.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><p>启动后：</p><ol><li>在「数据集」下拉框勾选<code>my_persona</code>。</li><li>点击「预览数据集」，确认能看到human与gpt交替的多轮对话。</li></ol><blockquote><p>说明：务必在<code>LLaMA-Factory</code>仓库目录内启动，程序会在当前目录的<code>./data</code>下查找配置。若在其它目录启动，界面将读不到任何数据集。修改配置后需重启界面进程并强制刷新浏览器（Mac：<code>Cmd + Shift + R</code>）。</p></blockquote><h2 id="启动LoRA微调"><a class="headerlink" href="#启动LoRA微调"></a>启动LoRA微调</h2><h3 id="参数配置表（48GB显存优化值）"><a class="headerlink" href="#参数配置表（48GB显存优化值）"></a>参数配置表（48GB显存优化值）</h3><table><thead><tr><th>配置项</th><th>推荐值</th><th>说明</th></tr></thead><tbody><tr><td>模型路径</td><td><code>/你的绝对路径/models/Qwen3-14B-Instruct</code></td><td>使用本地绝对路径</td></tr><tr><td>对话模板</td><td><code>qwen3_nothink</code></td><td>非思考模式，直接问答</td></tr><tr><td>微调方法</td><td><code>lora</code></td><td>—</td></tr><tr><td>量化等级</td><td><code>none</code></td><td>48G显存直接跑BF16全精度</td></tr><tr><td>数据集</td><td><code>my_persona</code></td><td>前面注册的数据集</td></tr><tr><td>截断长度</td><td><code>2048</code></td><td>保证多轮上下文不被截断</td></tr><tr><td>批处理大小</td><td><code>2</code></td><td>—</td></tr><tr><td>梯度累积</td><td><code>8</code></td><td>等效总batch = 2 × 8 = 16</td></tr><tr><td>学习率</td><td><code>1.5e-4</code></td><td>LoRA标准区间1e-4 ~ 2e-4</td></tr><tr><td>训练轮数</td><td><code>4.0</code></td><td>3~4轮可拿捏口癖</td></tr><tr><td>计算精度</td><td><code>bf16</code></td><td>4090原生支持</td></tr><tr><td>LoRA作用模块</td><td><code>all</code></td><td>覆盖全部线性层，风格拟合最彻底</td></tr><tr><td>LoRA秩</td><td><code>32</code></td><td>—</td></tr><tr><td>LoRA缩放系数</td><td><code>64</code></td><td>取秩的2倍</td></tr></tbody></table><p><strong>三条参数纪律</strong></p><ul><li>对话模板必须选<code>qwen3_nothink</code>（或<code>qwen</code>），不能用<code>default</code>。<code>default</code>不带千问专属角色标识符，模型无法区分对话双方。</li><li>学习率必须设在1e-4 ~ 2e-4。1e-5是全参数微调的量级，对LoRA过小，训练后仍会保留基座的通用AI腔。</li><li>LoRA作用模块填<code>all</code>。留空时框架默认只训练注意力层的q/v投影，风格还原不彻底。</li></ul><h3 id="启动训练"><a class="headerlink" href="#启动训练"></a>启动训练</h3><p><img src="/img/myself-quantized-new/Parameter.png" alt="Parameter"></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line">conda activate llama</span><br><span class="line"></span><br><span class="line">CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \</span><br><span class="line">    --stage sft \</span><br><span class="line">    --do_train \</span><br><span class="line">    --model_name_or_path /你的绝对路径/models/Qwen3-14B-Instruct \</span><br><span class="line">    --dataset my_persona \</span><br><span class="line">    --dataset_dir ./data \</span><br><span class="line">    --template qwen3_nothink \</span><br><span class="line">    --finetuning_type lora \</span><br><span class="line">    --lora_target all \</span><br><span class="line">    --lora_rank 32 \</span><br><span class="line">    --lora_alpha 64 \</span><br><span class="line">    --lora_dropout 0.05 \</span><br><span class="line">    --output_dir ./saves/persona_14b_lora \</span><br><span class="line">    --overwrite_output_dir \</span><br><span class="line">    --cutoff_len 2048 \</span><br><span class="line">    --preprocessing_num_workers 4 \</span><br><span class="line">    --per_device_train_batch_size 2 \</span><br><span class="line">    --gradient_accumulation_steps 8 \</span><br><span class="line">    --lr_scheduler_type cosine \</span><br><span class="line">    --logging_steps 5 \</span><br><span class="line">    --save_steps 50 \</span><br><span class="line">    --learning_rate 1.5e-4 \</span><br><span class="line">    --num_train_epochs 4.0 \</span><br><span class="line">    --plot_loss \</span><br><span class="line">    --bf16</span><br></pre></td></tr></table></figure><h3 id="训练过程观察"><a class="headerlink" href="#训练过程观察"></a>训练过程观察</h3><p><img src="/img/myself-quantized-new/process.png" alt="process"></p><table><thead><tr><th>观察项</th><th>正常表现</th></tr></thead><tbody><tr><td>显存占用</td><td>约42GB（48G显卡余量充足）</td></tr><tr><td>训练耗时</td><td>几百条样本约8~15分钟</td></tr><tr><td>Loss曲线</td><td>从5.x平稳下降至2.x</td></tr><tr><td>完成标志</td><td>终端输出<code>Training completed</code>，或界面出现绿色完成提示</td></tr></tbody></table><blockquote><p>说明：训练启动阶段显存被占满而GPU利用率接近0% 属正常现象，此时正在加载权重与分词。正式训练开始后功耗升至400W、利用率达100%。</p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">Every 1.0s: nvidia-smi                                                          10-60-22-134: Sat Oct 17 19:43:50 2026</span><br><span class="line"></span><br><span class="line">Sat Oct 17 19:43:50 2026</span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br><span class="line">| NVIDIA-SMI 615.71.09              KMD Version: 615.71.09     CUDA UMD Version: 13.4     |</span><br><span class="line">+-----------------------------------------+------------------------+----------------------+</span><br><span class="line">| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |</span><br><span class="line">| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |</span><br><span class="line">|                                         |                        |               MIG M. |</span><br><span class="line">|=========================================+========================+======================|</span><br><span class="line">|   0  NVIDIA GeForce RTX 4090        On  |   00000000:00:06.0 Off |                  Off |</span><br><span class="line">|100%   67C    P2            432W /  450W |   48507MiB /  49140MiB |     100%      Default |</span><br><span class="line">|                                         |                        |                  N/A |</span><br><span class="line">+-----------------------------------------+------------------------+----------------------+</span><br><span class="line"></span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br><span class="line">| Processes:                                                                              |</span><br><span class="line">|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |</span><br><span class="line">|        ID   ID                                                               Usage      |</span><br><span class="line">|=========================================================================================|</span><br><span class="line">|    0   N/A  N/A            1427      C   ...ge3/envs/llama/bin/python3.11        386MiB |</span><br><span class="line">|    0   N/A  N/A            1513      C   ...ge3/envs/llama/bin/python3.11      48106MiB |</span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br></pre></td></tr></table></figure><p><img src="/img/myself-quantized-new/result.png" alt="result"></p><h2 id="合并导出完整模型"><a class="headerlink" href="#合并导出完整模型"></a>合并导出完整模型</h2><p><strong>目的</strong>：把LoRA增量权重合并进基座，得到独立可用的完整模型，后续部署不再依赖适配器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line">conda activate llama</span><br><span class="line"></span><br><span class="line">llamafactory-cli <span class="built_in">export</span> \</span><br><span class="line">    --model_name_or_path /你的绝对路径/models/Qwen3-14B-Instruct \</span><br><span class="line">    --adapter_name_or_path ./saves/persona_14b_lora \</span><br><span class="line">    --template qwen3_nothink \</span><br><span class="line">    --finetuning_type lora \</span><br><span class="line">    --export_dir /你的绝对路径/models/my_persona_14b_merged \</span><br><span class="line">    --export_size 4 \</span><br><span class="line">    --export_device cpu \</span><br><span class="line">    --export_legacy_format False</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：导出目录包含多个<code>.safetensors</code>分卷（14B约25~29GB）、<code>config.json</code>、<code>tokenizer.json</code>、<code>tokenizer_config.json</code>。</p><hr><h2 id="转换为GGUF并量化"><a class="headerlink" href="#转换为GGUF并量化"></a>转换为GGUF并量化</h2><h3 id="为什么选择GGUF"><a class="headerlink" href="#为什么选择GGUF"></a>为什么选择GGUF</h3><p>量化格式按部署平台选择：</p><table><thead><tr><th>格式</th><th>量化是否需要GPU</th><th>适用生态</th><th>适用平台</th></tr></thead><tbody><tr><td><strong>GGUF</strong>（Q4_K_M / Q6_K_M）</td><td>否，纯CPU与内存即可</td><td>Ollama、LM Studio、llama.cpp</td><td><strong>Mac、跨平台</strong></td></tr><tr><td>AWQ（4-bit GEMM）</td><td>是，需跑前向激活</td><td>vLLM、TGI、SGLang</td><td>NVIDIA GPU高并发服务</td></tr></tbody></table><p><strong>Mac部署使用GGUF</strong>。AWQ与GPTQ面向CUDA架构设计，在Apple Silicon的Metal统一内存上无法发挥硬件加速；GGUF深度整合Metal，省内存、发热低、速度快。</p><h3 id="编译llama-cpp"><a class="headerlink" href="#编译llama-cpp"></a>编译llama.cpp</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装编译工具</span></span><br><span class="line"><span class="built_in">sudo</span> apt install cmake -y</span><br><span class="line"></span><br><span class="line"><span class="comment"># 拉取并编译</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/ggerganov/llama.cpp.git</span><br><span class="line"><span class="built_in">cd</span> llama.cpp</span><br><span class="line">cmake -B build</span><br><span class="line">cmake --build build --config Release -j</span><br><span class="line">pip install -r requirements.txt</span><br></pre></td></tr></table></figure><blockquote><p>说明：<code>cmake</code>必须在<code>llama.cpp</code>目录内执行，该目录才包含<code>CMakeLists.txt</code>。</p></blockquote><h3 id="确认分词器配置"><a class="headerlink" href="#确认分词器配置"></a>确认分词器配置</h3><p><strong>目的</strong>：确保合并模型的分词配置符合llama.cpp转换脚本的读取要求。</p><p>检查合并模型目录（如<code>my_persona_14b_merged</code>）中的<code>tokenizer_config.json</code>，确认<code>extra_special_tokens</code>字段为<strong>字典（键值对）格式</strong>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;extra_special_tokens&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;&lt;|im_start|&gt;&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&lt;|im_start|&gt;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;&lt;|im_end|&gt;&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&lt;|im_end|&gt;&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>若为列表格式（如下），请改为上面的字典格式：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;extra_special_tokens&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;&lt;|im_start|&gt;&quot;</span><span class="punctuation">,</span> <span class="string">&quot;&lt;|im_end|&gt;&quot;</span><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>同时确认目录内分词文件齐全：<code>tokenizer.json</code>、<code>tokenizer_config.json</code>、<code>vocab.json</code>、<code>merges.txt</code>。缺失时从基座模型目录复制补齐：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> /你的绝对路径/models/Qwen3-14B-Instruct/tokenizer.json       /你的绝对路径/models/my_persona_14b_merged/</span><br><span class="line"><span class="built_in">cp</span> /你的绝对路径/models/Qwen3-14B-Instruct/tokenizer_config.json /你的绝对路径/models/my_persona_14b_merged/</span><br><span class="line"><span class="built_in">cp</span> /你的绝对路径/models/Qwen3-14B-Instruct/vocab.json            /你的绝对路径/models/my_persona_14b_merged/</span><br><span class="line"><span class="built_in">cp</span> /你的绝对路径/models/Qwen3-14B-Instruct/merges.txt            /你的绝对路径/models/my_persona_14b_merged/</span><br></pre></td></tr></table></figure><blockquote><p>说明：Qwen使用BPE词表，本身不含SentencePiece的<code>tokenizer.model</code>文件。转换脚本会先尝试SentencePiece再回退到GPT2词表路径，属正常流程，无需额外补充<code>tokenizer.model</code>。</p></blockquote><h3 id="转换为GGUF"><a class="headerlink" href="#转换为GGUF"></a>转换为GGUF</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">python /你的绝对路径/llama.cpp/convert_hf_to_gguf.py /你的绝对路径/models/my_persona_14b_merged \</span><br><span class="line">    --outfile /你的绝对路径/models/my_persona_14b_f16.gguf \</span><br><span class="line">    --outtype f16</span><br></pre></td></tr></table></figure><h3 id="量化"><a class="headerlink" href="#量化"></a>量化</h3><p>推荐<code>Q6_K_M</code>：精度接近无损，体积比浮点小一半以上，适合内存充裕的Mac。若追求更小体积选<code>Q4_K_M</code>（约8.5GB）。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">/你的绝对路径/llama.cpp/build/bin/llama-quantize \</span><br><span class="line">    /你的绝对路径/models/my_persona_14b_f16.gguf \</span><br><span class="line">    /你的绝对路径/models/my_persona_14b_Q6_K_M.gguf \</span><br><span class="line">    Q6_K_M</span><br></pre></td></tr></table></figure><p><strong>验证</strong>：生成<code>my_persona_14b_Q6_K_M.gguf</code>，Q6_K_M约11~12GB，Q4_K_M约8.5GB。</p><p>可在服务器上量化完成后再传回Mac：原始浮点模型近30GB，量化后体积缩减约70%，传输大幅加快。中间产物<code>my_persona_14b_f16.gguf</code>与导出目录的分卷权重可在确认成品无误后清理。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(llama) ubuntu@10-60-22-134:~$ <span class="built_in">ls</span> -lh /home/ubuntu/models/</span><br><span class="line">-rw-rw-r-- 1 ubuntu ubuntu  28G Oct 17 20:32 my_persona_14b_f16.gguf</span><br><span class="line">-rw-rw-r-- 1 ubuntu ubuntu  12G Oct 17 20:35 my_persona_14b_Q6_K_M.gguf</span><br></pre></td></tr></table></figure><h2 id="服务器端加载测试"><a class="headerlink" href="#服务器端加载测试"></a>服务器端加载测试</h2><p><strong>目的</strong>：在传回Mac前确认「转换 → 量化」整条链路成功。</p><h3 id="单次推理测试"><a class="headerlink" href="#单次推理测试"></a>单次推理测试</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">/你的绝对路径/llama.cpp/build/bin/llama-cli \</span><br><span class="line">    -m /你的绝对路径/models/my_persona_14b_Q6_K_M.gguf \</span><br><span class="line">    -p <span class="string">&quot;你好，请介绍一下你自己。&quot;</span> \</span><br><span class="line">    -n 128 -ngl 99 -c 4096</span><br></pre></td></tr></table></figure><p>参数说明：</p><ul><li><code>-ngl 99</code>：全部网络层卸载到GPU。显存不足时会自动回退部分层到CPU，日志显示实际卸载层数。</li><li><code>-c</code>：上下文长度。Q6_K模型本体约11.3GB，更长上下文需额外数GB显存放KV Cache；若显卡已被其它进程占用较多，先用<code>-c 4096</code>测试。</li></ul><p><strong>验证</strong>：输出正常、无乱码、无重复，即链路完全成功。</p><h2 id="附录-·-关键参数速查"><a class="headerlink" href="#附录-·-关键参数速查"></a>附录 · 关键参数速查</h2><table><thead><tr><th>环节</th><th>关键项</th><th>正确值</th></tr></thead><tbody><tr><td>基座模型</td><td>版本</td><td><code>OpenPipe/Qwen3-14B-Instruct</code></td></tr><tr><td>对话模板</td><td>template</td><td><code>qwen3_nothink</code></td></tr><tr><td>微调方法</td><td>finetuning_type</td><td><code>lora</code></td></tr><tr><td>量化等级</td><td>quantization_bit</td><td><code>none</code>（48G直接用BF16）</td></tr><tr><td>学习率</td><td>learning_rate</td><td><code>1.5e-4</code></td></tr><tr><td>训练轮数</td><td>num_train_epochs</td><td><code>4.0</code></td></tr><tr><td>截断长度</td><td>cutoff_len</td><td><code>2048</code></td></tr><tr><td>批处理 × 累积</td><td>batch × accumulation</td><td><code>2 × 8</code>（等效16）</td></tr><tr><td>LoRA作用模块</td><td>lora_target</td><td><code>all</code></td></tr><tr><td>LoRA秩 / 缩放</td><td>rank / alpha</td><td><code>32 / 64</code></td></tr><tr><td>计算精度</td><td>—</td><td><code>bf16</code></td></tr><tr><td>部署格式</td><td>Mac</td><td>GGUF（Q6_K_M或Q4_K_M）</td></tr><tr><td>分词配置</td><td>extra_special_tokens</td><td>字典格式</td></tr></tbody></table><hr><h2 id="附录-·-全流程命令速查"><a class="headerlink" href="#附录-·-全流程命令速查"></a>附录 · 全流程命令速查</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">llamafactory-cli train \</span><br><span class="line">    --stage sft \</span><br><span class="line">    --do_train \</span><br><span class="line">    --model_name_or_path /home/ubuntu/models/Qwen3-14B-Instruct \</span><br><span class="line">    --preprocessing_num_workers 16 \</span><br><span class="line">    --finetuning_type lora \</span><br><span class="line">    --template qwen3_nothink \</span><br><span class="line">    --flash_attn fa2 \</span><br><span class="line">    --dataset_dir data \</span><br><span class="line">    --dataset my_persona \</span><br><span class="line">    --cutoff_len 2048 \</span><br><span class="line">    --learning_rate 1.5e-4 \</span><br><span class="line">    --num_train_epochs 4.0 \</span><br><span class="line">    --max_samples 100000 \</span><br><span class="line">    --seed 42 \</span><br><span class="line">    --per_device_train_batch_size 2 \</span><br><span class="line">    --gradient_accumulation_steps 8 \</span><br><span class="line">    --lr_scheduler_type cosine \</span><br><span class="line">    --max_grad_norm 1.0 \</span><br><span class="line">    --logging_steps 5 \</span><br><span class="line">    --save_steps 100 \</span><br><span class="line">    --warmup_steps 0 \</span><br><span class="line">    --packing False \</span><br><span class="line">    --enable_thinking False \</span><br><span class="line">    --report_to none \</span><br><span class="line">    --output_dir saves/persona_14b_lora \</span><br><span class="line">    --bf16 \</span><br><span class="line">    --plot_loss \</span><br><span class="line">    --trust_remote_code True \</span><br><span class="line">    --ddp_timeout 180000000 \</span><br><span class="line">    --include_num_input_tokens_seen True \</span><br><span class="line">    --optim adamw_torch \</span><br><span class="line">    --lora_rank 32 \</span><br><span class="line">    --lora_alpha 64 \</span><br><span class="line">    --lora_dropout 0.05 \</span><br><span class="line">    --lora_target all</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/17/myself-quantized-new/</id>
    <link href="https://u2tt.github.io/2026/10/17/myself-quantized-new/"/>
    <published>2026-10-17T17:00:00.000Z</published>
    <summary>量化自己，通过算力卡</summary>
    <title>自我蒸馏（新）</title>
    <updated>2026-10-20T07:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="blog" scheme="https://u2tt.github.io/tags/blog/"/>
    <category term="随笔" scheme="https://u2tt.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
    <category term="LLM" scheme="https://u2tt.github.io/tags/LLM/"/>
    <content>
      <![CDATA[<p>偶然读到了一篇文章<a href="https://blog.yunyi.beiyan.us/posts/FilterStalking/">《用NLP模型过滤缠扰言论》</a><br>大模型原来还可以被自我训练，量化出一个自己。看起来貌似也不是很难 。</p><p>那就做吧！</p><p>去年做blender渲染，找了一下能用的，便宜的云显卡。今年刚好能用上。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">(py310) root@10-23-218-65:~# lscpu | grep -E <span class="string">&#x27;Model name|^CPU\(s\)|Core\(s\) per socket|Socket\(s\)&#x27;</span></span><br><span class="line">CPU(s):                             16</span><br><span class="line">Model name:                         AMD EPYC 7642 48-Core Processor</span><br><span class="line">Core(s) per socket:                 8</span><br><span class="line">Socket(s):                          2</span><br><span class="line">(py310) root@10-23-218-65:~# free -h</span><br><span class="line">               total        used        free      shared  buff/cache   available</span><br><span class="line">Mem:            62Gi       1.9Gi        59Gi       0.0Ki       1.5Gi        60Gi</span><br><span class="line">Swap:             0B          0B          0B</span><br><span class="line">(py310) root@10-23-218-65:~# nvidia-smi</span><br><span class="line">Thu Oct 15 17:20:42 2026</span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br><span class="line">| NVIDIA-SMI 595.80                 Driver Version: 595.80         CUDA Version: 13.2     |</span><br><span class="line">+-----------------------------------------+------------------------+----------------------+</span><br><span class="line">| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |</span><br><span class="line">| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |</span><br><span class="line">|                                         |                        |               MIG M. |</span><br><span class="line">|=========================================+========================+======================|</span><br><span class="line">|   0  NVIDIA GeForce RTX 4090        Off |   00000000:00:03.0 Off |                  Off |</span><br><span class="line">| 45%   36C    P8             10W /  450W |       1MiB /  24564MiB |      0%      Default |</span><br><span class="line">|                                         |                        |                  N/A |</span><br><span class="line">+-----------------------------------------+------------------------+----------------------+</span><br><span class="line"></span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br><span class="line">| Processes:                                                                              |</span><br><span class="line">|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |</span><br><span class="line">|        ID   ID                                                               Usage      |</span><br><span class="line">|=========================================================================================|</span><br><span class="line">|  No running processes found                                                             |</span><br><span class="line">+-----------------------------------------------------------------------------------------+</span><br></pre></td></tr></table></figure><p><s>哎呀！怎么都涨价了？已经翻三倍了</s><br>草草用用吧。</p><h2 id="1-服务器基线确认"><a class="headerlink" href="#1-服务器基线确认"></a>1. 服务器基线确认</h2><h3 id="1-1-前置条件"><a class="headerlink" href="#1-1-前置条件"></a>1.1 前置条件</h3><p>已从算力平台获得：IP、SSH端口、root密码、已选镜像（PyTorch 2.1.2 / Python 3.10 / CUDA 12.1）。</p><h3 id="1-2-登录与系统确认"><a class="headerlink" href="#1-2-登录与系统确认"></a>1.2 登录与系统确认</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 从本地 Mac 连接；-p 23 为平台分配的 SSH 端口，非默认 22</span></span><br><span class="line">ssh -p 23 root@106.75.240.167</span><br></pre></td></tr></table></figure><p><strong>可验证结果</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">The authenticity of host &#x27;[106.75.240.167]:23 ([106.75.240.167]:23)&#x27; can&#x27;t be established.</span><br><span class="line">ED25519 key fingerprint is: SHA256:BbzgCQAzNbffEbv62JMv73XMooEY1edca97CJtjO27o</span><br><span class="line">...</span><br><span class="line">Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-113-generic x86_64)</span><br><span class="line">(py310) root@10-23-218-65:~#</span><br></pre></td></tr></table></figure><p>提示符<code>(py310)</code>说明conda已自动激活预置环境<code>py310</code>，路径在<code>/usr/local/miniconda3/envs/py310</code>。<strong>这一步决定了后续第2节的坑。</strong></p><h3 id="1-3-GPU与驱动确认"><a class="headerlink" href="#1-3-GPU与驱动确认"></a>1.3 GPU与驱动确认</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">nvidia-smi                       <span class="comment"># 预期：驱动版本 + CUDA Version + 23.5GB 显存 + 0 占用</span></span><br><span class="line">watch -n 1 nvidia-smi            <span class="comment"># 训练期间盯 Memory-Usage 与 Volatile GPU-Util</span></span><br><span class="line">nvtop                            <span class="comment"># 彩色曲线版，未预装时 apt install nvtop</span></span><br></pre></td></tr></table></figure><p><strong>可验证结果</strong>：<code>nvidia-smi</code>输出中<code>Memory-Usage</code>初始接近0 MiB；训练开始后应稳定在 16~21 GB区间，<code>Volatile GPU-Util</code>维持90~100%。低于50%说明数据加载或 batch配置存在瓶颈。</p><h3 id="1-4-磁盘确认"><a class="headerlink" href="#1-4-磁盘确认"></a>1.4 磁盘确认</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -h            <span class="comment"># 系统盘通常 30~50GB，模型与产物必须放数据盘</span></span><br><span class="line"><span class="built_in">du</span> -sh ~/.cache/modelscope    <span class="comment"># 基座模型缓存，7B bf16 约 15GB</span></span><br></pre></td></tr></table></figure><p>7B模型会产出三份大文件：基座缓存（~15GB）+ 合并模型（~15GB）+ GGUF（4~8GB）。系统盘放不下时必须切数据盘（AutoDL为<code>/root/autodl-tmp</code>）。</p><hr><h2 id="2-安装LLaMA-Factory"><a class="headerlink" href="#2-安装LLaMA-Factory"></a>2. 安装LLaMA-Factory</h2><h3 id="2-1-操作目的"><a class="headerlink" href="#2-1-操作目的"></a>2.1 操作目的</h3><p>获得训练 / 合并导出 / 格式转换的统一入口<code>llamafactory-cli</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/hiyouga/LLaMA-Factory.git</span><br><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line">pip install -e .[metrics]        <span class="comment"># -e 可编辑安装；[metrics] 附带评估指标依赖</span></span><br></pre></td></tr></table></figure><h3 id="2-2-报错-1：Python版本不满足"><a class="headerlink" href="#2-2-报错-1：Python版本不满足"></a>2.2 报错 1：Python版本不满足</h3><p><strong>触发命令</strong>：<code>pip install -e .[metrics]</code>（在默认<code>py310</code>环境中）</p><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">INFO: pip is looking at multiple versions of llamafactory to determine which version is compatible with</span><br><span class="line">other requirements. This could take a while.</span><br><span class="line">ERROR: Package &#x27;llamafactory&#x27; requires a different Python: 3.10.16 not in &#x27;&gt;=3.11.0&#x27;</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：报错直指<code>llamafactory==0.9.6.dev0</code>的元数据约束。从同一段日志可看到该版本的依赖收敛范围：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Collecting transformers!=4.57.0,!=5.6.0,&lt;=5.8.0,&gt;=4.55.0 (from llamafactory==0.9.6.dev0)</span><br><span class="line">Collecting trl&lt;=0.24.0,&gt;=0.18.0 (from llamafactory==0.9.6.dev0)</span><br><span class="line">Collecting peft&lt;=0.18.1,&gt;=0.18.0 (from llamafactory==0.9.6.dev0)</span><br><span class="line">Collecting accelerate&lt;=1.11.0,&gt;=1.3.0 (from llamafactory==0.9.6.dev0)</span><br></pre></td></tr></table></figure><p>即：<strong>0.9.6.dev0强制Python ≥ 3.11</strong>，与镜像预置的3.10.16冲突。选型阶段推荐的「Python 3.10依赖冲突最少」在此版本上已失效。</p><p><strong>解决过程</strong>：新建conda环境后重装。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">conda create -n py311 python=3.11 -y</span><br><span class="line">conda activate py311</span><br><span class="line"><span class="built_in">cd</span> ~/LLaMA-Factory</span><br><span class="line">pip install -e .[metrics]</span><br></pre></td></tr></table></figure><p><strong>可验证结果</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">python -V                        <span class="comment"># Python 3.11.x</span></span><br><span class="line">pip show llamafactory | <span class="built_in">head</span> -3  <span class="comment"># Version: 0.9.6.dev0</span></span><br><span class="line">llamafactory-cli --version       <span class="comment"># 命令可用</span></span><br></pre></td></tr></table></figure><p>实测装到的关键版本：<code>torch 2.14.1</code>、<code>transformers 5.8.0</code>、<code>peft 0.18.1</code>、<code>trl 0.24.0</code>、<code>accelerate 1.11.0</code>、<code>gradio 5.50.0</code>、<code>modelscope 1.40.2</code>。</p><p><strong>版本号核验（重要）</strong>：日志中的<code>llamafactory==0.9.6.dev0</code><strong>不是发布版本</strong>。核验结果：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">gh api repos/hiyouga/LLaMA-Factory/tags --jq <span class="string">&#x27;.[].name&#x27;</span> | <span class="built_in">head</span> -5   <span class="comment"># v0.9.5 v0.9.4 v0.9.3 ...</span></span><br><span class="line">curl -s https://pypi.org/pypi/llamafactory/json                     <span class="comment"># latest: 0.9.5</span></span><br></pre></td></tr></table></figure><p>PyPI与GitHub tag最新均为 <strong>v0.9.5</strong>，不存在0.9.6。<code>0.9.6.dev0</code>是<code>pip install -e .</code>从master源码安装时，setuptools_scm基于最新tag自动推断出的<strong>开发态版本号</strong>（即v0.9.5之后的未发布快照）。</p><p><strong>影响</strong>：凡本文引用<code>src/llamafactory/...</code>源码作为依据处，均指master分支当前代码，与v0.9.5正式版<strong>可能存在差异</strong>；与<code>0.9.6.dev0</code>快照的差异未做逐commit比对。</p><blockquote><p><strong>结论</strong>：镜像选择应直接选Python 3.11，可省掉这一轮重建。CUDA版本仍选12.1（预编译wheel的最大公约数，避免云端源码编译）。</p></blockquote><hr><h2 id="3-数据集构造与注册"><a class="headerlink" href="#3-数据集构造与注册"></a>3. 数据集构造与注册</h2><h3 id="3-1-前置条件"><a class="headerlink" href="#3-1-前置条件"></a>3.1 前置条件</h3><p>语料为微信聊天记录，由WeFlow导出为JSON，字段含<code>type</code>（消息类型）、<code>content</code>（文本）、<code>createTime</code>（秒级时间戳）、<code>isSend</code>（0=对方 / 1=我）。</p><h3 id="3-2-操作目的"><a class="headerlink" href="#3-2-操作目的"></a>3.2 操作目的</h3><p>把「一问一答」升级为「多轮对话」，保留真实的上下文抛接结构。</p><p>关键参数（脚本内）：</p><ul><li>静默阈值 <strong>30分钟</strong>：双方无消息超过该值即切分新的Session；</li><li>阵营合并：Session内同一人的连续消息合并为单条，直到对方插话；</li><li>入库约束：首条必须<code>human</code>、末条必须<code>gpt</code>、至少2轮。</li></ul><p><strong>可验证结果</strong>：脚本输出<code>重构完成！共提炼出1729个高质量多轮对话局，已保存至 sharegpt_train_data.json</code>。</p><h3 id="3-3-注册数据集"><a class="headerlink" href="#3-3-注册数据集"></a>3.3 注册数据集</h3><p>编辑<code>data/dataset_info.json</code>，把配置插在最外层<code>{</code>下面（<strong>不是</strong>追加到文件末尾）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;personal_style_sharegpt&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;file_name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sharegpt_train_data.json&quot;</span><span class="punctuation">,</span>   <span class="comment">// 放 data/ 目录下</span></span><br><span class="line">  <span class="attr">&quot;formatting&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sharegpt&quot;</span><span class="punctuation">,</span>                  <span class="comment">// 多轮格式，区别于 alpaca 单轮</span></span><br><span class="line">  <span class="attr">&quot;columns&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;messages&quot;</span><span class="punctuation">:</span> <span class="string">&quot;conversations&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;system&quot;</span><span class="punctuation">:</span> <span class="string">&quot;system&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tags&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;from&quot;</span><span class="punctuation">,</span>                      <span class="comment">// 角色字段名</span></span><br><span class="line">    <span class="attr">&quot;content_tag&quot;</span><span class="punctuation">:</span> <span class="string">&quot;value&quot;</span><span class="punctuation">,</span>                  <span class="comment">// 内容字段名</span></span><br><span class="line">    <span class="attr">&quot;user_role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;human&quot;</span><span class="punctuation">,</span>                    <span class="comment">// 对方</span></span><br><span class="line">    <span class="attr">&quot;bot_role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt&quot;</span>                        <span class="comment">// 我</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><h3 id="3-4-验证方式"><a class="headerlink" href="#3-4-验证方式"></a>3.4 验证方式</h3><p>WebUI Train页点击 <strong>预览数据集</strong>，弹窗第一条应是完整的多轮微信对话；命令行侧以训练日志<code>Num examples = 1,729</code>为准。</p><h3 id="3-5-报错-2：样本被丢弃"><a class="headerlink" href="#3-5-报错-2：样本被丢弃"></a>3.5 报错 2：样本被丢弃</h3><p><strong>触发命令</strong>：首次训练启动</p><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[WARNING|2026-10-11 13:19:54] llamafactory.data.processor.supervised:149 &gt;&gt; Dropped invalid example: []</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：日志中<code>Dropped invalid example:</code>后的列表为空<code>[]</code>，说明被丢弃的样本内容为空——即数据集注册格式与文件格式不匹配（alpaca格式配置去读 sharegpt文件，或反之），解析出的对话列表为空。该WARNING不中断训练，但会静默减少有效样本量。</p><p><strong>解决过程</strong>：核对<code>dataset_info.json</code>的<code>formatting</code>与<code>tags</code>是否与JSON文件实际字段一致。修正后重跑，日志显示<code>Num examples = 1,729</code>（无 dropped）。</p><blockquote><p><strong>结论</strong>：<code>Dropped invalid example</code>必须用<code>Num examples</code>交叉验证，不能只看「训练跑起来了」。</p></blockquote><hr><h2 id="4-训练"><a class="headerlink" href="#4-训练"></a>4. 训练</h2><h3 id="4-1-参数选择依据"><a class="headerlink" href="#4-1-参数选择依据"></a>4.1 参数选择依据</h3><p>先量化数据长度分布，再定<code>cutoff_len</code></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;sharegpt_train_data.json&quot;</span>, <span class="string">&quot;r&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    dataset = json.load(f)</span><br><span class="line">max_chars = exceed_count = total_chars = <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> item <span class="keyword">in</span> dataset:</span><br><span class="line">    item_chars = <span class="built_in">sum</span>(<span class="built_in">len</span>(t.get(<span class="string">&quot;value&quot;</span>, <span class="string">&quot;&quot;</span>)) <span class="keyword">for</span> t <span class="keyword">in</span> item.get(<span class="string">&quot;conversations&quot;</span>, []))</span><br><span class="line">    total_chars += item_chars</span><br><span class="line">    max_chars = <span class="built_in">max</span>(max_chars, item_chars)</span><br><span class="line">    <span class="keyword">if</span> item_chars &gt; <span class="number">500</span>:</span><br><span class="line">        exceed_count += <span class="number">1</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(dataset), total_chars / <span class="built_in">len</span>(dataset), max_chars, exceed_count)</span><br><span class="line"><span class="comment"># 1 汉字 ≈ 1.5~2 Token；1024 Token ≈ 500~650 汉字</span></span><br></pre></td></tr></table></figure><br><p><img src="/img/myself-quantized/Config.png" alt=""></p><p>实测输出：<strong>1729条 / 平均265.7字符 / 最大5313字符 / 超500字符的242条</strong>。</p><h3 id="4-2-报错-3-4-5：三次CUDA-OOM"><a class="headerlink" href="#4-2-报错-3-4-5：三次CUDA-OOM"></a>4.2 报错 3/4/5：三次CUDA OOM</h3><p><strong>触发配置与结果</strong>：</p><table><thead><tr><th>#</th><th>cutoff_len</th><th>batch size</th><th>grad_accum</th><th>gradient_checkpointing</th><th>结果</th></tr></thead><tbody><tr><td>1</td><td>2048</td><td>4</td><td>8</td><td>否</td><td>Step 1即OOM</td></tr><tr><td>2</td><td>2048</td><td>2</td><td>16</td><td>否</td><td>Step 9 OOM</td></tr><tr><td>3</td><td>2048</td><td>2</td><td>16</td><td>是</td><td>Step 9仍OOM</td></tr><tr><td>4</td><td>1024</td><td>1</td><td>32</td><td>是</td><td>✅ 跑完3 Epoch / 165步</td></tr></tbody></table><p><strong>错误原文（第 1 次）</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.38 GiB. GPU 0 has a total capacity of</span><br><span class="line">23.52 GiB of which 2.20 GiB is free. Process 312 has 386.00 MiB memory in use. Including non-PyTorch</span><br><span class="line">memory, this process has 20.92 GiB memory in use. Of the allocated memory 18.82 GiB is allocated by</span><br><span class="line">PyTorch, and 1.64 GiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is</span><br><span class="line">large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.</span><br><span class="line">0%| | 0/165 [00:06&lt;?, ?it/s]</span><br></pre></td></tr></table></figure><p>第 2、3 次报错同构，仅数字变化：<code>Tried to allocate 2.32 GiB ... of which 2.08 GiB is free ... this process has 21.04 GiB memory in use</code>。</p><p><strong>排查定位</strong>：</p><ul><li>崩溃点固定在<code>transformers/loss/loss_utils.py:37 fixed_cross_entropy</code>→<code>torch._C._nn.cross_entropy_loss</code>，即<strong>反向传播起点</strong>，不是前向；<br>-<code>18.82 GiB is allocated by PyTorch</code>说明基座权重 + 激活值已吃满，剩余2.20 GiB不够分配logits的梯度（batch×seq×152064词表）；</li><li>关键：第3次已开<code>gradient_checkpointing</code>仍崩 → 说明单样本峰值由<strong>最长序列</strong>决定，而数据中存在5,313字符的样本。<code>cutoff_len 2048</code>下该样本的 logits 张量本身就是显存炸弹。</li></ul><p><strong>解决过程</strong>：把<code>cutoff_len</code>从2048降到1024、batch降到1、梯度累积升到32保持等效batch size不变。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True   <span class="comment"># 可选：缓解碎片，非必需</span></span><br></pre></td></tr></table></figure><h3 id="4-3-报错-6：输出目录已存在"><a class="headerlink" href="#4-3-报错-6：输出目录已存在"></a>4.3 报错 6：输出目录已存在</h3><p><strong>触发命令</strong>：第二次启动训练（沿用相同<code>--output_dir</code>）</p><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ValueError: Output directory already exists and is not empty. Please set`overwrite_output_dir`.</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：安全拦截——目录下已有上一次的checkpoint。</p><p><strong>解决过程</strong>：二选一。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方案 A：显式允许覆盖（会清掉旧 checkpoint）</span></span><br><span class="line">--overwrite_output_dir True</span><br><span class="line"><span class="comment"># 方案 B：换目录名保留历史（推荐，便于对比不同超参）</span></span><br><span class="line">--output_dir saves/Qwen2.5-7B-Instruct/lora/train_2026-10-11-12-57-43-v2</span><br></pre></td></tr></table></figure><h3 id="4-4-最终可用命令"><a class="headerlink" href="#4-4-最终可用命令"></a>4.4 最终可用命令</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">llamafactory-cli train \</span><br><span class="line">  --stage sft --do_train True \</span><br><span class="line">  --model_name_or_path Qwen/Qwen2.5-7B-Instruct \</span><br><span class="line">  --preprocessing_num_workers 16 \</span><br><span class="line">  --finetuning_type lora --template qwen --flash_attn auto \</span><br><span class="line">  --dataset_dir data --dataset personal_style_sharegpt \</span><br><span class="line">  --cutoff_len 1024 \                      <span class="comment"># 截断长度：实测 242 条超长样本会在此处被切尾</span></span><br><span class="line">  --learning_rate 2e-05 --num_train_epochs 3.0 --max_samples 100000 --seed 42 \</span><br><span class="line">  --per_device_train_batch_size 1 \        <span class="comment"># 单卡单样本，规避 OOM</span></span><br><span class="line">  --gradient_accumulation_steps 32 \        <span class="comment"># 1×32 = 等效 batch size 32</span></span><br><span class="line">  --lr_scheduler_type cosine --max_grad_norm 1.0 \</span><br><span class="line">  --logging_steps 5 --save_steps 100 --warmup_steps 0 \</span><br><span class="line">  --packing False --enable_thinking True --report_to none \</span><br><span class="line">  --output_dir saves/Qwen2.5-7B-Instruct/lora/train_2026-10-11-12-57-43-v2 \</span><br><span class="line">  --overwrite_output_dir True \</span><br><span class="line">  --bf16 True --plot_loss True --trust_remote_code True \</span><br><span class="line">  --ddp_timeout 180000000 --include_num_input_tokens_seen True \</span><br><span class="line">  --optim adamw_torch --gradient_checkpointing True \   <span class="comment"># 重算激活值，显存换时间</span></span><br><span class="line">  --lora_rank 8 --lora_alpha 16 --lora_dropout 0 --lora_target all</span><br></pre></td></tr></table></figure><p><img src="/img/myself-quantized/Performance-Display.png" alt=""><br><img src="/img/myself-quantized/Quantified-Results.png" alt=""><br><img src="/img/myself-quantized/quantization-file.png" alt=""></p><p><strong>可验证结果</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Num examples = 1,729 | Num Epochs = 3 | Num update steps per epoch = 55</span><br><span class="line">Instantaneous batch size per device = 1 | Gradient Accumulation steps = 32</span><br><span class="line">Total train batch size (w. parallel, distributed &amp; accumulation) = 32</span><br><span class="line">Total optimization steps = 165</span><br><span class="line">Fine-tuning method: LoRA</span><br><span class="line">Found linear modules: k_proj,down_proj,gate_proj,q_proj,up_proj,o_proj,v_proj</span><br><span class="line">trainable params: 20,185,088 | all params: 7,635,801,600 | trainable%: 0.2643</span><br></pre></td></tr></table></figure><p>Loss轨迹：<code>5.7270 → 4.0200（约40步）→ 3.8~4.0 横盘 → 4.3965（第165步末端反弹）</code>，<code>train_loss 4.4711</code>。</p><p>末端反弹原因：<code>lr_scheduler_type cosine</code>把学习率压到<code>1.8125e-09</code>，优化器失去修正能力，同时shuffle末尾撞上风格突变样本。<strong>该解释基于日志推断，非官方结论，标注为待验证。</strong></p><hr><h2 id="5-合并导出（LoRA-→-完整HF模型）"><a class="headerlink" href="#5-合并导出（LoRA-→-完整HF模型）"></a>5. 合并导出（LoRA → 完整HF模型）</h2><h3 id="5-1-前置条件"><a class="headerlink" href="#5-1-前置条件"></a>5.1 前置条件</h3><p>第4节训练完成，产出<code>saves/Qwen2.5-7B-Instruct/lora/train_2026-10-11-12-57-43-v2</code>。</p><h3 id="5-2-操作目的"><a class="headerlink" href="#5-2-操作目的"></a>5.2 操作目的</h3><p>把LoRA低秩矩阵吸收进基座权重，产出独立的、不依赖adapter的标准HF目录——<strong>这是量化的必需前置</strong>，量化工具不接受分离的adapter。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">llamafactory-cli <span class="built_in">export</span> \</span><br><span class="line">  --model_name_or_path Qwen/Qwen2.5-7B-Instruct \</span><br><span class="line">  --adapter_name_or_path saves/Qwen2.5-7B-Instruct/lora/train_2026-10-11-12-57-43-v2 \</span><br><span class="line">  --template qwen \</span><br><span class="line">  --finetuning_type lora \</span><br><span class="line">  --export_dir models/chenxinyu-qwen2.5-7b-chat \</span><br><span class="line">  --export_size 2 \                 <span class="comment"># 分片大小，单位 GB；源码默认 5</span></span><br><span class="line">  --export_device cuda \</span><br><span class="line">  --export_legacy_format False      <span class="comment"># False = 输出 safetensors；True = 旧式 .bin</span></span><br></pre></td></tr></table></figure><p><strong>参数依据</strong>（<code>src/llamafactory/hparams/model_args.py</code>）：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export_size: <span class="built_in">int</span> = field(default=<span class="number">5</span>, metadata=&#123;<span class="string">&quot;help&quot;</span>: <span class="string">&quot;The file shard size (in GB) of the exported model.&quot;</span>&#125;)</span><br><span class="line">export_device: <span class="type">Literal</span>[<span class="string">&quot;cpu&quot;</span>, <span class="string">&quot;auto&quot;</span>] = field(default=<span class="string">&quot;cpu&quot;</span>,</span><br><span class="line">    metadata=&#123;<span class="string">&quot;help&quot;</span>: <span class="string">&quot;The device used in model export, use`auto`to accelerate exporting.&quot;</span>&#125;)</span><br><span class="line">export_legacy_format: <span class="built_in">bool</span> = field(default=<span class="literal">False</span>,</span><br><span class="line">    metadata=&#123;<span class="string">&quot;help&quot;</span>: <span class="string">&quot;Whether or not to save the`.bin`files instead of`.safetensors`.&quot;</span>&#125;)</span><br></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>版本差异</strong>：main分支的<code>export_device</code>只接受<code>cpu</code>/<code>auto</code>，而本文实测在0.9.6.dev0上传<code>cuda</code>被正常接受并完成导出。以实际版本的<code>llamafactory-cli export --help</code>为准。</p></blockquote><h3 id="5-3-可验证结果"><a class="headerlink" href="#5-3-可验证结果"></a>5.3 可验证结果</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Loading checkpoint shards: 100%|██████████| 4/4 [00:00&lt;00:00, 91.87it/s]</span><br><span class="line">[INFO] llamafactory.model.adapter:144 &gt;&gt; Merged 1 adapter(s).</span><br><span class="line">[INFO] llamafactory.model.adapter:144 &gt;&gt; Loaded adapter(s): saves/.../train_2026-10-11-12-57-43-v2</span><br><span class="line">[INFO] llamafactory.model.loader:144 &gt;&gt; all params: 7,615,616,512</span><br></pre></td></tr></table></figure><p>目录内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> LLaMA-Factory/models/chenxinyu-qwen2.5-7b-chat</span><br><span class="line"><span class="comment"># added_tokens.json  chat_template.jinja  config.json  generation_config.json</span></span><br><span class="line"><span class="comment"># merges.txt  model-00001-of-00004.safetensors  ...  model-00004-of-00004.safetensors</span></span><br><span class="line"><span class="comment"># model.safetensors.index.json  special_tokens_map.json</span></span><br><span class="line"><span class="comment"># tokenizer.json  tokenizer_config.json  vocab.json</span></span><br></pre></td></tr></table></figure><p><strong>注意</strong>：导出目录<strong>缺少<code>tokenizer.model</code></strong>（Qwen2.5使用<code>tokenizer.json</code>现代词表，非SentencePiece<code>.model</code>）。这直接导致第6节的报错9。</p><hr><h2 id="6-量化：四条路径的实测结果"><a class="headerlink" href="#6-量化：四条路径的实测结果"></a>6. 量化：四条路径的实测结果</h2><h3 id="路径结论汇总（先看结果）"><a class="headerlink" href="#路径结论汇总（先看结果）"></a>路径结论汇总（先看结果）</h3><table><thead><tr><th>路径</th><th>做法</th><th>结果</th><th>判定</th></tr></thead><tbody><tr><td>A</td><td>LLaMA-Factory Export页选量化等级</td><td>下拉框置灰，未产出</td><td>❌ 该选项是 <strong>GPTQ位数量化（2/3/4/8）</strong>，不是GGUF；置灰原因是多选了checkpoint</td></tr><tr><td>B</td><td><code>convert_hf_to_gguf.py --outtype q4_k_m</code></td><td>三个报错连环，未产出</td><td>❌<code>--outtype</code>根本不支持K-quant</td></tr><tr><td><strong>C</strong></td><td><strong>独立 venv +<code>convert_hf_to_gguf.py --outtype f16</code>→<code>llama-quantize Q4_K_M</code></strong></td><td>未在本文环境跑完</td><td>✅ <strong>唯一可行</strong>，官方two-phase流程</td></tr><tr><td>D</td><td><code>ollama create -f Modelfile</code>导入 safetensors</td><td>可运行</td><td>⚠️ 可用于验证推理效果，<strong>但Ollama导入时不量化</strong></td></tr></tbody></table><p><strong>唯一可达成目标的路径是C。</strong> B的失败见6.2，C的完整做法见6.3，D的定位见6.4。</p><h3 id="6-1-路径A：LLaMA-Factory直接导出GGUF-——-失败"><a class="headerlink" href="#6-1-路径A：LLaMA-Factory直接导出GGUF-——-失败"></a>6.1 路径A：LLaMA-Factory直接导出GGUF —— 失败</h3><p><strong>操作</strong>：Export页面选择「导出量化等级」，或命令行<code>--export_quantization_bit</code>。</p><p><strong>现象</strong>：WebUI中「导出量化等级」下拉框<strong>置灰不可选</strong>；命令行方式同样未产出GGUF。</p><p><strong>源码级排查（克隆<code>hiyouga/LLaMA-Factory</code>main分支，<code>src/llamafactory/webui/components/export.py</code>）</strong>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">GPTQ_BITS = [<span class="string">&quot;8&quot;</span>, <span class="string">&quot;4&quot;</span>, <span class="string">&quot;3&quot;</span>, <span class="string">&quot;2&quot;</span>]                      <span class="comment"># 第 37 行</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 下拉框的全部可选项 —— 注意：没有 q4_k_m / q8_0</span></span><br><span class="line">export_quantization_bit = gr.Dropdown(choices=[<span class="string">&quot;none&quot;</span>] + GPTQ_BITS, value=<span class="string">&quot;none&quot;</span>)   <span class="comment"># 第 121 行</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">can_quantize</span>(<span class="params">checkpoint_path: <span class="built_in">str</span> | <span class="built_in">list</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="string">&quot;gr.Dropdown&quot;</span>:                <span class="comment"># 第 40 行</span></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(checkpoint_path, <span class="built_in">list</span>) <span class="keyword">and</span> <span class="built_in">len</span>(checkpoint_path) != <span class="number">0</span>:</span><br><span class="line">        <span class="keyword">return</span> gr.Dropdown(value=<span class="string">&quot;none&quot;</span>, interactive=<span class="literal">False</span>)      <span class="comment"># ← 置灰的确切条件</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">return</span> gr.Dropdown(interactive=<span class="literal">True</span>)</span><br></pre></td></tr></table></figure><p><strong>置灰根因（已定位，非&quot;环境缺组件&quot;）</strong>：当<code>checkpoint_path</code>是<strong>非空 list</strong>（即在检查点选择框里选了多个checkpoint目录）时，<code>can_quantize()</code>直接返回<code>interactive=False</code>。<strong>与llama.cpp组件、GGUF依赖无关。</strong></p><p><strong>该选项的真实语义（已定位，不是GGUF）</strong>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># src/llamafactory/model/model_utils/quantization.py:143</span></span><br><span class="line"><span class="keyword">elif</span> model_args.export_quantization_bit <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:  <span class="comment"># gptqmodel</span></span><br><span class="line">    <span class="keyword">if</span> model_args.export_quantization_bit <span class="keyword">not</span> <span class="keyword">in</span> [<span class="number">8</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>]:</span><br><span class="line">        <span class="keyword">raise</span> ValueError(<span class="string">&quot;AutoGPTQ only accepts 2/3/4/8-bit quantization.&quot;</span>)</span><br><span class="line">    check_version(<span class="string">&quot;optimum&gt;=1.24.0&quot;</span>, mandatory=<span class="literal">True</span>)</span><br><span class="line">    check_version(<span class="string">&quot;gptqmodel&gt;=2.0.0&quot;</span>, mandatory=<span class="literal">True</span>)</span><br><span class="line">    ...</span><br><span class="line">    init_kwargs[<span class="string">&quot;quantization_config&quot;</span>] = GPTQConfig(bits=..., dataset=...)</span><br><span class="line">    logger.info_rank0(<span class="string">f&quot;Quantizing model to <span class="subst">&#123;model_args.export_quantization_bit&#125;</span> bit with GPTQModel.&quot;</span>)</span><br></pre></td></tr></table></figure><p><strong>结论</strong>：</p><ol><li>「导出量化等级」= <strong>GPTQ位数量化</strong>（2/3/4/8 bit），依赖<code>optimum</code>+<code>gptqmodel</code>，产出的是 <strong>GPTQ量化的safetensors模型</strong>，<strong>与GGUF的q4_k_m是两条完全不同的技术路线</strong>；</li><li>置灰触发条件是<strong>多选了checkpoint</strong>，不是环境缺组件；</li><li>GPTQ路线还要求必填校准数据集（<code>__post_init__</code>中<code>export_quantization_dataset is None</code>直接<code>raise ValueError</code>），且GPTQ + LoRA adapter组合被WebUI显式禁止（<code>err_gptq_lora</code>）；</li><li>LLaMA-Factory README（main）<strong>全文无<code>GGUF</code>/<code>llama.cpp</code>/<code>--export_quantization</code>关键词</strong>，GGUF导出无官方文档依据。</li></ol><p><strong>判定</strong>：LLaMA-Factory 无法产出q4_k_m GGUF，不作为量化路径。</p><h3 id="6-2-路径B：llama-cpp脚本一步转q4-k-m-——-失败（三次不同报错）"><a class="headerlink" href="#6-2-路径B：llama-cpp脚本一步转q4-k-m-——-失败（三次不同报错）"></a>6.2 路径B：llama.cpp脚本一步转q4_k_m —— 失败（三次不同报错）</h3><h4 id="报错-7：脚本不存在"><a class="headerlink" href="#报错-7：脚本不存在"></a>报错 7：脚本不存在</h4><p><strong>触发命令</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python llama.cpp/convert_hf_to_gguf.py models/chenxinyu-qwen2.5-7b-chat --outfile models/chenxinyu-q4_k_m.gguf --outtype q4_k_m</span><br></pre></td></tr></table></figure><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python: can&#x27;t open file &#x27;/root/llama.cpp/convert_hf_to_gguf.py&#x27;: [Errno 2] No such file or directory</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：<code>llama.cpp</code>目录根本不存在——此前只执行了<code>pip install -r llama.cpp/requirements.txt</code>，从未<code>git clone</code>仓库。</p><p><strong>解决过程</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~</span><br><span class="line">git <span class="built_in">clone</span> https://github.com/ggml-org/llama.cpp.git</span><br><span class="line"><span class="comment"># 实测输出：remote: Total 138682 (delta 57), reused 48 (delta 48), pack-reused 138573</span></span><br><span class="line"><span class="comment">#          Receiving objects: 100% (138682/138682), 446.28 MiB | 11.08 MiB/s, done.</span></span><br><span class="line"><span class="built_in">cd</span> llama.cpp &amp;&amp; pip install -r requirements.txt &amp;&amp; <span class="built_in">cd</span> ~</span><br></pre></td></tr></table></figure><blockquote><p><strong>致命副作用（本次实操最大的坑）</strong>：<code>pip install -r requirements.txt</code>会卸载GPU版PyTorch并替换为CPU版，同时降级transformers。详见报错10。</p></blockquote><h4 id="报错-8：脚本文件名混淆"><a class="headerlink" href="#报错-8：脚本文件名混淆"></a>报错 8：脚本文件名混淆</h4><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python: can&#x27;t open file &#x27;/root/llama.cpp/convert-hf-to-gguf.py&#x27;: [Errno 2] No such file or directory</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：尝试用横线版文件名<code>convert-hf-to-gguf.py</code>，实际文件是下划线版。中间一度被建议改用横线名，属于误判——<code>ls</code>确认实际文件名后应以<code>ls</code>结果为准。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> ~/llama.cpp/convert*.py    <span class="comment"># 以实际输出为准，不要凭记忆写文件名</span></span><br></pre></td></tr></table></figure><h4 id="报错-9：词表写入失败（本次最核心的失败）"><a class="headerlink" href="#报错-9：词表写入失败（本次最核心的失败）"></a>报错 9：词表写入失败（本次最核心的失败）</h4><p><strong>触发命令</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python llama.cpp/convert_hf_to_gguf.py LLaMA-Factory/models/chenxinyu-qwen2.5-7b-chat \</span><br><span class="line">  --outfile models/chenxinyu-f16.gguf --outtype f16</span><br></pre></td></tr></table></figure><p><strong>现象</strong>：权重转换阶段完全正常，逐层输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">INFO:hf-to-gguf:token_embd.weight, torch.bfloat16 --&gt; F16, shape = &#123;3584, 152064&#125;</span><br><span class="line">INFO:hf-to-gguf:blk.0.attn_norm.weight, torch.bfloat16 --&gt; F32, shape = &#123;3584&#125;</span><br><span class="line">...</span><br><span class="line">INFO:hf-to-gguf:gguf: context length = 32768</span><br><span class="line">INFO:hf-to-gguf:Set model quantization version</span><br><span class="line">INFO:hf-to-gguf:Set model tokenizer        &lt;-- 在此处崩溃</span><br></pre></td></tr></table></figure><p><strong>错误原文（完整因果链，三层异常）</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Traceback (most recent call last):</span><br><span class="line">  File &quot;/root/llama.cpp/conversion/qwen.py&quot;, line 63, in set_vocab</span><br><span class="line">    self._set_vocab_sentencepiece()</span><br><span class="line">  File &quot;/root/llama.cpp/conversion/base.py&quot;, line 2262, in _set_vocab_sentencepiece</span><br><span class="line">    tokens, scores, toktypes = self._create_vocab_sentencepiece()</span><br><span class="line">  File &quot;/root/llama.cpp/conversion/base.py&quot;, line 2279, in _create_vocab_sentencepiece</span><br><span class="line">    raise FileNotFoundError(f&quot;File not found: &#123;tokenizer_path&#125;&quot;)</span><br><span class="line">FileNotFoundError: File not found: LLaMA-Factory/models/chenxinyu-qwen2.5-7b-chat/tokenizer.model</span><br><span class="line"></span><br><span class="line">During handling of the above exception, another exception occurred:</span><br><span class="line">  ...</span><br><span class="line">  File &quot;/usr/local/miniconda3/envs/py310/lib/python3.10/site-packages/torch/_inductor/runtime/hints.py&quot;, line 40, in &lt;module&gt;</span><br><span class="line">    import triton.backends.compiler</span><br><span class="line">ModuleNotFoundError: No module named &#x27;triton.backends&#x27;</span><br><span class="line"></span><br><span class="line">The above exception was the direct cause of the following exception:</span><br><span class="line">  ...</span><br><span class="line">ModuleNotFoundError: Could not import module &#x27;AutoTokenizer&#x27;. Are this object&#x27;s requirements defined correctly?</span><br></pre></td></tr></table></figure><p><strong>排查定位（三层因果）</strong>：</p><ol><li><strong>第一层</strong>：<code>qwen.py:63</code>先尝试SentencePiece路径<code>_set_vocab_sentencepiece()</code>，去找<code>tokenizer.model</code>；Qwen2.5导出目录只有<code>tokenizer.json</code>，故<code>FileNotFoundError</code>。</li><li><strong>第二层</strong>：代码捕获该异常后 fallback到<code>_set_vocab_gpt2()</code>（<code>qwen.py:65</code>），其内部执行<code>from transformers import AutoTokenizer</code>（<code>base.py:1756</code>）。</li><li><strong>第三层</strong>：此时transformers 4.57.6加载链<code>generation/utils.py</code>→<code>torch._dynamo</code>→<code>torch._inductor</code>→<code>import triton.backends.compiler</code>失败，抛出<code>No module named 'triton.backends'</code>，最终包装成<code>Could not import module 'AutoTokenizer'</code>。</li></ol><p><strong>根因不是「缺AutoTokenizer」，而是torch与triton版本不匹配。</strong></p><h4 id="报错-10：环境被-requirements-txt污染"><a class="headerlink" href="#报错-10：环境被-requirements-txt污染"></a>报错 10：环境被 requirements.txt污染</h4><p><strong>触发命令</strong>：<code>pip install -r llama.cpp/requirements.txt</code>（在训练环境<code>py310</code>中）</p><p><strong>错误原文</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Uninstalling aiohttp-3.14.5: Successfully uninstalled aiohttp-3.14.5</span><br><span class="line">Attempting uninstall: transformers</span><br><span class="line">Found existing installation: transformers 5.8.0</span><br><span class="line">Uninstalling transformers-5.8.0: Successfully uninstalled transformers-5.8.0</span><br><span class="line">ERROR: pip&#x27;s dependency resolver does not currently take into account all the packages that are installed.</span><br><span class="line">This behaviour is the source of the following dependency conflicts.</span><br><span class="line">torchvision 0.29.1 requires torch&gt;=2.14.0, but you have torch 2.11.0+cpu which is incompatible.</span><br><span class="line">Successfully installed aiohttp-3.9.5 distro-1.9.0 gguf-0.19.0 huggingface-hub-0.36.2 ...</span><br><span class="line">torch-2.11.0+cpu transformers-4.57.6 ...</span><br></pre></td></tr></table></figure><p><strong>排查定位</strong>：llama.cpp的转换依赖声明了<code>torch==2.11.0</code>（CPU版）与<code>transformers==4.57.6</code>，pip为满足约束<strong>卸载了GPU版torch</strong>，装上CPU版。后果：训练/推理GPU不可用，且triton 2.1.0与新torch不匹配，直接触发报错9。</p><p><strong>解决过程（恢复 GPU 环境）</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pip uninstall -y torch torchvision torchaudio</span><br><span class="line">pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121</span><br></pre></td></tr></table></figure><p><strong>结论（本文最重要的一条）</strong>：<strong>绝不在训练环境里<code>pip install -r llama.cpp/requirements.txt</code>。</strong> GGUF转换必须在独立venv中进行。</p><h3 id="6-3-路径C：独立venv-官方两步法（唯一可行路径）"><a class="headerlink" href="#6-3-路径C：独立venv-官方两步法（唯一可行路径）"></a>6.3 路径C：独立venv + 官方两步法（唯一可行路径）</h3><p><strong>依据</strong>：llama.cpp<code>tools/quantize/README.md</code>原文 —— <em>「Quantization is done in two phases: Convert the original model to GGUF format. Quantize the converted GGUF file.」</em></p><p><strong>关键事实（源码级核验，<code>convert_hf_to_gguf.py</code>argparse定义）</strong>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;--outtype&quot;</span>, <span class="built_in">type</span>=<span class="built_in">str</span>,</span><br><span class="line">choices=[<span class="string">&quot;f32&quot;</span>, <span class="string">&quot;f16&quot;</span>, <span class="string">&quot;bf16&quot;</span>, <span class="string">&quot;q8_0&quot;</span>, <span class="string">&quot;tq1_0&quot;</span>, <span class="string">&quot;tq2_0&quot;</span>, <span class="string">&quot;auto&quot;</span>], default=<span class="string">&quot;auto&quot;</span>,</span><br><span class="line"><span class="built_in">help</span>=<span class="string">&quot;output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, tq1_0 or tq2_0 for ternary, and auto for the highest-fidelity 16-bit float type&quot;</span></span><br></pre></td></tr></table></figure><p><code>--outtype</code><strong>不接受任何K-quant类型</strong>（无 q2_k / q3_k / q4_k_m / q5_k_m / q6_k）。K-quant必须交给<code>llama-quantize</code>。</p><blockquote><p><strong>对原流程的修正</strong>：原对话中建议的<code>python convert_hf_to_gguf.py ... --outtype q4_k_m</code>会被argparse直接以<code>invalid choice</code>拒绝，不可能产出q4_k_m。该建议错误。</p></blockquote><p><strong>第一步：HF → F16 GGUF（在独立venv中执行）</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1) 建独立环境——llama.cpp 的 requirements 会装 CPU 版 torch，绝不能进训练 env</span></span><br><span class="line">python -m venv ~/ggufenv</span><br><span class="line"><span class="built_in">source</span> ~/ggufenv/bin/activate</span><br><span class="line">pip install -r ~/llama.cpp/requirements.txt</span><br><span class="line"><span class="comment"># 官方说明：requirements 默认装 transformers 4；新模型可能需要 transformers 5，可安全升级</span></span><br><span class="line">pip install -U transformers</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2) 转换（只转 dtype，不做 K-quant）</span></span><br><span class="line">python ~/llama.cpp/convert_hf_to_gguf.py \</span><br><span class="line">  ~/LLaMA-Factory/models/chenxinyu-qwen2.5-7b-chat \</span><br><span class="line">  --outfile ~/models/chenxinyu-f16.gguf \</span><br><span class="line">  --outtype f16</span><br></pre></td></tr></table></figure><p><strong>可验证结果</strong>：日志末行<code>Model successfully exported to ...</code>；此前会输出<code>INFO:hf-to-gguf:gguf: context length = 32768</code>。<strong>第6.2节的报错9就发生在<code>Set model tokenizer</code>之后、<code>write()</code>完成之前</strong>——即在venv中仍需保证transformers能正常import（见报错10）。</p><p><strong>第二步：F16 → Q4_K_M（<code>llama-quantize</code>二进制）</strong></p><p>官方命令形态（<code>tools/quantize/README.md</code>原文示例）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 二进制路径为 cmake 编译产物；也可下载 release 预编译包</span></span><br><span class="line">./build/bin/llama-quantize gemma-4-E2B-it-bf16.gguf gemma-4-E2B-it-Q4_K_M.gguf Q4_K_M</span><br></pre></td></tr></table></figure><p>对应到本文路径：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/llama.cpp</span><br><span class="line">cmake -B build &amp;&amp; cmake --build build --config Release -j $(<span class="built_in">nproc</span>)   <span class="comment"># 产出 build/bin/llama-quantize</span></span><br><span class="line">./build/bin/llama-quantize \</span><br><span class="line">  ~/models/chenxinyu-f16.gguf \</span><br><span class="line">  ~/models/chenxinyu-Q4_K_M.gguf \</span><br><span class="line">  Q4_K_M                                    <span class="comment"># 类型名大小写不敏感，官方文档两种写法均出现</span></span><br></pre></td></tr></table></figure><p>常用选项（官方清单）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">--imatrix imatrix.gguf                       <span class="comment"># 重要性矩阵，官方称可降低 ppl/KLD 损失</span></span><br><span class="line">--leave-output-tensor                        <span class="comment"># output.weight 不量化，增大体积换质量</span></span><br><span class="line">--pure                                       <span class="comment"># 禁用 k-quant 混合，全张量同一类型</span></span><br><span class="line">--output-tensor-type q5_k --token-embedding-type q3_k   <span class="comment"># 分张量指定精度</span></span><br><span class="line">--keep-split                                 <span class="comment"># 按输入分片输出，便于大模型</span></span><br></pre></td></tr></table></figure><p><strong>可验证结果</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -lh ~/models/*.gguf</span><br><span class="line"><span class="comment"># 用 gguf 包读元数据，确认架构与量化类型写入正确</span></span><br><span class="line">python -c <span class="string">&quot;from gguf import GGUFReader; r=GGUFReader(&#x27;chenxinyu-Q4_K_M.gguf&#x27;); \</span></span><br><span class="line"><span class="string">print(r.fields[&#x27;general.architecture&#x27;], r.fields[&#x27;general.file_type&#x27;])&quot;</span></span><br></pre></td></tr></table></figure><p><strong>体积依据</strong>（官方<code>tools/quantize/README.md</code>，Llama-3.1-8B实测表）：</p><table><thead><tr><th>类型</th><th>bits/weight</th><th>体积 (GiB)</th></tr></thead><tbody><tr><td>F16</td><td>16.0005</td><td>14.96</td></tr><tr><td>Q8_0</td><td>8.5008</td><td>7.95</td></tr><tr><td>Q6_K</td><td>6.5633</td><td>6.14</td></tr><tr><td>Q5_K_M</td><td>5.7036</td><td>5.33</td></tr><tr><td><strong>Q4_K_M</strong></td><td><strong>4.8944</strong></td><td><strong>4.58</strong></td></tr><tr><td>Q4_K_S</td><td>4.6672</td><td>4.36</td></tr></tbody></table><blockquote><p>7B Qwen2.5词表152064（大于Llama-3的128256），embedding层更大，<strong>Q4_K_M实际体积预计略高于4.58 GiB，约4.7~5.0 GiB。该数字为外推估算，待实测验证。</strong></p></blockquote><h3 id="6-4-路径D：Ollama直接导入-——-可作为部署手段，但不能产出量化模型"><a class="headerlink" href="#6-4-路径D：Ollama直接导入-——-可作为部署手段，但不能产出量化模型"></a>6.4 路径D：Ollama直接导入 —— 可作为部署手段，但<strong>不能产出量化模型</strong></h3><p><strong>官方依据</strong>（<code>ollama/ollama``docs/import.mdx</code>）明确支持两种导入：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方式一：Safetensors 目录</span></span><br><span class="line"><span class="keyword">FROM</span> /path/to/safetensors/directory</span><br><span class="line"><span class="comment"># 方式二：单文件 GGUF</span></span><br><span class="line"><span class="keyword">FROM</span> /path/to/file.gguf</span><br><span class="line"><span class="comment"># 方式三：分片 GGUF（保留原始分片名，用通配符）</span></span><br><span class="line"><span class="keyword">FROM</span> /path/to/model-*.gguf</span><br></pre></td></tr></table></figure><p><strong>同一文档中的决定性一句</strong>：</p><blockquote><p><em>&quot;Ollama does not quantize GGUF models during import. Prepare and quantize them first with a GGUF tool such as llama.cpp's<code>llama-quantize</code>.&quot;</em></p></blockquote><p><strong>结论</strong>：Ollama在<code>ollama create</code>阶段<strong>不做任何量化</strong>。导入safetensors目录只会得到bf16精度的可运行模型，<strong>不满足本文「量化模型版本」的目标</strong>。</p><p><strong>因此路径D的正确定位</strong>：验证合并模型推理效果的快速手段，而非量化路径。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在含 Modelfile 的目录下（Modelfile 由 LLaMA-Factory 导出时自动生成）</span></span><br><span class="line"><span class="comment"># Changelog 依据：hiyouga/LLaMA-Factory &quot;[25/02/11] We supported saving the Ollama modelfile</span></span><br><span class="line"><span class="comment"># when exporting the model checkpoints.&quot;</span></span><br><span class="line"><span class="built_in">cd</span> ~/LLaMA-Factory/models/chenxinyu-qwen2.5-7b-chat</span><br><span class="line">ollama create chenxinyu -f Modelfile      <span class="comment"># 内部等价于 FROM ./safetensors 目录</span></span><br><span class="line">ollama run chenxinyu</span><br></pre></td></tr></table></figure><p><strong>若要得到量化版 Ollama 模型</strong>，必须先在6.3产出GGUF，再改写 Modelfile：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> /path/to/chenxinyu-Q4_K_M.gguf</span><br></pre></td></tr></table></figure><hr><h2 id="7-量化产物的验证方法"><a class="headerlink" href="#7-量化产物的验证方法"></a>7. 量化产物的验证方法</h2><table><thead><tr><th>验证维度</th><th>方法</th><th>判定标准</th></tr></thead><tbody><tr><td>文件完整性</td><td><code>ls -lh</code>体积落在预期区间</td><td>官方Llama-3.1-8B基准：F16 14.96 / Q8_0 7.95 / Q5_K_M 5.33 / <strong>Q4_K_M 4.58</strong> / Q4_K_S 4.36 GiB。Qwen2.5-7B词表152064 &gt;Llama-3的128256，预计Q4_K_M ≈ 4.7~5.0 GiB（<strong>外推估算，待实测</strong>）</td></tr><tr><td>元数据正确</td><td><code>GGUFReader</code>读<code>general.architecture</code>/<code>general.file_type</code></td><td>应为<code>qwen2</code>/ 对应 quant 类型枚举值</td></tr><tr><td>上下文长度</td><td>元数据<code>context_length</code></td><td>应为32768，与转换日志<code>gguf: context length = 32768</code>一致</td></tr><tr><td>可加载</td><td><code>./build/bin/llama-cli -m &lt;gguf&gt; -p &quot;...&quot;</code>或<code>ollama run</code></td><td>无报错即通过</td></tr><tr><td>语义一致性</td><td>同一prompt分别喂bf16原模型与Q4_K_M，对比输出</td><td>主题与结论一致，允许措辞差异</td></tr><tr><td>风格保真</td><td>用训练语料中的典型问法测试</td><td>口癖、标点习惯是否保留（<strong>本次未通过，见下</strong>）</td></tr><tr><td>精度损失量化</td><td>llama.cpp官方提供ppl / KLD度量，<code>--imatrix</code>可降低损失</td><td><strong>本文未执行</strong></td></tr></tbody></table><p><strong>本次的最终状态</strong>：第4、5节成功；第6节量化<strong>未产出成品GGUF</strong>，即目标产物未达成。已完成的验证止于「合并模型可加载推理」。</p><p><strong>未通过项</strong>（如实记录）：合并模型实际对话时「说话不像本人」。已定位的四类嫌疑（按可能性排序）：</p><ol><li>清洗阶段误删口癖/标点习惯等风格指纹；<br>2.<code>lora_rank 8</code>对强风格拟合偏保守；</li><li>推理<code>temperature 0.7</code>偏低、<code>max_new_tokens 512</code>过长，把语气拉回「助手腔」；</li><li>1,729条数据中长对话被<code>cutoff_len 1024</code>切尾。</li></ol><hr><h2 id="8-核验依据清单（8-1已核验-8-2待验证）"><a class="headerlink" href="#8-核验依据清单（8-1已核验-8-2待验证）"></a>8. 核验依据清单（8.1已核验 / 8.2待验证）</h2><h3 id="8-1-已核验（含依据来源）"><a class="headerlink" href="#8-1-已核验（含依据来源）"></a>8.1 已核验（含依据来源）</h3><table><thead><tr><th>#</th><th>内容</th><th>结论</th><th>依据</th></tr></thead><tbody><tr><td>1</td><td><code>--outtype</code>是否支持<code>q4_k_m</code></td><td><strong>不支持</strong>。合法值仅<code>f32/f16/bf16/q8_0/tq1_0/tq2_0/auto</code></td><td><code>ggml-org/llama.cpp</code>master<code>convert_hf_to_gguf.py</code>argparse源码</td></tr><tr><td>2</td><td>量化是否必须两步</td><td><strong>是</strong>。官方明写two phases：先转 GGUF，再<code>llama-quantize</code></td><td><code>llama.cpp/tools/quantize/README.md</code></td></tr><tr><td>3</td><td><code>llama-quantize</code>命令签名</td><td><code>./build/bin/llama-quantize &lt;in.gguf&gt; &lt;out.gguf&gt; Q4_K_M [nthread]</code>；二进制为cmake编译产物</td><td>同上</td></tr><tr><td>4</td><td>requirements 的 transformers 版本</td><td>默认装transformers 4，官方称可安全<code>pip install -U transformers</code>升到5</td><td>同上</td></tr><tr><td>5</td><td>体积参考</td><td>Llama-3.1-8B：F16 14.96 / Q8_0 7.95 / Q5_K_M 5.33 / <strong>Q4_K_M 4.58</strong> GiB</td><td>同上（官方实测表）</td></tr><tr><td>6</td><td>LLaMA-Factory「导出量化等级」的真实语义</td><td><strong>GPTQ位数量化</strong>，选项仅<code>none/8/4/3/2</code>（<code>GPTQ_BITS = [&quot;8&quot;,&quot;4&quot;,&quot;3&quot;,&quot;2&quot;]</code>），依赖<code>optimum&gt;=1.24.0</code>+<code>gptqmodel&gt;=2.0.0</code>，产出GPTQ safetensors，<strong>不是GGUF</strong></td><td><code>webui/components/export.py:37,121</code>+<code>model/model_utils/quantization.py:143</code></td></tr><tr><td>7</td><td>下拉框置灰的确切条件</td><td><code>can_quantize()</code>中<code>checkpoint_path</code>为<strong>非空 list</strong>（多选checkpoint）时返回<code>interactive=False</code>。与llama.cpp组件无关</td><td><code>webui/components/export.py:40-44</code></td></tr><tr><td>8</td><td><code>0.9.6.dev0</code>是否为发布版</td><td><strong>不是</strong>。PyPI latest = 0.9.5，GitHub 最新 tag = v0.9.5；<code>0.9.6.dev0</code>是<code>pip install -e .</code>时setuptools_scm推断的开发态快照号</td><td><code>gh api repos/hiyouga/LLaMA-Factory/tags</code>+<code>pypi.org/pypi/llamafactory</code></td></tr><tr><td>9</td><td>Ollama导入时是否量化</td><td><strong>不量化</strong>。官方原句见6.4节</td><td><code>ollama/ollama``docs/import.mdx</code></td></tr><tr><td>10</td><td>Ollama支持的导入格式</td><td>safetensors目录 / 单文件 GGUF / 分片GGUF（<code>model-*.gguf</code>）三种</td><td>同上</td></tr></tbody></table><h3 id="8-2-待验证"><a class="headerlink" href="#8-2-待验证"></a>8.2 待验证</h3><table><thead><tr><th>#</th><th>内容</th><th>备注</th></tr></thead><tbody><tr><td>1</td><td>Qwen2.5-7B（词表152064）Q4_K_M的实际体积</td><td>外推4.7~5.0 GiB，需实测</td></tr><tr><td>2</td><td>Qwen2转换时<code>tokenizer.model</code>缺失的官方推荐解法</td><td>本文以「保证transformers可正常import」绕过，未查官方说明</td></tr><tr><td>3</td><td><code>0.9.6.dev0</code>快照与 master 分支的逐 commit 差异</td><td>本文源码依据均取自master，未与当时快照比对</td></tr><tr><td>4</td><td>合并前后参数量差20,185,088的口径解释</td><td>待核验</td></tr><tr><td>5</td><td>Loss末端反弹的成因解释</td><td>推断，待验证</td></tr><tr><td>6</td><td>Q4_K_M相对bf16的风格保真损失量化（ppl / KLD）</td><td>官方提供<code>--imatrix</code>与ppl度量方法，<strong>本文未执行</strong></td></tr></tbody></table><hr><h2 id="9-报错总表与关键认知修正"><a class="headerlink" href="#9-报错总表与关键认知修正"></a>9. 报错总表与关键认知修正</h2><table><thead><tr><th>#</th><th>阶段</th><th>触发命令</th><th>错误原文（摘）</th><th>根因</th><th>解决</th></tr></thead><tbody><tr><td>1</td><td>安装</td><td><code>pip install -e .[metrics]</code></td><td><code>Package 'llamafactory' requires a different Python: 3.10.16 not in '&gt;=3.11.0'</code></td><td>0.9.6.dev0要求Python ≥3.11</td><td><code>conda create -n py311</code>后重装</td></tr><tr><td>2</td><td>数据</td><td>训练启动</td><td><code>Dropped invalid example: []</code></td><td>数据集格式配置与文件不匹配</td><td>核对<code>formatting</code>/<code>tags</code>，用<code>Num examples</code>复验</td></tr><tr><td>3</td><td>训练</td><td>cutoff 2048 / bs 4</td><td><code>CUDA out of memory. Tried to allocate 2.38 GiB ... 20.92 GiB memory in use</code></td><td>反向传播logits梯度峰值</td><td>降 cutoff / batch</td></tr><tr><td>4</td><td>训练</td><td>cutoff 2048 / bs 2</td><td><code>Tried to allocate 2.32 GiB ... 21.04 GiB memory in use</code></td><td>同上，最长样本触发</td><td>同上</td></tr><tr><td>5</td><td>训练</td><td>cutoff 2048 / bs 2 + gc</td><td>同 #4</td><td>单样本峰值不受gc影响</td><td>cutoff降到1024</td></tr><tr><td>6</td><td>训练</td><td>二次启动同目录</td><td><code>Output directory already exists and is not empty</code></td><td>安全拦截</td><td><code>--overwrite_output_dir True</code>或换目录</td></tr><tr><td>7</td><td>量化</td><td><code>convert_hf_to_gguf.py</code></td><td><code>can't open file '/root/llama.cpp/convert_hf_to_gguf.py'</code></td><td>未clone仓库</td><td><code>git clone ggml-org/llama.cpp</code></td></tr><tr><td>8</td><td>量化</td><td>横线文件名</td><td><code>can't open file '.../convert-hf-to-gguf.py'</code></td><td>文件名记错</td><td>以<code>ls</code>结果为准</td></tr><tr><td>9</td><td>量化</td><td>转换写词表</td><td><code>FileNotFoundError: ... tokenizer.model</code>→<code>No module named 'triton.backends'</code>→<code>Could not import module 'AutoTokenizer'</code></td><td>torch/triton/transformers版本链断裂</td><td>恢复GPU torch与匹配triton</td></tr><tr><td>10</td><td>量化</td><td><code>pip install -r requirements.txt</code></td><td><code>torchvision 0.29.1 requires torch&gt;=2.14.0, but you have torch 2.11.0+cpu</code></td><td>requirements用CPU torch覆盖GPU torch</td><td>独立venv；或重装cu121 torch</td></tr><tr><td>11</td><td>导出</td><td>Export 页选量化等级</td><td>下拉框置灰，不可选</td><td><strong>多选了checkpoint</strong>（<code>can_quantize()</code>对非空 list 返回<code>interactive=False</code>）；且该选项本身是GPTQ位数量化，非GGUF</td><td>走路径C</td></tr><tr><td>12</td><td>认知</td><td><code>--outtype q4_k_m</code></td><td>argparse<code>invalid choice: 'q4_k_m'</code>（未被触发，因该命令从未执行成功）</td><td>把GGUF K-quant与convert的dtype转换混为一谈</td><td>改两步法：<code>--outtype f16</code>+<code>llama-quantize Q4_K_M</code></td></tr><tr><td>13</td><td>认知</td><td>认为<code>ollama create</code>会量化</td><td>无报错，但产物仍为bf16</td><td>Ollama官方明写导入时不量化</td><td>先用<code>llama-quantize</code>产出GGUF，再改写 Modelfile</td></tr><tr><td>14</td><td>认知</td><td>认为「导出量化等级」= GGUF量化</td><td>无报错</td><td>该选项实为GPTQ（<code>none/8/4/3/2</code>）</td><td>明确区分GPTQ与GGUF两条路线</td></tr></tbody></table><p><strong>附：本文相对原流程的三处硬修正（均有源码/官方文档依据）</strong></p><p>1.<code>--outtype q4_k_m</code>不可行——<code>convert_hf_to_gguf.py</code>的合法值仅<code>f32/f16/bf16/q8_0/tq1_0/tq2_0/auto</code>。<br>2. LLaMA-Factory的「导出量化等级」不是GGUF——它是GPTQ位数量化，选项只有<code>none/8/4/3/2</code>。<br>3.<code>ollama create</code>不量化——官方原句：<em>&quot;Ollama does not quantize GGUF models during import.&quot;</em></p>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/15/myself-quantized/</id>
    <link href="https://u2tt.github.io/2026/10/15/myself-quantized/"/>
    <published>2026-10-15T17:00:00.000Z</published>
    <summary>量化自己，通过算力卡</summary>
    <title>量化自己</title>
    <updated>2026-10-16T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="blog" scheme="https://u2tt.github.io/tags/blog/"/>
    <category term="随笔" scheme="https://u2tt.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
    <content>
      <![CDATA[<p>在脑子不清醒的时候，处于一个曲水流觞，醉酒当歌，浩浩乎如冯虚御风的状态。<br>写下一些无厘头的、神奇的语言。<br>是成不了兰亭集序那样的精彩诗篇的。<br>总归是写下的内容，总归是有价值的，总归是有用的。</p><blockquote><p>“自由”一词，在政治学领域已经泛滥了，所以先澄清一下：<br>本文所说的“自由”，主要偏重于和“人权”及“民权”相关的自由——比如：思想的自由、言论的自由、支配私有财产的自由、使用网络的自由……<br>——<a href="https://program-think.blogspot.com/2015/08/Technology-and-Freedom.html">编程幻想 《“对抗专制、捍卫自由”的N种技术力量》</a></p></blockquote><p>“翻墙是违法行为。”<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p><p>于是，一堵墙便有了它的正当性。</p><p>至于墙的另一边有什么，人民应该知道什么，不应该知道什么，似乎不再需要由人民自己决定。</p><p>我们被告知，墙外的信息并不可靠，境外势力居心叵测，互联网需要管理，社会需要稳定。</p><p>这些理由听起来似乎无懈可击。</p><p>一个人有可能被谎言欺骗，也有能力辨别真假,<br>一个人可能受到错误观点的影响，也可以通过阅读与思考修正自己的判断。</p><p>为什么不让人们自己判断？<br>为什么要在接触信息之前，就预先划定一道边界？</p><p>如果人民有能力独立思考，那么就应当允许接触信息、比较观点、作出判断；如果一种观点只能依靠隔绝其他信息来维持，那么这种做法究竟是在保护，还是在限制？</p><p>互联网本应连接世界。人们可以借此了解不同的文化，阅读不同国家的新闻，接触不同的思想，讨论彼此的生活。</p><p>但这堵墙让这些事情不再理所当然。</p><p>它限制的不仅是网站的访问，更是信息流动的范围。当某些新闻、观点与讨论难以进入人们日常接触的信息环境时，人们能够了解到什么，便在一定程度上受到这种环境的支配。</p><p>而一个人无法了解的事情，往往也很难成为他思考与讨论的对象。</p><br><p>《中华人民共和国宪法》规定：“中华人民共和国公民有言论、出版、集会、结社、游行、示威的自由。”<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p><p>或许吧。</p><p>在国内的社交平台上，一条评论在发布后悄无声息地消失；一篇帖子突然被删除；一个账号突然被封禁。</p><p>你留下了一句实话，刷新页面后，它便不复存在，仿佛从未被说出口。</p><p>官方媒体在发布新闻时，直接关闭评论区。<br>新闻照常发布，报道照常播出，官方的声音可以迅速传遍网络，而公众却连留下意见的机会都没有。</p><p>官方发布什么、如何发布、发布多少，普通人却未必有权回应。<br>官方媒体把自己的声音传遍网络，公众却可能连一句反驳、质疑或不满都无法留下。</p><p>评论区和弹幕里刷屏的不见活人的👍👍👍、🌹🌹🌹、🫰🫰🫰。<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup></p><p>有些词不能出现，有些人物不能直接提及。</p><p>维尼熊是敏感词，包子也是。<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup></p><p>于是，人们开始学会绕开语言本身。</p><p>不能说名字，就换一个称呼；不能直接表达，就使用谐音、代称和隐喻。不能公开讨论，就把真正想说的话藏进看似无关的句子里。</p><p>有些词，只有了解背景的人才能看懂。<br>荒诞的句子，实际上是在讨论一个无法被直接讨论的话题。</p><p>语言并没有消失，只是被逼得越来越隐晦。</p><p>不能直呼其名，就用代称；不能直接批评，就用谐音；不能公开讨论，就用隐喻、反讽和看似无关的词语，把真正想说的话藏进去。</p><p>于是，维尼熊、包子等，便不再只是普通的词语。它们成了暗号，成了被删掉的名字重新返回公共空间的方式，也成了人们在高压审查之下彼此辨认、彼此回应的语言。</p><p>人们只能故意说得不那么直接，故意留下一点模糊，让机器无法准确识别，让审核者难以轻易下手，也让真正想表达的人能够看懂其中的意思。</p><p>人们当然知道哪些词可能触发审查，哪些句子可能导致删帖，哪些话题可能让账号被禁言。<br>但他们并没有因此放弃表达，而是在不断试探边界：换一个字，改一个音，拆开一个词，借用一个表情，写一句表面无关、实际上人人都明白的话。</p><p>审查越严密，暗语就越复杂；禁忌越多，隐喻就越密集。</p><p>荒诞、含糊甚至莫名其妙的表达，往往并不是没有意义，而是因为真正的意义不能被直接写出来。人们用擦边的词汇绕开封锁，用隐晦的方式传递愤怒，用玩笑包裹批评，用表面的荒诞保存真实的记忆。</p><p>不是没人敢说，而是不能用原本的方式说。</p><p>把反抗藏进谐音，把讽刺藏进玩笑，把质疑藏进一句看似无害的话里。只要还有人能够看懂，只要这些话还能在审查的缝隙中流动，官方试图制造的沉默就没有真正完成。</p><p>被删掉的是字面，无法被彻底删除的，是人们想要表达的意志。</p><p><strong>人民过得快乐吗？</strong><br>官方说，人民是快乐的。<br>那么，我就快乐吧。</p><p>毕竟，新闻里的经济持续增长，城市里高楼林立，宣传片中的人民安居乐业，生活似乎一天比一天富足。至于现实中的人究竟如何感受，生活里又有多少疲惫、困惑与不如意，似乎并没有那么重要。</p><p>只要宏大的叙事足够完整，个体的感受便可以被放到一边；只要宣传中的生活足够明亮，现实中的阴影就仿佛不值一提。</p><p>如果有人提出质疑。<br>那么，问题定不在现实本身，而在于这个人“不够积极”“不懂感恩”或者“被错误思想影响”。</p><p>官方说人民幸福，所以人民就必须幸福；<br>官方说社会稳定，所有不满都必须被解释为少数人的问题；<br>官方说一切都在变好，那么那些感到失望、疲惫和无力的人，就只能怀疑自己。</p><p>所谓的言论自由，不只是法律条文中一句看上去庄严的承诺，也不只是允许你在没有触碰权力和禁区的情况下说几句无关痛痒的话。</p><p>你可以赞美，但不能质疑；可以歌颂，但不能追问；可以转发官方结论，却不能提出不同答案。</p><br><blockquote><p>每一個人，既是時代的英雄，亦是時代的一粒沙。那些青春的熱血和勇氣，不應該成為沒有人念的悼詞。那些追殺學生的軍人，也不會因為他們在執行任務而不是劊子手。<br>八九六四，不僅鑲嵌在中國與世界的歷史裡，這一場席捲了千萬人的運動，他們就是我們，就是我們身邊的每一個人。<br>——<a href="https://8964museum.com/">六四記憶‧人權博物館</a></p></blockquote><p>历史不该被遗忘。<br>至少，不应该只记住那些被允许记住的历史。</p><br><p>中国的政治教育，相较于资本主义国家，有着更强的存在感。<br>尤其是爱国主义教育。</p><p>中国人有爱国的义务。<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>。</p><p>爱国不是一种情感，而是一种政治立场。</p><br><p>“当然，行是行的，这固然很好，可是千万别闹出什么乱子。”<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup><br>别里科夫不过是一个希腊文教师。他能把自己的恐惧变成周围人的恐惧，却终究没有足够的权力，让整个小镇都按照他的意愿生活。<br>如果赋予他权利，整个小镇得回归封建社会。</p><p>当代中国的许多管理体系中，都能看到某种相似的逻辑：能不动就不动，能不改就不改。<br>即使眼前显然存在更好的解决方案，也未必有人愿意迈出一步。<br>改革意味着风险，意味着责任，意味着原有秩序可能受到冲击，维持现状，意味着安全，意味着稳定。<br>科技进步与我何干？<br>我只是个拿工资办事的。</p><p>于是，问题不再是谁能把事情做得更好，而是谁能保证不出乱子。</p><p>越来越多的别里科夫坐上了更高的位置。他们不再只是把自己装进套子里，而是试图把整个社会也装进去。他们守着陈旧的规则，维护着僵化的秩序，把不出错视为最大的成绩，把不改变视作最稳妥的选择。</p><p>至于那些更好的方案，那些不同的声音，那些可能打破现状的尝试——</p><p>一棒子打死。</p><p>解决提出问题的人可比解决问题简单多了。</p><br><p>官僚体系的保守，让一个社会难以向前迈进。<br>对思想与信息的控制，则能让人们逐渐失去判断它究竟有没有向前迈进的能力。</p><p>文字狱实在是太聪明了。<br>知识分子不敢讲，普通人接触不到，讨论者担心后果，记录者逐渐沉默。<br>人们不知道自己不知道什么，更不知道那些从未被允许讨论的事情究竟意味着什么。没有比较，就很难辨别优劣；没有完整的信息，就很难形成独立的判断。当某段历史从课本、媒体与公共讨论中消失，它甚至不必被所有人否认，只需要不再被人提起，就足以逐渐淡出一代人的记忆。</p><p>经济发展可能因此错失机会，科技进步可能因此受到阻碍，社会也可能因为无法及时纠正错误而付出更大的成本。<br>但这又有什么关系呢？</p><p>1949年建立的“新中国”，名义上是“人民共和国”，实质上是“党天下”。<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup></p><p>如果统治的目标不是让社会变得更好，而是让现有秩序尽可能长久地维持下去，那么发展迟缓或许也算不上最重要的问题。<br>毕竟，一个人只有见过更好的生活，才会意识到自己正在忍受怎样的生活；一个人只有了解不同的历史，才可能追问为什么自己的历史只能有一种讲法。<br>封闭信息，至少可以让这些问题更晚出现。<br>有些历史必须被遗忘，有些作品必须被封堵，有些声音必须被消除。</p><p>至于<a href="https://zh.wikipedia.org/wiki/%E5%85%AD%E5%9B%9B%E4%BA%8B%E4%BB%B6">六四</a>、<a href="https://zh.wikipedia.org/wiki/%E7%99%BD%E7%B4%99%E9%81%8B%E5%8B%95">白纸</a>这样的事件，更是需要被尽可能地隔离于公共记忆之外。</p><p>倘若它们只是毫无意义的闹剧，只是经不起推敲的谣言，又何必如此忌惮人们去了解它们？</p><p>可有些事情偏偏不能说，有些问题偏偏不能问，有些记忆偏偏不能留下。</p><p>于是，我们被要求热爱国家，却未必被允许完整地认识它，<br>被要求铭记历史，却未必能够自由地讨论历史，<br>被要求相信现有秩序是最好的，却又被隔绝在那些可能证明它并非最好的信息之外。</p><br><p>历史不该被遗忘。</p><p>那些曾经站出来的人，不应该只存在于某些人的私人记忆里；那些曾经发生过的事，也不应该因为不再被提起，就仿佛从未发生。</p><p>而遗忘最可怕的地方，或许就在于：当一代人终于忘记了那些曾经发生过的事情，他们甚至可能不再明白，自己究竟失去了什么。</p><br><blockquote><p>我们的存在，就是对恶意最大的反抗。<br>——<a href="https://mtf.wiki">MtF.wiki</a></p></blockquote><p>2026 年，我开始频繁地刷X。</p><p>这是一个消磨时间的地方。</p><p>我可以浏览各种信息，看新闻，看讨论，看一些与自己的生活并无太大关联的事情。国内那些令人不安的近况，内网难以看到的新闻，以及来自不同角落的声音，都在这里交织在一起。</p><p>大多数内容看过便忘了，偶尔遇到感兴趣的，也不过是停留片刻。</p><p>后来，我开始频繁地刷到彩虹旗<sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup>。</p><p>起初只是偶然。一个帖子，一张图片，几句陌生的讨论。我不太了解其中的含义，却总会下意识地点进去看看。</p><p>看得越多，算法便推送得越多。就这样，一个过去对我而言相当陌生的群体，逐渐进入了我的视野。</p><p>我开始了解LGBT群体，接触跨性别者的经历，阅读那些关于性别认同、自我接纳与社会偏见的讨论。我看见了许多与自己原先想象不同的生活，也发现，自己过去所不知道的事情，远比想象中多得多。</p><p>开始真正了解起她们，事情便不再只是好奇那么简单。</p><p>我开始翻看她们过去的帖子，点进某个头像，却发现这个账号已经许久没有更新。而最上面那条帖子的评论里，却满是“晚安”。</p><p>有时，还能看到讣告。</p><p>有人留下了告别的文字，有人甚至没能留下什么。一个曾经活跃的人，就这样成为了他人记忆中的一部分。</p><p>我逐渐意识到，在这些看似平常的网络交流背后，存在着一些远比帖子本身沉重的现实。</p><p>对于一些MtF而言，生活并不是简单地做自己，然后等待他人理解。</p><p>她们可能要面对家人的不理解、周围人的歧视，以及求学、就业、医疗与社会关系中的种种阻碍。她们需要花费巨大的精力，才能争取到一些原本不该如此艰难的东西：被认真对待，按照自己的意愿生活，拥有一个可以期待的未来。</p><p>而在缺少可靠支持的环境中，连寻找信息也可能成为一道难关。许多经验散落在个人帖子里，许多问题只能自己摸索，走过的弯路也未必有人告诉后来者。</p><p>但我也因此看到了另一面。</p><p><a href="https://mtf.wiki">MtF.wiki</a></p><p><a href="https://one-among.us/">One Among Us</a></p><p><a href="https://rle.wiki/">RLE.wiki</a></p><p>………</p><p>这些网站，成为我认识这个领域的入口。</p><p>有人将自己的经历写下来，有人整理资料、分享经验，有人维护网站，不断修正和补充内容。那些资料并非凭空出现，其中有许多来自真实的生活，有些甚至凝结着并不愉快的过去。</p><p>她们将自己曾经摸索过的道路，整理成后来者可以阅读的文字。</p><p>我想，这是一件很值得敬佩的事情。</p><p>我无法仅凭几篇帖子，就真正理解她们所经历的全部艰难。但我能看见那些文字中的疲惫，也能看见有人在自己同样不易的时候，依然试着帮助另一个陌生人。</p><p>她们未必是什么意义上的英雄。她们只是普通人，会难过，会疲惫，会抱怨生活，也会因为一些小事感到高兴。她们不应该被要求永远坚强。</p><p>但我敬佩那些在困境中仍然努力生活的人，敬佩那些曾经得到过帮助，于是决定将帮助传递下去的人，也敬佩那些默默整理资料、分享经验，为后来者留下路标的人。</p><p>这些事情或许无法立刻改变现实，也无法保证每个人都能等到生活好起来的那一天。</p><p>而这恰恰是我最难过的地方。</p><p>不是每一个人都能等到帮助抵达，也不是每一个人都能等到自己期待的未来。</p><p>我不知道每一个人的全部故事，不能轻率地将每一次离去归结于某个原因。但当相似的告别一次次出现在视野中，我便很难再把它们当作普通的网络消息，划过去，然后忘记。</p><p><strong>每一个名字，都曾经对应着一个具体的人。</strong></p><p>她也曾期待过一段关系，认真地学习某种技能，兴致勃勃地分享一件小事。她也可能只是希望明天比今天好一点。</p><p>这些愿望并不宏大。</p><p>普通到不应该成为奢望。</p><p>我原本只是想了解一个群体，后来却开始在意那些人的命运。</p><p>我希望她们能够得到更多支持与理解，能够学习、工作、爱人，也能够被爱。<br>我希望那些仍在寻找答案的人，能够找到可靠的资料，那些感到孤独的人，能够遇见愿意倾听自己的人。</p><p>我也希望，那些正在努力生活的人，不必总是在解释自己、保护自己与寻找退路之间耗尽力气。</p><p>我不知道这样的日子究竟何时才能到来。</p><p>但我希望它能够到来。</p><p>愿离去的人被记得，愿仍在路上的人能够平安地走下去。</p><p>愿她们终有一天，不必再仅仅为了存在本身而反抗。</p><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>《中华人民共和国计算机信息网络国际联网管理暂行规定》 <a href="#fnref1" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p>《中华人民共和国宪法》 <a href="#fnref2" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p>见抖音、bilibil等平台的人名日报评论区 <a href="#fnref3" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p>见维基百科中“對習近平的負面稱呼”栏目 <a href="#fnref4" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p>《中华人民共和国宪法》第五十四条 <a href="#fnref5" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p>契诃夫 《装在套子里的人》 <a href="#fnref6" class="footnote-backref">↩︎</a></p></li><li id="fn7" class="footnote-item"><p>刘晓波《零八宪章》 <a href="#fnref7" class="footnote-backref">↩︎</a></p></li><li id="fn8" class="footnote-item"><p>这里指的是以跨性别旗为背景的账号头像🏳️‍⚧️ <a href="#fnref8" class="footnote-backref">↩︎</a></p></li></ol></section>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/06/xia-ji-ba-xie/</id>
    <link href="https://u2tt.github.io/2026/10/06/xia-ji-ba-xie/"/>
    <published>2026-10-06T10:00:00.000Z</published>
    <summary>在脑子不清醒的时候写一些不是写的东西。ps：好废话的表达（貌似看不到`description`呢）</summary>
    <title>浑浑噩噩的写一些乱七八糟的东西i</title>
    <updated>2026-10-06T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <category term="备份" scheme="https://u2tt.github.io/tags/%E5%A4%87%E4%BB%BD/"/>
    <content>
      <![CDATA[<blockquote><p><a href="https://zh.wikipedia.org/wiki/%E9%98%AE%E6%99%93%E5%AF%B0">阮晓寰</a>（1977年6月10日—），中华人民共和国IT行业从业者、博客作家、异见人士。其从2009年开始在Blogger上以“编程随想”为名撰写文章，内容包括各种网络安全知识、翻墙方法、匿名教学以及政治评论。2021年5月10日，阮晓寰被上海警方以涉嫌煽动颠覆国家政权罪逮捕。2023年2月10日，上海市第二中级人民法院以煽动颠覆国家政权罪判处其有期徒刑7年、剥夺政治权利2年，并处罚金2万元；2024年12月13日，上海市高级人民法院驳回上诉并维持原判。</p><p>——维基百科</p></blockquote><br><p>编程随想<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>的<a href="https://program-think.blogspot.com/">博客</a>和<a href="https://program-think.blogspot.com/2021/05/share-books.html">电子书库</a>在国内长期被GFW封禁。</p><p>原站在墙内打不开，收录方式一直是靠托管平台的备份加<code>BT Sync</code>同步。</p><p>自2021被捕后，博客永远的就停在了那一刻。</p><p>26年的今天，我还能打开<a href="https://program-think.blogspot.com/">博客</a>，访问他的<a href="https://github.com/programthink">GitHub</a>，访问他的<a href="https://x.com/programthink">X</a>，在我看来，这如同一个奇迹一般。因为在网上见识过太多太多的被ban掉的内容，我不得不考虑做一份备份记录一下。</p><p>备份主要内容：分享的书籍和博客。</p><h2 id="统计"><a class="headerlink" href="#统计"></a>统计</h2><p>博客正文710篇，2009到2021年4月。</p><p>标签里政治279篇、时事评论175篇、信息安全100篇、历史84篇、翻墙50篇。</p><p>电子书2146个文件。</p><p>按一级目录分11类：IT、军事、历史、哲学、心理学、政治、文艺、社会学、科普、管理、经济。</p><table><thead><tr><th style="text-align:center">PDF</th><th style="text-align:center">EPUB</th><th style="text-align:center">MOBI</th><th style="text-align:center">TXT</th><th style="text-align:center">CHM</th><th style="text-align:center">ZIP</th><th style="text-align:center">AZW3</th><th style="text-align:center">DJVU</th></tr></thead><tbody><tr><td style="text-align:center">1316</td><td style="text-align:center">379</td><td style="text-align:center">204</td><td style="text-align:center">99</td><td style="text-align:center">76</td><td style="text-align:center">40</td><td style="text-align:center">15</td><td style="text-align:center">9</td></tr></tbody></table><p>扫描件占绝大多数，同一本书常存着两三种格式，所以文件数比书目数多得多。</p><p>总文件超过16 GB,一共需要分17次推完。</p><p>10.04凌晨02:25开始，04:28推完，17次提交。</p><p>需要按体积切批，GitHub每次提交总量不能大于2 GB，必须切分上传。</p><p>第一批把blog那6162个小文件凑够1.44 GB。后面13批全是books，一百多到三百多个不等，每批是1.4 GB上下。间隔五到七分钟。</p><p>不过好在没有超过100 MB的文件，否则就不好上传了</p><p>HEAD上所有文件的字节加起来<code>16.19 GB</code>。<code>.git</code>目录达到15 GiB：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">count: 2489      size: 13.98 GiB</span><br><span class="line">packs: 1         size-pack: 1.22 GiB</span><br></pre></td></tr></table></figure><h2 id="使用"><a class="headerlink" href="#使用"></a>使用</h2><p>访问作者bolg或通过直接打开<code>blog/html/index.html</code>使用。</p><p>CHM已经编译好，依赖<code>hhc.exe</code>和<code>hha.dll</code>，<code>file</code>判定是1999年的PE32 Intel 80386二进制，只支持Windows。生成<code>.hhp</code>/<code>.hhc</code>配置的脚本不在仓库里，EPUB同理，想重新生成需要自己找工具链。</p><p><code>__所有电子书的清单__.html</code>和<code>所有电子书目录的密钥__.txt</code>都是描述的下载链接，已经分根据密钥通过Resilio Sync下载到了books，下载方法和指导详见作者<a href="https://github.com/programthink/books">GitHub</a>。</p><h2 id="公开"><a class="headerlink" href="#公开"></a>公开</h2><p>仓库：<a href="https://github.com/u2tt/programthink-Backup">programthink-Backup</a></p><p>目前是public，可供自由下载。<s>被ban掉前</s></p><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>阮晓寰 <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/04/programthink-backup/</id>
    <link href="https://u2tt.github.io/2026/10/04/programthink-backup/"/>
    <published>2026-10-04T11:20:00.000Z</published>
    <summary>「编程随想」的博客全文和电子书库，搬进自己的仓库做备份</summary>
    <title>编程随想的博客和书籍备份</title>
    <updated>2026-10-04T12:15:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <category term="备份" scheme="https://u2tt.github.io/tags/%E5%A4%87%E4%BB%BD/"/>
    <category term="OneDrive" scheme="https://u2tt.github.io/tags/OneDrive/"/>
    <content>
      <![CDATA[<p>习惯做多份备份。</p><p>上学期间，每学期都会产生很多docx、pptx、pdf等，或许是结课论文，或许是课堂汇报ppt，或许是要交的材料文件，总爱做一份备份。<br>既是记录生活，也是存下过去。</p><p>由于我有两台电脑，而且手机也是也需要查看，所以云端必不可少。<br>往往还需要发给别人看，需要发送链接。</p><p>国内那些云盘有多难用，我就没必要赘述了。</p><p>过去一直用的<code>OneDrive</code>。但只有5GB免费空间，同步又需要走微软服务器，需要常驻后台进程，还会有延迟和强占带宽。</p><p>如果两台电脑同时开着同一个docx更麻烦。同步回来多出一个名字带(2)的副本，哪个是新的得自己判断。</p><p>最好的记录方式当然是<code>git</code><s>实在是小题大做</s>。</p><p>但是既然可取，何不试试？<br>私有仓库，564个文件，711.1MiB，12个提交。</p><h2 id="拦截大文件"><a class="headerlink" href="#拦截大文件"></a>拦截大文件</h2><p>由于GitHub的限制，单文件不可以超过100MB，所以做了一个钩子来避免上传超过100MB的文件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/your/path/cloud-storage/.git/hooks/pre-commit</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">MAX_SIZE=$((<span class="number">100</span> * <span class="number">1024</span> * <span class="number">1024</span>))   <span class="comment"># 100MB</span></span><br><span class="line">git diff --cached --name-only --diff-filter=ACM | <span class="keyword">while</span> <span class="built_in">read</span> f; <span class="keyword">do</span></span><br><span class="line">  size=$(<span class="built_in">stat</span> -f%z <span class="string">&quot;<span class="variable">$f</span>&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$size</span>&quot;</span> -gt <span class="string">&quot;<span class="variable">$MAX_SIZE</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    git restore --staged <span class="string">&quot;<span class="variable">$f</span>&quot;</span>     <span class="comment"># 移出暂存区，不阻止提交</span></span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"><span class="built_in">exit</span> 0</span><br></pre></td></tr></table></figure><h2 id="文件组成"><a class="headerlink" href="#文件组成"></a>文件组成</h2><table><thead><tr><th style="text-align:center">类型</th><th style="text-align:center">个数</th><th style="text-align:center">体积</th></tr></thead><tbody><tr><td style="text-align:center">mp4</td><td style="text-align:center">3</td><td style="text-align:center">193.8 MiB</td></tr><tr><td style="text-align:center">pdf</td><td style="text-align:center">74</td><td style="text-align:center">184.0 MiB</td></tr><tr><td style="text-align:center">ppt / pptx</td><td style="text-align:center">34</td><td style="text-align:center">107.7 MiB</td></tr><tr><td style="text-align:center">docx</td><td style="text-align:center">63</td><td style="text-align:center">98.8 MiB</td></tr><tr><td style="text-align:center">doc</td><td style="text-align:center">30</td><td style="text-align:center">52.0 MiB</td></tr><tr><td style="text-align:center">jpg</td><td style="text-align:center">317</td><td style="text-align:center">66.2 MiB</td></tr><tr><td style="text-align:center">md</td><td style="text-align:center">14</td><td style="text-align:center">0.2 MiB</td></tr></tbody></table>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/04/cloud-storage/</id>
    <link href="https://u2tt.github.io/2026/10/04/cloud-storage/"/>
    <published>2026-10-03T19:00:00.000Z</published>
    <summary>把OneDrive里的课程资料整个交给Git之后，`.git`长到了670MB。拆一遍对象库：那次562个文件重命名一个字节都没多写，真正占地方的是一次被reset掉的测试提交留下的110MB。</summary>
    <title>学期资料备份GitHub</title>
    <updated>2026-10-03T19:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <category term="macOS" scheme="https://u2tt.github.io/tags/macOS/"/>
    <category term="开源" scheme="https://u2tt.github.io/tags/%E5%BC%80%E6%BA%90/"/>
    <content>
      <![CDATA[<p>macOS系统的桌面壁纸是一些240帧的视频，但只能在屏幕保护界面播放。</p><p>那么高清好看的壁纸居然不可以作为动态壁纸播放！太糟糕了！</p><p>市面上有很多动态壁纸的软件，但是他们大多软件功能复杂，操作繁琐，价格高昂，语言不便，并且自带下载视频，而不是播放系统壁纸视频。</p><p>项目已在GitHub开源，地址：<a href="https://github.com/u2tt/Lumina">Lumina</a></p><p>9个Swift文件，2995行，Xcode Command Line Tools编译，无需引第三方库。</p><p>同步发布软件包，可以直接下载使用。</p><p>第一次运行会自动寻找系统自带的Aerial素材，在<code>~/Library/Application Support/com.apple.wallpaper/aerials/videos/</code>下面，缓存的壁纸文件mov路径，也可以自备视频。</p><h2 id="软件制作困难"><a class="headerlink" href="#软件制作困难"></a>软件制作困难</h2><h3 id="遮挡"><a class="headerlink" href="#遮挡"></a>遮挡</h3><p>视频壁纸并未直接替代原壁纸图片，而是选择新建图层覆盖。</p><p>macOS的桌面有多层窗口。用<code>CGWindowListCopyWindowInfo</code>dump出来如下：</p><table><thead><tr><th style="text-align:center">层级</th><th style="text-align:left">owner</th></tr></thead><tbody><tr><td style="text-align:center">-2147483624</td><td style="text-align:left">程序坞，系统自己的壁纸</td></tr><tr><td style="text-align:center">-2147483603</td><td style="text-align:left">访达，桌面图标，铺满整块屏</td></tr><tr><td style="text-align:center">-2147483601</td><td style="text-align:left">通知中心小组件</td></tr><tr><td style="text-align:center">0</td><td style="text-align:left">普通应用窗口</td></tr></tbody></table><p>动态壁纸层需要位于系统壁纸层与桌面图标层之间。最终取值为<code>-2147483610</code>，比图标层低7层。</p><p>设置中存在名为“壁纸盖住桌面图标”的开关。</p><p>4K视频持续解码，需要在电池供电时暂停。电池状态通过IOKit获取：调用<code>IOPSCopyPowerSourcesInfo</code>获取电源信息列表，读取<code>kIOPSPowerSourceStateKey</code>；若其值等于<code>kIOPSBatteryPowerValue</code>，则判定为电池供电。</p><p>全屏模式检测不使用辅助功能。改为遍历窗口列表中<code>layer == 0</code>的窗口，取叠放顺序最靠前的窗口，计算其与屏幕的交集面积；若覆盖面积超过98.5%，则判定为全屏。</p><p>遮挡检测同样遍历窗口列表，但选项不同。窗口列表按叠放顺序提供，索引越靠前表示层级越高。因此需要查找位于壁纸窗口之前、<code>layer &gt;= 0</code>且覆盖面积超过98%的窗口。此处不能添加<code>.excludeDesktopElements</code>；添加后会将壁纸窗口本身过滤掉，导致遮挡检测始终返回未被遮挡。全屏检测可以添加该选项，因为该场景本应忽略桌面层。</p><p>锁屏和屏幕休眠通过通知处理。在<code>DistributedNotificationCenter</code>上监听<code>com.apple.screenIsLocked</code>和<code>com.apple.screenIsUnlocked</code>，并在<code>NSWorkspace</code>上监听<code>screensDidSleep</code>和<code>screensDidWake</code>；事件到达时立即置位。电池和全屏状态每2秒轮询一次，仅在状态实际翻转时回调。</p><p>满足暂停条件时，日志记录一行“暂停播放（全屏应用 / 电池供电）”，并并列列出原因。</p><p>每块屏幕对应一个独立的<code>NSWindow</code>，通过遍历<code>NSScreen.screens</code>完成创建。插拔显示器或更改分辨率会发送<code>didChangeScreenParametersNotification</code>，且一次操作会连续发送多次；因此增加0.6秒防抖后再重建。</p><p>切换Space或激活其他应用后，偶尔会出现窗口不在当前Space的情况。在<code>didActivateApplicationNotification</code>和<code>activeSpaceDidChangeNotification</code>之后延迟0.35秒再次调用<code>orderFrontRegardless</code>。该数值经实验确定；若延迟更短，系统尚未完成窗口位置调整。</p><h3 id="倍速"><a class="headerlink" href="#倍速"></a>倍速</h3><p>AVPlayer在2×以上存在硬性上限。使用macOS自带Aerial航拍素材测试（3840x2160、HEVC Main 10、239.76 fps），2.0×可稳定达到120帧/秒；调整至2.2×后降至每秒0至1帧，<code>AVPlayerItem.canPlayFastForward</code>也为false。仅相差0.2×即完全停止。</p><p>规避方法是将倍速预置到时间轴。使用<code>AVMutableComposition</code>将整段视频<code>scaleTimeRange</code>压缩至1/倍速 的时长，播放器仅以1×播放。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> compressed <span class="operator">=</span> <span class="type">CMTimeMultiplyByFloat64</span>(asset.duration, multiplier: <span class="number">1.0</span> <span class="operator">/</span> speed)</span><br><span class="line">track.scaleTimeRange(full, toDuration: compressed)</span><br></pre></td></tr></table></figure><p>时间轴压缩后，修改倍速必须重建内容，而重建会导致播放进度归零。修正方法是在重建前记录源视频进度（<code>currentTime × bakedSpeed</code>），重建后seek到<code>position / factor</code>。</p><p>另有一个较不显著的问题：将“是否需要预置倍速”表示为布尔值并纳入配置指纹。3×、4×、5×均对应true，配置签名不变，因此不触发重建，始终按首次的3×播放。改为比较实际倍速值后行为正确。</p><p>重建还会导致短暂黑屏。<code>replaceCurrentItem</code>会清空图层缓冲。先移除旧层再创建新层，期间必然出现黑屏。使用旧层快照进行遮挡也不可行，因为<code>AVPlayerLayer.contents</code>恒为空，播放器不通过该通道输出。</p><p>最终采用双缓冲。保持两个<code>(AVPlayer, AVPlayerLayer)</code>实例，在不可见实例上更换item、执行seek并开始播放，轮询直到<code>isReadyForDisplay</code>为真且时间轴推进（最多等待1秒），然后交换可见性；此时才暂停旧实例并调用<code>replaceCurrentItem(with: nil)</code>释放解码器。轮播切换复用同一路径，手动“下一幅”同样不会出现黑屏。</p><p>另有一处与倍速相关的问题：<code>AVPlayer.play()</code>等价于<code>rate = 1.0</code>，因此先设置<code>rate</code>再调用<code>play()</code>会覆盖速度。表现为初始速度正确，但暂停后恢复时速度变为1×。所有起播路径统一改为<code>playImmediately(atRate:)</code>。</p><p>高倍速下卡顿是另一类问题。4×下出现周期性卡顿。最初认为由解码能力不足导致，但实测时间推进准确，CPU占用仅2.3%。实际原因是帧生成速率与上屏速率不匹配：30 fps素材在4×下需要120帧/秒，而AppKit窗口默认不以ProMotion的120 Hz满速运行，需要显式申请。macOS不提供<code>CALayer.preferredFrameRateRange</code>，该API属于iOS；只能使用<code>NSView.displayLink</code>。声明后可通过回调频率验证，日志可输出<code>display link实测120.5 Hz</code>。</p><p>5×属于物理限制。30 fps素材在5×下需要150帧/秒，120 Hz屏幕最多显示120帧/秒，必然丢帧。设置界面按“屏幕刷新率 ÷ 源帧率”计算该素材的最高无损倍速，超过该值时以橙色标记，无需逐档测试。</p><h3 id="开机自启"><a class="headerlink" href="#开机自启"></a>开机自启</h3><p>开机自启配置写入<code>~/Library/LaunchAgents/com.chenxinyu.lumina.plist</code>。其中<code>ProgramArguments</code>包含<code>--autostart</code>，设置<code>RunAtLoad</code>，并将<code>LimitLoadToSessionType</code>设为<code>Aqua</code>。执行<code>bootstrap</code>前先执行一次<code>bootout</code>；否则重复切换自启开关时会返回<code>service already loaded</code>。</p><p>应用采用ad-hoc签名（codesign --sign -），不包含开发者证书。用户下载并解压后，Gatekeeper会拦截。处理方式有两种：在“系统设置 → 隐私与安全性”中选择“仍要打开”；或执行<code>xattr -dr com.apple.quarantine /Applications/Lumina.app</code>。该操作应在构建脚本中一并执行。</p><p>此外，将<code>.app</code>组装在<code>mktemp</code>创建的临时目录中，项目内仅保留zip文件。macOS 26的App库依据LaunchServices注册表收录bundle；只要项目目录中曾出现<code>.app</code>，就会与<code>/Applications</code>中的副本共同注册，导致显示两个图标。</p>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/04/lumina/</id>
    <link href="https://u2tt.github.io/2026/10/04/lumina/"/>
    <published>2026-10-03T19:00:00.000Z</published>
    <summary>自己写了个把本地视频当macOS桌面壁纸的小工具。记下窗口层级怎么卡在桌面图标下面、暂停判定为什么要分四种写，以及倍速撞上AVPlayer的2×上限之后怎么绕过去。</summary>
    <title>macOS桌面动态壁纸</title>
    <updated>2026-10-03T19:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <category term="开源" scheme="https://u2tt.github.io/tags/%E5%BC%80%E6%BA%90/"/>
    <category term="协作" scheme="https://u2tt.github.io/tags/%E5%8D%8F%E4%BD%9C/"/>
    <content>
      <![CDATA[<p><a href="https://rle.wiki">RLE.wiki</a></p><p>「一份 RLE 指北」</p><p><code>Real Life Experience</code>实际生活体验。</p><blockquote><p>欢迎大家来到 RLE.wiki. 这是一份给跨性别者与性别多元群体的 RLE 指南。更多帮助正在路上，敬请期待……</p></blockquote><p>笔者从X得知的这个项目。在翻它的校园栏目的时候。有着一百七十多个学校，大部分城市都能翻出几条。但是没有我的大学。</p><p>怎么可以没有大农专！我写！</p><br><p>写了<code>docs/campus/NJAU.md</code>，162行。开头一张总览表，规则是情况正面用<code>✓</code>、负面用<code>✗</code>、不确定用<code>?</code>、视情况而定用<code>-</code>。</p><p>PR挂上去没多久，Twinsherry在下面留了句话，说LS-KR之前已经从这里撤过一次南京农业大学的条目，作为数据遗产资料的继承人，她希望把旧版本里LGBT相关的部分补进来。</p><p>我当天问了旧内容能在哪个commit或者归档仓库里查到，她给了个链接，PR 586，标题「doc: rm NJAU」，2025年5月合并的，改一个文件，纯删85行。</p><p><s>我一开始就是知道的，那是学姐。只是她后来想不开了，走之前rm了南农相关的文章。git历史我也是看到了的</s></p><br><p>旧版本能找到的东西就是那段差分里剩下的全部。</p><p>其中的LGBT内容和校内医疗相关的说法挑出来，一部分并进正文，部分核实不了的用<code>::: details 旧版资料</code>折叠起来挂在对应小节下面，标注了是上一版投稿人的描述。</p><blockquote><p>::: details旧版资料：上一版投稿人对宿舍搜查的描述（笔者没遇到过，认为是特殊情况，予以保留）</p><blockquote><p>辅导员或学院或学校会定期秘密地搜查学生的宿舍，频率为一个学期至少两次。同班所有人的物品均有被翻动的痕迹。<br>笔者从医院回到宿舍时发现自己的电脑已经是只剩一次机会否则全盘加密锁定的状态。<br>没有藏好的糖（还需要吃的那些）也被收走。<br>带锁是必须的，不要用密码锁。</p></blockquote><p>:::</p></blockquote><p>我没法验证。校医院能不能测六项、益丰大药房有没有可能出现二十多块一盒的补子。</p><p>第二个提交<code>f33b805</code>改了59行删了24行，把那张总览表重新排版对齐，另外补了几处我当时想起来的细节：军训16天、后半程有晚训、每天三个时段分性别训等等。</p><h2 id="审稿尺度"><a class="headerlink" href="#审稿尺度"></a>审稿尺度</h2><p>投稿指南里定的尺子很具体。</p><blockquote><ol><li>不得修改模板中的二级标题，如果认为不需要填写该项目，请注释掉。markdown的注释语法为<code>&lt;!-- --&gt;</code>，例如<code>&lt;!-- ## 其他信息 --&gt;</code>。</li><li>如有必要，可以自由添加多级标题。</li><li>为了观察方便，英文与汉字之间需要添加空格，例如<code>例子 example 例子</code>。</li><li>专有名词缩写注意大小写，例如<code>MtF</code>，具体可以参考<a href="https://mtf.wiki/zh-cn/docs/useful-info/abbreviation/">https://mtf.wiki/zh-cn/docs/useful-info/abbreviation/</a>。</li><li>主观内容请使用引用格式，例如<code>&gt; 引用内容</code>。</li><li>句末需要加上中文句号<code>。</code>。</li><li>如果有引用到静态文件，例如图片视频等，请在<code>docs/assets</code>目录下上传图片（以文档名开头），并在markdown文件中使用<strong>相对路径</strong>的方式引用。例如我的文档名为<code>example.md</code>，在其中引用一张名为<code>Test.png</code>的图片，那么我需要将<code>Test.png</code>改名为<code>example_Test.png</code>，上传在<code>docs/assets</code>目录下，然后在markdown里面使用<code>![](/assets/example_Test.png)</code>的方式引用。</li><li>对于过时内容的处理：<ul><li>影响力持续至今的事情，尽管其已经结束，但为了因果关系完整，仍应予以保留。（举例：2020年，某校因为COVID-19疫情防控措施而取消了某政策，至今没有恢复，但由于2022年12月的防控措施变化，而在近期存在恢复可能的，在等待两年（至2024年12月）、排除其恢复可能后，将“以前的政策”和“受COVID-19疫情防控措施影响”这两句按下方处理）</li><li>已无影响力，但影响力消失不足两年的，予以折叠、标注过时（使用<code>:::details :::</code>语法）。</li><li>已无影响力，影响力消失满两年的，予以删除。</li></ul></li></ol></blockquote><h2 id="部署"><a class="headerlink" href="#部署"></a>部署</h2><p>PR一开就有bot来贴预览地址，，每个PR独占一个Cloudflare Pages部署，改完刷新就能看到修改完的界面是什么样。</p><p><s>好高级！我都不知道GitHub居然还能这样</s></p>]]>
    </content>
    <id>https://u2tt.github.io/2026/10/04/rle-wiki/</id>
    <link href="https://u2tt.github.io/2026/10/04/rle-wiki/"/>
    <published>2026-10-03T19:00:00.000Z</published>
    <summary>记录一次往RLE.wiki提交校园条目的过程：从fork到PR 778合并，中间把一年多前被删掉的旧版本内容又接了回来。</summary>
    <title>给RLE.wiki添加农专院校</title>
    <updated>2026-10-03T19:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <category term="开源" scheme="https://u2tt.github.io/tags/%E5%BC%80%E6%BA%90/"/>
    <category term="SQL" scheme="https://u2tt.github.io/tags/SQL/"/>
    <content>
      <![CDATA[<p>不知道该学什么SQL，和AI交流下来，我一个非计算机本专业生，学习基础的，简单的SQLite好像就够了。<s>简单在哪里啦！</s></p><p>想练SQL一直卡在环境上。在线基本只有长文教程，本地装个数据库又麻烦，适配性不强。不如自己写一个：纯静态页面，双击HTML就能用，离线使用。</p><p>现在有50关，从<code>SELECT</code>一路排到递归CTE。每题写真正的SQL，由SQLite执行后比对结果集，对了才放行。</p><p>仓库在<a href="https://github.com/u2tt/sql-quest">u2tt/sql-quest</a>，在线版<a href="https://u2tt.github.io/sql-quest/">https://u2tt.github.io/sql-quest/</a>。</p><h2 id="底子"><a class="headerlink" href="#底子"></a>底子</h2><p>sql.js把SQLite编译成了WebAssembly，查询全在浏览器本地跑，数据不出机器。</p><p>麻烦在分发。常规做法是一个index.html加一堆js再加个640KB的<code>.wasm</code>，还得起HTTP服务——<code>file://</code>下<code>fetch</code>拿不到本地文件。我把wasm转成base64塞进js，<code>npm run build</code>出一个1.2MB的<code>sql-quest.html</code>，依赖全在里面。base64会让体积涨三分之一，换来的是不用起服务，值。</p><h2 id="题库"><a class="headerlink" href="#题库"></a>题库</h2><p>基础篇是书店业务：作者、书籍、客户、订单、订单明细五张表，25本书、30笔订单。数据里埋了几个坑。有个客户从没下过单，练<code>LEFT JOIN</code>的时候正好拿来分辨<code>COUNT(o.id)</code>和<code>COUNT(*)</code>——后者会把没订单的那行数成1。有本书的<code>genre</code>是NULL，<code>IS NULL</code>和相关子查询里的<code>IS</code>（不是<code>=</code>）都得用它。订单有已完成、已取消、待付款三种状态，聚合之前不先过滤就算错。</p><p>进阶篇换成校园选课，也是五张表，但<code>courses.prereq_id</code>指向同一张表的<code>id</code>，自连接和递归CTE有真实的层级能展开。另外特意留了5个一节课都没选的学生，不然<code>NOT EXISTS</code>这类题没有正确答案。</p><p>进阶关不难在语法，难在组合。表一多，得自己想清楚从哪张出发、怎么连过去；而且多半要拆两步，先算中间结果再基于它算答案。</p><p><img src="/img/sql-quest/03-advanced.jpg" alt="递归CTE展开课程先修链，右栏自动切到了校园选课库"></p><h2 id="判题"><a class="headerlink" href="#判题"></a>判题</h2><p>答案写法定不下来，没法做文本比对。每关存一份参考SQL，运行时现场算出期望结果，再和你的结果比。四种比法：<code>columns</code>是列名加无序行集合，<code>ordered</code>要求行顺序也对，<code>set</code>只看行集合，<code>scalar</code>比单个数值带浮点容差。</p><p>不通过时给差异清单。测<code>COUNT(*)</code>那个坑的时候，面板直接列出「冯磊0」和「冯磊1」两行，比说一句答案错误有用得多。</p><p><img src="/img/sql-quest/02-diff.jpg" alt="差异面板把写错的那一行直接摆出来"></p><p>另外加了关键字约束。题目要求用<code>LEFT JOIN</code>，你写死<code>WHERE name = '冯磊'</code>凑答案会被拦下。结果对了，但这题就白做了。</p><h2 id="编辑器"><a class="headerlink" href="#编辑器"></a>编辑器</h2><p>一开始就是个textarea。后来想加高亮和补全，发现两个都不好办。</p><p>高亮最后做成了「文字透明的textarea叠在一层<code>&lt;pre&gt;</code>上面」。选区、光标、输入法都还是textarea的原生行为，看到的高亮来自底下那层。两层必须像素对齐，否则文字会重影，所以排版规则只写一处、两层共用，另外textarea用<code>scrollbar-gutter: stable</code>常驻预留滚动条槽位，高亮层补上同样的右内边距，两层的内容盒才等宽。测试里卡的是内容盒宽度、<code>scrollHeight</code>和13项排版属性全等。</p><p>补全要贴着光标弹，得先知道光标在textarea里的像素坐标。用了个镜像div：复刻textarea的计算样式，把光标之前的文本灌进去，再插一个span量它的位置。好处是字号改了自动跟随，不用改代码。</p><p>排序按光标位置分。语句开头给关键字（<code>sel</code> → <code>SELECT</code>），<code>FROM</code>或<code>JOIN</code>后面给表名（<code>bo</code> → <code>books</code>），查询中段给列名（<code>st</code> → <code>stock</code>），打了<code>b.</code>就只给books的列。别名也认：写了<code>COUNT(*) AS book_count</code>，后面打<code>book_co</code>能补出来；<code>WITH city_stat AS (...)</code>之后能补CTE名；派生表后面打<code>b.ti</code>，能从内层SELECT推出<code>title</code>。</p><p><img src="/img/sql-quest/01-autocomplete.jpg" alt="JOIN后面只给表名，列名在那儿是不合法的"></p><p>格式化用sql-formatter。全量包312KB，带20多种方言，这项目只用SQLite，就写了个esbuild插件把方言注册表换成只含sqlite的版本，压到57KB。</p><h2 id="几个坑"><a class="headerlink" href="#几个坑"></a>几个坑</h2><p>WASM实例那块最要命。切关卡要恢复到该关的初始状态，我最初的写法是这样：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> old = <span class="variable language_">this</span>.<span class="property">db</span>;</span><br><span class="line"><span class="variable language_">this</span>.<span class="property">db</span> = <span class="keyword">new</span> <span class="variable language_">this</span>.<span class="property">SQL</span>.<span class="title class_">Database</span>(snapshot);</span><br><span class="line">old.<span class="title function_">close</span>();</span><br></pre></td></tr></table></figure><p><code>this.db</code>平时就是数据集注册表里的常驻实例，把旧的关掉，那套数据就永久废了——之后任何查询都抛<code>ErrnoError: FS error</code>，而且不可恢复。这个bug只在反复跨数据集切换时才触发，只做一套库的话永远碰不到。改成原地替换注册表引用就好了：新实例顶上，注册表指向新的，再关旧的。</p><p>CSS表格一次踩了仨。</p><p><code>max-width</code>对table cell不生效。我给<code>td</code>设了<code>max-width: 320px</code>想截断长文本，实测宽度360px。CSS 2.1里这个属性对表格单元格就是没用，最后把截断压在内层元素上。</p><p>数值列右对齐好看，但表头默认左对齐，两者能差50px以上。得先按列判断是不是数值列（该列所有非空值都是数字），给<code>th</code>和<code>td</code>加同一个class才对齐。这个问题之前一直没被测出来，因为我的几何断言只比了单元格<strong>盒子</strong>的坐标——盒子当然对得齐，但文字能差一大截。后来改用<code>Range</code>量文字的实际位置，按对齐方式比对应边缘。</p><p>还有<code>width: 100%</code>会把多余空间按比例摊给每一列。<code>id</code>列内容只要40px，被撑到90px，看着间距很怪。改成让最后一个文本列吸收剩余宽度——文本列变宽只有好处，数值列变宽只会让数字贴着右边、中间空一大截。</p><p><img src="/img/sql-quest/05-table.jpg" alt="改完之后：文本列左对齐、数值列右对齐，列宽按内容走"></p><p>还有个正则在补全里作祟。在<code>FROM orders o JOIN o</code>后面打下去，候选里给的是<code>order_date</code>（一个列名）、<code>ON</code>、<code>OR</code>、<code>ORDER BY</code>，偏偏没有<code>orders</code>。</p><p>原因是我用一条正则从<strong>第一个</strong><code>FROM</code>开始匹配：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">FROM orders o JOIN o</span><br><span class="line">└──── 整段被吞掉 ────┘</span><br></pre></td></tr></table></figure><p><code>JOIN</code>落在被吞掉的部分里，被当成「这个子句里已经有别的关键字了」，于是判定失败、退化成列名上下文。改成取最后一个引导词再往后看就行。顺带把表名位置的候选也收干净了——那儿本来就只有表名合法，现在<code>JOIN order_d</code>干脆什么都不给，而不是递上一个<code>order_date</code>误导人。</p><h2 id="测试"><a class="headerlink" href="#测试"></a>测试</h2><p>这类工具有个麻烦：错了不一定看得出来。所以写了一套脚本，改完必跑。<code>verify</code>检查50关的参考答案能不能过自己的校验、起始代码会不会被误判成通关；<code>e2e</code>是浏览器端到端的46项；<code>editor</code>管格式化、补全、高亮那56项；<code>fullplay</code>把50关在浏览器里逐个打通；<code>offline</code>验单文件版在<code>file://</code>加断网下能不能跑。</p><p>最后一个是这次新加的：<code>live</code>，部署完实测线上站点能不能真答题。之前只看HTTP 200就以为发布成功了，其实接口通了不代表页面能跑。</p><p><img src="/img/sql-quest/06-allclear.jpg" alt="浏览器里把50关逐个打通之后"></p><h2 id="仓库"><a class="headerlink" href="#仓库"></a>仓库</h2><p>代码、题库、两套示例库都在<a href="https://github.com/u2tt/sql-quest">u2tt/sql-quest</a>。不构建也能用，<code>dist/sql-quest.html</code>下载下来双击就开，放U盘或者发给别人都行。两套库也单独导出了<code>dist/bookstore.sql</code>和<code>dist/campus.sql</code>，可以拖进DBeaver自己练。MIT协议。</p><p>主题做了深浅两套，浅色不是简单反色，强调色压暗到<code>#9a6200</code>，六类语法色在白底上的对比度都在4.8:1以上。</p><p><img src="/img/sql-quest/04-light.jpg" alt="浅色主题"></p>]]>
    </content>
    <id>https://u2tt.github.io/2026/09/27/sql-quest/</id>
    <link href="https://u2tt.github.io/2026/09/27/sql-quest/"/>
    <published>2026-09-26T18:45:00.000Z</published>
    <summary>一个在浏览器里真跑SQL的闯关练习工具。50道关卡、两套示例库，SQLite编译成WebAssembly本地执行。记几个实现细节和踩到的坑：WASM实例的生命周期、表格对齐的三个CSS陷阱、贪婪正则让JOIN后的补全失效。</summary>
    <title>自制SQL学习工具，闯关</title>
    <updated>2026-09-26T19:25:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="硬件" scheme="https://u2tt.github.io/tags/%E7%A1%AC%E4%BB%B6/"/>
    <category term="DIY" scheme="https://u2tt.github.io/tags/DIY/"/>
    <category term="嵌入式" scheme="https://u2tt.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>RP2040开发板，做嵌入式开发</p><p>拿它做点能跑起来的东西，配了一块1.8寸屏：屏幕、电池、主控、充电板。</p><p><img src="/img/rp2040/01-pi-demo-wiring.jpg" alt="桌面原型：YD-RP2040、1.8寸屏、TP4056与500mAh锂电用飞线连成一整套，屏幕正在显示圆周率"></p><p>项目开在<a href="https://github.com/u2tt/rp2040-txt-reader">u2tt/rp2040-txt-reader</a>，接线表在<code>hardware/PINOUT.md</code>，带图的分步制作指南在<code>docs/BUILD_GUIDE.zh-CN.md</code>。</p><h2 id="选型"><a class="headerlink" href="#选型"></a>选型</h2><p>对于板子的选择：</p><table><thead><tr><th>方案</th><th>卡在哪</th></tr></thead><tbody><tr><td>ESP32 + TFT_eSPI</td><td>经典Arduino路线，性能够，但放文件需要重编译烧录，或者开网页传，工具链麻烦</td></tr><tr><td>ESP32-C3 SuperMini</td><td>25元、硬币大小、自带Wi-Fi热点，手机连上用网页传文件——还是甩不开Arduino</td></tr><tr><td>ESP32 + I2C OLED</td><td>1.3寸OLED配U8g2，对比度好，但屏偏小，固件构建也重</td></tr><tr><td><strong>RP2040 + CircuitPython</strong></td><td>不用Wi-Fi：刷上固件后板载16MB闪存直接变成一块U盘，代码、字体、TXT拖进去就能用</td></tr></tbody></table><p>选RP2040最为方便：<strong>插上线就是U盘</strong>，驱动装一次就识别、不用开热点、不用编译器。</p><h2 id="物料清单"><a class="headerlink" href="#物料清单"></a>物料清单</h2><table><thead><tr><th>部件</th><th>型号 / 规格</th><th>尺寸</th><th>成本</th></tr></thead><tbody><tr><td>主控</td><td>YD-RP2040（16MB闪存版）</td><td>53.3 × 22.9 mm</td><td>¥9–11</td></tr><tr><td>屏幕</td><td>1.8寸TFT，ST7735，128×160 SPI</td><td>模块约56 × 34 mm</td><td>¥7–10</td></tr><tr><td>电池</td><td>503035聚合物锂电，3.7V 500mAh</td><td>约35 × 30 × 3.5 mm</td><td>¥5–8</td></tr><tr><td>充电保护</td><td>TP4056 Type-C模块</td><td>—</td><td>¥1–2</td></tr><tr><td>翻页键</td><td>6×6×5轻触微动开关×2</td><td>—</td><td>&lt;¥1</td></tr><tr><td>电源开关</td><td>SS12D00拨动开关（可选）</td><td>—</td><td>&lt;¥1</td></tr></tbody></table><p>自己焊接的话，主控要买<strong>未焊接排针的散件版</strong>，屏幕需要焊接好的，不能是裸板。</p><h2 id="结构"><a class="headerlink" href="#结构"></a>结构</h2><p><strong>屏幕（最底层，屏面朝外）→ 缓冲层 + 电池（中层）→YD-RP2040 + TP4056（顶层）</strong></p><p>主控53.3 × 22.9 mm能放在约56 × 34 mm的屏幕背后，电池夹在中间。</p><h3 id="缓冲层"><a class="headerlink" href="#缓冲层"></a>缓冲层</h3><p>屏幕背面有LDO、贴片阻容，有的批次还带SD卡座，凸起1–2mm。软包锂电池皮很薄，不能直接压上去。需要垫一些软的中层。</p><h2 id="焊接"><a class="headerlink" href="#焊接"></a>焊接</h2><h3 id="先拆掉排针"><a class="headerlink" href="#先拆掉排针"></a>先拆掉排针</h3><p>如果收到板子后者屏幕是已经焊好排针的版本，需要先拆下排针：</p><blockquote><ol><li>用斜口钳或尖嘴钳夹碎排针底部的黑色塑料座，让金属针彼此独立。</li><li>一手镊子夹住一根针，一手用电烙铁加热背面的焊点；锡化啦就轻轻往外抽。</li><li>吸锡器吸净孔内残锡，或者用牙签趁热捅穿——<strong>孔必须通透</strong>，后面要穿线。</li></ol></blockquote><h3 id="屏幕接线"><a class="headerlink" href="#屏幕接线"></a>屏幕接线</h3><p>剪8根约5cm的30AWG硅胶线，分别穿过屏幕底板那8个孔（VCC、GND、SCL、SDA、RES、DC、CS、BLK）焊死。</p><h3 id="电源链路"><a class="headerlink" href="#电源链路"></a>电源链路</h3><p>电池红线焊TP4056的<strong>B+</strong>，黑线焊<strong>B−</strong>。<strong>这两根正负极不能接反或短接</strong>。</p><p>接着OUT+出来先过拨动开关再过主控Vin，OUT−接主控GND。</p><h3 id="飞线对接"><a class="headerlink" href="#飞线对接"></a>飞线对接</h3><p>把屏幕上焊接的8根线从电池侧边翻上来，一根一根焊到主控对应引脚上。<strong>每焊一根都先比划长度</strong>：剪掉多余的、重新剥皮再焊，保证线贴着电池边缘走、不拱起来。</p><p>全部接完就是文章开头那张图的样子。</p><h3 id="按键"><a class="headerlink" href="#按键"></a>按键</h3><p>两个按键各自的一个引脚并联起来接GND，另一个引脚分别接GP16 / GP17。主控内部上拉，按下即拉低触发，不占地方也不用额外元件。</p><h2 id="接线表"><a class="headerlink" href="#接线表"></a>接线表</h2><h3 id="屏幕（1-8寸ST7735，走硬件SPI1）"><a class="headerlink" href="#屏幕（1-8寸ST7735，走硬件SPI1）"></a>屏幕（1.8寸ST7735，走硬件SPI1）</h3><table><thead><tr><th>屏幕引脚</th><th>接到YD-RP2040</th><th>作用</th></tr></thead><tbody><tr><td>VCC</td><td>3V3</td><td>屏幕逻辑供电</td></tr><tr><td>GND</td><td>GND</td><td>信号地</td></tr><tr><td>SCL / SCK</td><td>GP10</td><td>SPI硬件时钟</td></tr><tr><td>SDA / MOSI</td><td>GP11</td><td>SPI数据输出</td></tr><tr><td>RES / RST</td><td>GP12</td><td>屏幕硬件复位</td></tr><tr><td>DC / A0</td><td>GP13</td><td>数据 / 命令切换</td></tr><tr><td>CS / CE</td><td>GP14</td><td>SPI片选</td></tr><tr><td>BLK / LED</td><td>3V3</td><td>背光供电（常亮）</td></tr></tbody></table><p>板上通常只引出一个3V3孔，接两根线不方便。做法是<strong>在屏幕端把VCC和BLK短接</strong>，并联后共用一根飞线接到3V3，最整洁。</p><h3 id="电源链路（电池-→-充电板-→-开关-→-主控）"><a class="headerlink" href="#电源链路（电池-→-充电板-→-开关-→-主控）"></a>电源链路（电池 → 充电板 → 开关 → 主控）</h3><table><thead><tr><th>起点</th><th>终点</th><th>说明</th></tr></thead><tbody><tr><td>电池红线(+)</td><td>TP4056 B+</td><td>充电输入正极</td></tr><tr><td>电池黑线(−)</td><td>TP4056 B−</td><td>充电输入负极</td></tr><tr><td>TP4056 OUT−</td><td>主控GND</td><td>系统共地</td></tr><tr><td>TP4056 OUT+</td><td>拨动开关 引脚1</td><td>正极先过开关</td></tr><tr><td>拨动开关 引脚2</td><td>主控Vin</td><td>主板供电</td></tr><tr><td>TP4056 OUT+</td><td>主控Vin</td><td>不加开关时的直连方案</td></tr></tbody></table><h3 id="翻页按键"><a class="headerlink" href="#翻页按键"></a>翻页按键</h3><table><thead><tr><th>功能</th><th>一端</th><th>另一端</th><th>触发逻辑</th></tr></thead><tbody><tr><td>下一页</td><td>GP16</td><td>GND</td><td>按下拉低</td></tr><tr><td>上一页</td><td>GP17</td><td>GND</td><td>按下拉低</td></tr></tbody></table><img src="/img/rp2040/pico-pinout-dimensions.jpg" alt="Raspberry Pi Pico 引脚图与产品尺寸图" style="width:62%;display:block;margin:0 auto;"><h2 id="通电前检查"><a class="headerlink" href="#通电前检查"></a>通电前检查</h2><p>拨开关之前确认这几处：</p><ul class="contains-task-list"><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 剪断的金属废线头全部清理干净，没有掉进任何板子缝隙里。</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 从底板翻上来的导线外皮完好，没有被板边锋利焊盘割破露铜。</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 电池与任何金属针脚、焊点之间都有绝缘（双面胶或硬塑料片），没有被刺穿的可能。</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 有条件就上万用表蜂鸣档：该通的关键引脚确实通，相邻引脚没有意外短路。</li></ul><img src="/img/rp2040/02-repl-startup.jpg" alt="第一次上电：屏幕亮起并显示 CircuitPython 启动信息与提示文字" style="width:58%;display:block;margin:0 auto;"><h2 id="上电"><a class="headerlink" href="#上电"></a>上电</h2><p>接线正确、固件也烧好之后拨开关，屏幕会亮起来打印启动信息——这台跑的是CircuitPython 10.3.0，画面里的提示文字是固件自带的拼音化中文。</p><p>屏幕上出现字，说明SPI那五根线、复位、片选和供电正确。</p><img src="/img/rp2040/03-image-viewer.jpg" alt="另一个角度：主控电源灯亮起，屏幕显示一张 24 位 BMP 图片" style="width:80%;display:block;margin:0 auto;"><p>屏幕是彩屏，能显示标准24位BMP图片，仓库里<code>examples/image_viewer.py</code>可做测试。</p><h2 id="放文件：插上线就是U盘"><a class="headerlink" href="#放文件：插上线就是U盘"></a>放文件：插上线就是U盘</h2><ol><li>Type-C线插设备连电脑，会弹出一块名叫<code>CIRCUITPY</code>的U盘。</li><li>把UTF-8编码的<code>book.txt</code>拖进盘根目录，拔线。</li><li>拨开关开机，按「下一页」从第一页开始读。</li></ol><p>充电时线要插在<strong>TP4056的Type-C口</strong>，插主控口是直接给主控供电，不走充电板。</p><h2 id="固件"><a class="headerlink" href="#固件"></a>固件</h2><p>板子跑的是CircuitPython（仓库：<a href="https://github.com/adafruit/circuitpython">adafruit/circuitpython</a>）。屏幕驱动、中文字体、翻页逻辑一共216行，另外依赖三个库，都在Adafruit的仓库里：</p><ul><li><a href="https://github.com/adafruit/Adafruit_CircuitPython_ST7735R">Adafruit_CircuitPython_ST7735R</a> — 屏幕底层驱动</li><li><a href="https://github.com/adafruit/Adafruit_CircuitPython_Display_Text">Adafruit_CircuitPython_Display_Text</a> — 文本排版</li><li><a href="https://github.com/adafruit/Adafruit_CircuitPython_Bitmap_Font">Adafruit_CircuitPython_Bitmap_Font</a> — 加载点阵字体</li><li>想一次拿齐，用官方合集<a href="https://github.com/adafruit/Adafruit_CircuitPython_Bundle">Adafruit_CircuitPython_Bundle</a></li></ul><p>中文用的是BDF点阵字体，<a href="https://github.com/AmusementClub/WenQuanYi-Bitmap-Song-TTF">文泉驿点阵宋体</a>有现成的中文字形可以转。</p><p>注意：</p><ul><li>CircuitPython 9.x起<code>FourWire</code>从<code>displayio</code>拆成了独立的<code>fourwire</code>模块，照8.x教程写会报<code>AttributeError</code>，表现是<strong>背光亮但屏幕全黑</strong>——很容易误判成焊接问题。</li><li>这块屏的色序是BGR，建display时要显式<code>bgr=True</code>，不设颜色会反。</li></ul><h2 id="仓库"><a class="headerlink" href="#仓库"></a>仓库</h2><p>完整制作指南（选型 → 烧录 → 组装 → 排障）在<a href="https://github.com/u2tt/rp2040-txt-reader/blob/main/docs/BUILD_GUIDE.zh-CN.md">docs/BUILD_GUIDE.zh-CN.md</a>，接线表在<a href="https://github.com/u2tt/rp2040-txt-reader/blob/main/hardware/PINOUT.md">hardware/PINOUT.md</a>，代码与示例在仓库根目录。MIT协议，欢迎照抄。</p>]]>
    </content>
    <id>https://u2tt.github.io/2026/09/13/rp2040-txt-reader/</id>
    <link href="https://u2tt.github.io/2026/09/13/rp2040-txt-reader/"/>
    <published>2026-09-13T15:10:00.000Z</published>
    <summary>第一次碰RP2040开发板做的硬件实践：用YD-RP2040配1.8寸ST7735屏，从选型、三明治结构设计到焊接装配、接线与通电检查，材料不到30块。</summary>
    <title>RP2040开发板电路设计</title>
    <updated>2026-10-04T20:50:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="blog" scheme="https://u2tt.github.io/tags/blog/"/>
    <category term="Hexo" scheme="https://u2tt.github.io/tags/Hexo/"/>
    <category term="GitHub" scheme="https://u2tt.github.io/tags/GitHub/"/>
    <content>
      <![CDATA[<p>博客是静态站：Markdown写作，内容推送到仓库，GitHub Pages出站。<br>文章格式一直是Markdown加front-matter，三次迁移换的都是主题。下面是三次用过的引擎、目录结构和配置。</p><table><thead><tr><th></th><th>Hugo</th><th>Astro</th><th>Hexo</th></tr></thead><tbody><tr><td>语言</td><td>Go</td><td>Node</td><td>Node</td></tr><tr><td>安装</td><td>单二进制</td><td>npm</td><td>npm</td></tr><tr><td>站点配置</td><td>config.yml</td><td>astro.config.mjs</td><td>_config.yml</td></tr><tr><td>输出目录</td><td>public</td><td>dist</td><td>public</td></tr><tr><td>主题索引</td><td>themes.gohugo.io</td><td>astro.build/themes</td><td>hexo.io/themes</td></tr></tbody></table><h2 id="第一版：Hugo"><a class="headerlink" href="#第一版：Hugo"></a>第一版：Hugo</h2><p>Hugo用Go编写，安装是一个二进制文件，放进PATH即可使用，不需要Node环境。全站一个配置文件，文章按目录组织，主题放在themes目录（直接放或用git submodule管理）。front-matter支持TOML、YAML、JSON三种格式。当时用的版本是0.165.0。本地预览是<code>hugo server</code>，生成是<code>hugo</code>，输出在public目录。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">content/</span><br><span class="line">  posts/</span><br><span class="line">    2026-09-08-hello-world.md</span><br><span class="line">themes/</span><br><span class="line">  PaperMod/</span><br><span class="line">config.yml</span><br></pre></td></tr></table></figure><p>主题是PaperMod，自带排版、深色模式、目录（TOC）、代码高亮和搜索。星空背景、鼠标点击特效、访问统计不在主题里，需要自己写CSS和JS挂在主题外面。这几个效果要维护的代码量接近主题本身。所以换掉了。</p><h2 id="第二版：Astro"><a class="headerlink" href="#第二版：Astro"></a>第二版：Astro</h2><p>Astro属于Node生态，用npm装依赖，配置文件是astro.config.mjs，构建输出到dist目录。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">src/</span><br><span class="line">  content/</span><br><span class="line">    blog/</span><br><span class="line">      hello.md</span><br><span class="line">  layouts/</span><br><span class="line">    BlogPost.astro</span><br><span class="line">astro.config.mjs</span><br><span class="line">package.json</span><br></pre></td></tr></table></figure><p>内容集合是它的一个机制：文章的front-matter字段在TypeScript文件里定义，不符合schema的字段会在构建时报错。输出默认不含前端框架的JavaScript，需要交互的组件单独声明。本地预览是<code>npm run dev</code>。</p><p>日常使用上，改配置后要重新执行npm install和npm run build，node_modules目录装下来几百兆。所以换到了Hexo。</p><h2 id="第三版：Hexo"><a class="headerlink" href="#第三版：Hexo"></a>第三版：Hexo</h2><p>Hexo属于Node生态，当前版本8.1.2，主题是开源的安知鱼（anzhiyu），版本1.7.0。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">source/</span><br><span class="line">  _posts/</span><br><span class="line">    2026-09-08-hello-world.md</span><br><span class="line">themes/</span><br><span class="line">  anzhiyu/</span><br><span class="line">_config.yml              # 站点配置</span><br><span class="line">_config.anzhiyu.yml      # 主题配置（站点级覆盖）</span><br></pre></td></tr></table></figure><p>配置两层：站点_config.yml管生成与目录参数，_config.anzhiyu.yml管外观和功能开关。新建文章用<code>hexo new</code>，本地预览用<code>hexo server</code>，生成用<code>hexo generate</code>，输出在public目录。主题自带星空背景、鼠标点击特效、首页打字机、本地搜索、字数统计、访客统计、深色模式、繁简转换、版权声明、音乐页、分类标签归档页、图片灯箱、相对时间，都在主题配置里开关，不用自己写代码。其中本地搜索由生成search.xml、前端检索实现，不依赖外部服务。</p><p>默认字体是霞鹜文楷，文章支持封面图、置顶、分类和标签。背景粒子和首页头图自写脚本放置。定制放在_config.anzhiyu.yml里，主题更新不受影响。</p><p>三个引擎的产物都是纯静态文件，HTML加CSS加JS，不需要服务端。</p>]]>
    </content>
    <id>https://u2tt.github.io/2026/09/08/blog-rebuilt-three-times/</id>
    <link href="https://u2tt.github.io/2026/09/08/blog-rebuilt-three-times/"/>
    <published>2026-09-08T18:00:00.000Z</published>
    <summary>Hugo、Astro到Hexo的三次迁移：引擎、目录结构、配置文件与主题功能对照。</summary>
    <title>博客三次重构：Hugo、Astro到Hexo</title>
    <updated>2026-09-15T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="HPC" scheme="https://u2tt.github.io/tags/HPC/"/>
    <category term="SLURM" scheme="https://u2tt.github.io/tags/SLURM/"/>
    <category term="Linux" scheme="https://u2tt.github.io/tags/Linux/"/>
    <content>
      <![CDATA[<p>学校高性能计算平台共两百余台机器，纳入SLURM调度的171台，合计12352个CPU核，调度器统计内存90余TB，外挂23PB并行文件系统。硬件分批采购，代际混合。</p><p>120台为早期CPU节点，双路32核Xeon 8358P，单机64核 / 500G内存，承载日常主要算力。数台胖节点为四路24核Xeon 8360H，单机96核 / 2T内存，供大内存作业使用。最新批次为GPU节点，单机96核 / 1T内存、8张H20，另有一台配2块4090。另有2台国产K100异构节点，自五月末起持续offline。</p><p>平台另有二十余台GPU机器未纳入SLURM节点列表；登录、管理、监控类机器同样不计入调度器口径，<code>sinfo</code>不显示。</p><p>主要用户方向为生物信息与计算化学，<code>module avail</code>提供87个模块，blast、bwa、vasp、gromacs占多数。</p><p>登录节点仅承担登录与作业提交，计算须下发至计算节点。调度系统版本SLURM 23.11.10，以下数据均在登录节点实测采集。</p><h2 id="节点详情"><a class="headerlink" href="#节点详情"></a>节点详情</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sinfo -s                             <span class="comment"># 分区概览：节点数、状态分布</span></span><br><span class="line">sinfo -N -o <span class="string">&quot;%N %P %c %m %G %t&quot;</span>      <span class="comment"># 逐节点：核数、内存、加速卡、状态</span></span><br><span class="line">squeue -o <span class="string">&quot;%.8i %.8P %.18j %.8t %.10M %R&quot;</span>    <span class="comment"># 自己的作业</span></span><br><span class="line">sacct -u <span class="variable">$USER</span> --starttime=now-2hours        <span class="comment"># 自己的历史作业与资源用量</span></span><br></pre></td></tr></table></figure><p><code>sinfo -s</code>输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">PARTITION AVAIL  TIMELIMIT   NODES(A/I/O/T) NODELIST</span><br><span class="line">cpu96        up   infinite          1/4/0/5   fat[01-05]</span><br><span class="line">cpu64*       up   infinite        25/95/0/120 node[01-120]</span><br><span class="line">test96       up   infinite          0/2/0/2   node[121-122]</span><br><span class="line">soy-com      up   infinite          0/22/0/22   sri-com-[01-22]</span><br><span class="line">sri-com      up   infinite      13/1/0/14     sri-com-[23-36]</span><br><span class="line">sri-mem      up   infinite          0/2/0/2   sri-mem-[01-02]</span><br><span class="line">sri-ghz      up   infinite          0/2/0/2   sri-ghz-[01-02]</span><br><span class="line">sri-gpu      up   infinite          0/1/0/1   sri-gpu-01</span><br><span class="line">sri-gnm      up   infinite          0/1/0/1   sri-gnm-01</span><br><span class="line">dcu          up   infinite          0/0/2/2   dk-gpu[01-02]</span><br></pre></td></tr></table></figure><p>带星号的<code>cpu64</code>为默认分区，不指定<code>-p</code>时作业落入此分区。中间四列为allocated / idle / other / total。</p><h2 id="分区"><a class="headerlink" href="#分区"></a>分区</h2><table><thead><tr><th>分区</th><th>节点数</th><th>CPU型号</th><th>单机核数</th><th>单机内存</th><th>加速卡</th></tr></thead><tbody><tr><td><code>cpu64</code>（默认）</td><td>120</td><td>Xeon Platinum 8358P</td><td>64</td><td>500G</td><td>—</td></tr><tr><td><code>cpu96</code></td><td>5</td><td>Xeon Platinum 8360H</td><td>96</td><td>2T</td><td>—</td></tr><tr><td><code>test96</code></td><td>2</td><td>Xeon Platinum 8558P</td><td>96</td><td>250G</td><td>—</td></tr><tr><td><code>soy-com</code></td><td>22</td><td>Xeon Platinum 8558P</td><td>96</td><td>500G</td><td>—</td></tr><tr><td><code>sri-com</code></td><td>14</td><td>—</td><td>96</td><td>500G</td><td>—</td></tr><tr><td><code>sri-mem</code></td><td>2</td><td>—</td><td>96</td><td>2T</td><td>—</td></tr><tr><td><code>sri-ghz</code></td><td>2</td><td>—</td><td>32</td><td>500G</td><td>—</td></tr><tr><td><code>sri-gpu</code></td><td>1</td><td>—</td><td>96</td><td>1T</td><td>H20 × 8</td></tr><tr><td><code>sri-gnm</code></td><td>1</td><td>—</td><td>96</td><td>500G</td><td>4090 × 2</td></tr><tr><td><code>dcu</code></td><td>2</td><td>—</td><td>48</td><td>250G</td><td>国产K100 × 2</td></tr></tbody></table><p>十个分区合计171台、12352核。<code>cpu64</code>中118台为500G内存，其余两台384G。</p><p><code>sri-*</code>一组绑定专属QoS，普通账号提交返回<code>Invalid qos specification</code>，CPU型号无法读取；<code>dcu</code>两台offline，同样无法读取。</p><p><code>cpu64</code>的120台为主力：空闲95台时可直接提交；25台在跑时backfill仍可插入空闲窗口。调度器记录内存500G，节点内<code>free -g</code>实测503G。</p><p>DCU两台装国产K100，<code>sinfo</code>中为<code>down*</code>，<code>scontrol show node</code>给出的原因为<code>Not responding</code>，自五月底持续至今。另两台带加速卡的节点——<code>sri-gpu</code>的H20与<code>sri-gnm</code>的4090——位于上述受限QoS组内。</p><p>系统代际存在差异：早期CPU节点为CentOS 8.2 / 内核4.18，后加的GPU与内存节点为RHEL 9.5 / 内核5.14。<code>/hpc/software</code>下gcc覆盖4.8.5至14.1.0，glibc保留2.29与2.36两套，对应两代系统各留一版。自行编译的二进制跨分区搬运存在兼容风险。</p><h2 id="作业提交"><a class="headerlink" href="#作业提交"></a>作业提交</h2><p><strong>交互式调试</strong>用<code>srun</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">srun -p cpu64 -N1 -n1 --<span class="keyword">time</span>=00:30:00 --pty bash</span><br></pre></td></tr></table></figure><p>实测分配到node08，<code>grep -m1 'model name' /proc/cpuinfo</code>为Xeon Platinum 8358P。进入后<code>nproc</code>返回<strong>1</strong>而非64——srun该步仅分配1个CPU，确认整机规格需读<code>/proc/cpuinfo</code>或使用<code>lscpu</code>。</p><p><strong>正式任务</strong>写脚本交<code>sbatch</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment">#SBATCH -J quant-backtest</span></span><br><span class="line"><span class="comment">#SBATCH -p cpu96</span></span><br><span class="line"><span class="comment">#SBATCH --cpus-per-task=32</span></span><br><span class="line"><span class="comment">#SBATCH --mem=256G</span></span><br><span class="line"><span class="comment">#SBATCH --time=08:00:00</span></span><br><span class="line"><span class="comment">#SBATCH -o /hpc/home/%u/logs/%j.out</span></span><br><span class="line"><span class="comment">#SBATCH -e /hpc/home/%u/logs/%j.err</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> ~/quant</span><br><span class="line">python backtest.py</span><br></pre></td></tr></table></figure><p>申请加速卡加一行<code>#SBATCH --gres=gpu:1</code>，具体型号见分区<code>Gres</code>字段。</p><p>集群<code>SelectTypeParameters</code>为<code>CR_CORE</code>，调度按核数分配，内存不参与资源分配——作业不写<code>--mem</code>亦可运行，节点<code>AllocMem</code>恒为0。以两个1核作业验证，运行期间所在节点<code>AllocMem</code>均为0。同节点多作业之间互不可见实际内存占用，先占满内存的作业会影响其余作业，故提交时须显式指定<code>--mem</code>。<code>sacct</code>中ReqMem显示500000M，为未填该字段时按节点总内存回填的显示值，不代表实际申请整机。</p><p>数组作业上限<code>MaxArraySize</code>为1001，单次最多提交1001个子任务，更大规模需分批提交。</p><h2 id="调度器按什么排"><a class="headerlink" href="#调度器按什么排"></a>调度器按什么排</h2><p><code>PriorityType = priority/multifactor</code>，<code>SchedulerType = sched/backfill</code>。前者按作业年龄、份额、队列权重计算优先级，后者负责插空——作业若能在一个空闲窗口内跑完，即可提前启动。</p><p>该组合利于短作业，反向亦然：为一个5分钟的任务写<code>--time=72:00:00</code>，backfill找不到足够长的窗口，作业只能排队。</p><p><strong>所有分区时间上限均为<code>infinite</code></strong>，MaxTime无系统级限制，时限交由使用者自行声明，长时作业可长期占用节点。</p><p><code>squeue -a</code>在普通账号下不显示其他用户作业：实测<code>squeue -a -h | wc -l</code>返回0，同一时刻<code>sinfo</code>显示25个节点处于alloc状态。判断集群负载只能依据<code>sinfo</code>的节点状态分布。</p><h2 id="存储：23P与5000亿inode"><a class="headerlink" href="#存储：23P与5000亿inode"></a>存储：23P与5000亿inode</h2><p><code>df -hT</code>中容量最大的一行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Filesystem      Type      Size  Used Avail Use% Mounted on</span><br><span class="line">/hpc             dpc        23P  1.1P   22P   5% /hpc</span><br></pre></td></tr></table></figure><p>23PB并行文件系统，已用1.1PB。文件系统类型为<code>dpc</code>，属分布式存储客户端挂载，与本地盘行为不同——小文件随机读性能与顺序读写相差量级，登录节点禁用<code>du</code> / <code>find</code>即源于此。</p><p>inode容量见<code>df -i /hpc</code>：总量5000亿，已用2.33亿。元数据按目录树铺开、查询需走网络，是该量级的成因。</p><p>另两个挂载点：</p><ul><li><code>/usr/local/globle/softs</code>为NFS，191G、59%已用，存放slurm二进制与公共软件，所有节点共享同一份。</li><li><code>/dev/shm</code>为252G内存盘。数据管道中间结果置于此处，读写不落盘；重启即清空。</li></ul><p>家目录位于<code>/hpc/home/&lt;用户名&gt;</code>。以本账号为例，占用接近48TB，主要为从ModelScope拉取的49个模型，其余为量化日线数据。</p><h2 id="环境与启动速度"><a class="headerlink" href="#环境与启动速度"></a>环境与启动速度</h2><p>公共软件通过module加载，<code>module avail</code>挂载87个：生信类（<code>blast</code>、<code>bwa</code>、<code>hmmer</code>、<code>canu</code>、<code>megahit</code>、<code>gtdbtk</code>、<code>metawrap</code>）、计算化学类（<code>vasp</code>、<code>gromacs</code>、<code>amber</code>、<code>autodock</code>）、<code>gcc/4.8.5</code>至<code>gcc/14.1.0</code>、<code>openmpi</code> 3.1至5.0.5、<code>cuda/11.7</code>与<code>12.4</code>、Python 3.7/3.8、R三个版本、<code>singularity</code>两个版本。使用前<code>module load gcc/14.1.0 openmpi/5.0.5</code>即可，优于在home内自行编译。</p><p>Python环境采用miniconda，按用途拆为四个：<code>myenv</code>放通用工具，<code>quant</code>放baostock相关依赖，<code>agent</code>与<code>agent-gpu</code>分开的原因是GPU侧依赖锁定在另一cuda版本。</p><p>zsh启动时间做过专项优化。原始耗时约15秒，成因是oh-my-zsh及插件每次登录重新source，而home挂在分布式存储上，网络文件系统随机读延迟高。处理办法是把omz与历史文件缓存到<code>/dev/shm</code>，并将&quot;重新加载配置&quot;改写为跳过omz重复加载的函数。优化后约0.8秒，多终端窗口并发打开时差异明显。</p><h2 id="长任务用tmux托管"><a class="headerlink" href="#长任务用tmux托管"></a>长任务用tmux托管</h2><p>登录节点直接运行长任务，网络断开即进程终止，改用tmux托管：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tmux new -s pull        <span class="comment"># 建会话</span></span><br><span class="line"><span class="comment"># Ctrl-b d 脱离</span></span><br><span class="line">tmux attach -t pull     <span class="comment"># 重新接上</span></span><br></pre></td></tr></table></figure><p>集群tmux版本为2.7。小时级以上的任务交<code>sbatch</code>；tmux用于托管数据拉取循环与监控进程。</p>]]>
    </content>
    <id>https://u2tt.github.io/2026/09/08/hpc-slurm-notes/</id>
    <link href="https://u2tt.github.io/2026/09/08/hpc-slurm-notes/"/>
    <published>2026-09-08T17:30:00.000Z</published>
    <summary>学校高性能计算平台的SLURM实测：机器构成与代际、分区参数对照（CPU型号/核数/内存/加速卡）、作业提交与内存记账的坑、23P存储的挂载类型与inode规模、环境与启动速度优化。</summary>
    <title>学校HPC集群</title>
    <updated>2026-10-04T21:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>陈鑫宇</name>
    </author>
    <category term="blog" scheme="https://u2tt.github.io/tags/blog/"/>
    <category term="随笔" scheme="https://u2tt.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
    <content>
      <![CDATA[<h1 id="恭喜！"><a class="headerlink" href="#恭喜！"></a>恭喜！</h1><h1 id="2026年9月8日-本博客正式上线。"><a class="headerlink" href="#2026年9月8日-本博客正式上线。"></a>2026年9月8日,本博客正式上线。</h1><p>笔者会在博客记录日常，记录做了什么事情，记录写的代码，会记录做的GitHub仓库。</p><br><p>总之就是什么都会记录喵～</p>]]>
    </content>
    <id>https://u2tt.github.io/2026/09/08/hello-world/</id>
    <link href="https://u2tt.github.io/2026/09/08/hello-world/"/>
    <published>2026-09-08T04:00:00.000Z</published>
    <summary>写A股量化、Python工具链和实践记录。Hexo搭的，GitHub Pages托管。</summary>
    <title>博客上线</title>
    <updated>2026-09-15T12:00:00.000Z</updated>
  </entry>
</feed>
