<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="https://www.w3.org/2005/Atom">

 <title>张志敏的技术专栏</title>
 <link href="https://beginor.github.io/atom.xml" rel="self"/>
 <link href="https://beginor.github.io"/>
 <updated>2025-11-24T15:06:18+00:00</updated>
 <id>https://beginor.github.io</id>
 <author>
   <name>张志敏</name>
   <email>beginor(at)qq.com</email>
 </author>

 
 <entry>
   <title>在 Redmi K40S 上安装 LineageOS 记录</title>
   <link href="https://beginor.github.io/2025/11/24/install-lineageos-on-redmi-k40s.html"/>
   <updated>2025-11-24T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2025/11/24/install-lineageos-on-redmi-k40s</id>
   <content type="html"><![CDATA[<p><img src="/assets/post-images/20251120225153.png" alt="红米 K40S" /></p>

<p>手上的红米 K40S 已经服役了 3 年多， 从 MIUI 12 升级到 MIUI 13， 再到现在的 HyperOS 1.0 (Android 14)， 官方的系统支持肯定是结束了， 在更换手机或者转为备用机之前，还想继续折腾一下， 当然也有其它几个方面的原因，一并说明如下：</p>

<ol>
  <li>官方系统停止更新，止步于 HyperOS 1.0 (Android 14)；</li>
  <li>官方系统臃肿， 而且广告很多， 而且有些广告还去不掉；</li>
  <li>安装第三方 apk ， 时不时跳出验证， 体验非常糟糕；</li>
  <li>LineageOS 官方支持 <a href="https://wiki.lineageos.org/devices/munch/">K40S</a>， 目前是 LineageOS 22.2 (Android 15) ， 很大可能也会有 LineageOS 23 (Android 16) ；</li>
  <li>时隔多年想再体验下类原生安卓系统；</li>
</ol>

<p>鉴于以上几点原因， 准备将 K40S 解锁并安装 LineageOS 。</p>

<h2 id="解锁-bootloader">解锁 Bootloader</h2>

<p>现在小米刷类原生安卓系统最大的障碍就是解锁 Bootloader ，随着 HyperOS 的发布， 小米旗下机型解锁的难度越来越高， 令不少人都望而却步。 不出意外， 这部 K40S 将是我的最后一部小米手机， 后续机型不能解锁 Bootloader 的话， 将不会再购买小米的任何手机。</p>

<blockquote>
  <p>引用 GitHub 上大佬 MlgmXyysd 的话，那就是： 自从小米限制解锁 BootLoader 后，小米就一直在违背”极客”精神，甚至违背了 GPL。 <a href="https://github.com/MlgmXyysd/Xiaomi-HyperOS-BootLoader-Bypass/blob/master/docs/README-zh.md">原文链接</a></p>
</blockquote>

<p>幸好， 在 github 上有一些开源的项目可以绕过小米 HyperOS 对 BootLoader 解锁账户绑定限制社区等级的 PoC ， 它们是：</p>

<ul>
  <li><a href="https://github.com/MlgmXyysd/Xiaomi-HyperOS-BootLoader-Bypass">Xiaomi-HyperOS-BootLoader-Bypass</a></li>
  <li><a href="https://github.com/TheAirBlow/HyperSploit">HyperSploit</a></li>
</ul>

<p>只要出厂是 MIUI 的手机， 一般都可以借助这两个软件成功解锁。 比较了这两个软件， 我选择的是 HyperSploit ， 因为使用起来更加简单， 不用折腾 PHP 。</p>

<p>解锁的过程很顺利， 简单罗列如下：</p>

<ol>
  <li>需要一台 Windows 电脑， 安装小米官方的手机驱动程序；</li>
  <li>打开 ADB 调试模式并授权电脑调试；</li>
  <li>开发者模式界面根据提示绑定账号；</li>
  <li>运行 HyperSploit ，根据提示进行操作即可；</li>
</ol>

<blockquote>
  <p>虽然 HyperSploit 有 macOS 版本， 我尝试了一下，没有达到预期目标， 只好用另一台 Windows 电脑进行操作。</p>
</blockquote>

<p>具体可以查看<a href="https://github.com/MlgmXyysd/Xiaomi-HyperOS-BootLoader-Bypass/blob/master/docs/README-zh.md">这里</a>的 <code class="language-plaintext highlighter-rouge">前置要求</code> 和 <code class="language-plaintext highlighter-rouge">使用教程</code> ， 这两个软件的用法基本上是相同的。</p>

<p>幸运的是， 我的小米帐号没有被封控， 绑定到 K40S 之后， 只要等7天就可以顺利解锁。</p>

<p><img src="/assets/post-images/20251120232338.png" alt="unlock" /></p>

<p><img src="/assets/post-images/20251120232405.png" alt="unlock success" /></p>

<p>解锁成功之后， 就可以开始安装 LineageOS 了。</p>

<h2 id="安装-lineageos">安装 LineageOS</h2>

<h3 id="升级固件版本">升级固件版本</h3>

<p>在正式安装 LineageOS 之前，需要确认手机的固件版本， 如果不是最新版的话， 需要先升级到最新版， 否则安装 LineageOS 可能会失败。 目前 LineageOS 支持的 K40S 固件版本为 <a href="https://cdnorg.d.miui.com/OS1.0.2.0.ULMCNXM/miui_MUNCH_OS1.0.2.0.ULMCNXM_6438305cc0_14.0.zip">OS1.0.2.0.ULMCNXM</a> ，如果是这个版本的话， 可以根据 LineageOS 的<a href="https://wiki.lineageos.org/devices/munch/fw_update/variant2/">固件升级指南</a>升级下固件。</p>

<blockquote>
  <p>所谓的固件 (Firmware) 也就是手机硬件的驱动程序， LineageOS 依赖官方镜像的固件驱动程序才能正常运行。</p>
</blockquote>

<p>我记得以前的固件是直接包含在 LineageOS 固件中的， 现在不知什么原因，需要单独下载了，不过这些对于要刷机的人来说， 根本都不是事儿。</p>

<p>刷固件相当于装驱动， 对手机内的数据没有任何影响，因此可以放心刷， 步骤如下：</p>

<h4 id="下载官方镜像">下载官方镜像</h4>

<ul>
  <li>从 MIUI 官方网站下载 K40S 的完整安装镜像 <a href="https://cdnorg.d.miui.com/OS1.0.2.0.ULMCNXM/miui_MUNCH_OS1.0.2.0.ULMCNXM_6438305cc0_14.0.zip">OS1.0.2.0.ULMCNXM</a></li>
  <li>下载完成之后得到的文件为 <code class="language-plaintext highlighter-rouge">miui_MUNCH_OS1.0.2.0.ULMCNXM_6438305cc0_14.0.zip</code></li>
</ul>

<h4 id="提取固件">提取固件</h4>

<p>使用 <a href="https://github.com/ssut/payload-dumper-go/releases/latest">payload-dumper-go</a> 提取所需的固件</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>payload-dumper-go <span class="nt">-o</span> <span class="nb">.</span> miui_MUNCH_OS1.0.2.0.ULMCNXM_6438305cc0_14.0.zip
</code></pre></div></div>

<p>可以提取出许多 img 文件， 下一步用到。</p>

<h4 id="fastboot-模式刷固件">Fastboot 模式刷固件</h4>

<p>将手机关机， 然后按住 <code class="language-plaintext highlighter-rouge">音量减</code> + <code class="language-plaintext highlighter-rouge">电源</code> 按钮开机， 进入 Fastboot 模式， 然后使用下面的命令刷入固件：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastboot flash abl_ab abl.img
fastboot flash aop_ab aop.img
fastboot flash bluetooth_ab bluetooth.img
fastboot flash cmnlib_ab cmnlib.img
fastboot flash cmnlib64_ab cmnlib64.img
fastboot flash devcfg_ab devcfg.img
fastboot flash dsp_ab dsp.img
fastboot flash featenabler_ab featenabler.img
fastboot flash hyp_ab hyp.img
fastboot flash imagefv_ab imagefv.img
fastboot flash keymaster_ab keymaster.img
fastboot flash modem_ab modem.img
fastboot flash qupfw_ab qupfw.img
fastboot flash tz_ab tz.img
fastboot flash uefisecapp_ab uefisecapp.img
fastboot flash xbl_ab xbl.img
fastboot flash xbl_config_ab xbl_config.img
</code></pre></div></div>

<p>如果一切正常， 重启手机即可。</p>

<h4 id="下载-lineageos">下载 LineageOS</h4>

<p>经过前面的解锁和固件更新，终于可以开始安装 LineageOS 了。 从 LineageOS 的网站下载最新的 K40S 的<a href="https://download.lineageos.org/devices/munch/builds">安装镜像</a> ， 目前的版本是 <code class="language-plaintext highlighter-rouge">lineage-22.2-20251117-nightly-munch-signed.zip</code> ， 文件需要的文件列表为：</p>

<ul>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/lineage-22.2-20251117-nightly-munch-signed.zip">lineage-22.2-20251117-nightly-munch-signed.zip</a></li>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/boot.img">boot.img</a></li>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/dtbo.img">dtbo.img</a></li>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/super_empty.img">super_empty.img</a></li>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/vbmeta.img">vbmeta.img</a></li>
  <li><a href="https://mirrorbits.lineageos.org/full/munch/20251117/vendor_boot.img">vendor_boot.img</a></li>
</ul>

<h4 id="刷写启动-boot-分区-和-恢复-recovery-分区">刷写启动 (boot) 分区 和 恢复 (recovery) 分区</h4>

<p>再次将手机重启到 Fastboot 模式， 用下面的命令来刷写启动分区：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastboot flash boot boot.img
</code></pre></div></div>

<p>刷写启动分区之后， 可以保证恢复 (Recovery) 分区正常工作， 接下来刷写恢复分区：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastboot flash vendor_boot vendor_boot.img
</code></pre></div></div>

<h4 id="在恢复模式下清空数据">在恢复模式下清空数据</h4>

<p>刷写恢复分区完成之后， 启动到恢复模式：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastboot reboot recovery
</code></pre></div></div>

<p>在恢复模式下， 选择 <code class="language-plaintext highlighter-rouge">Factory Reset</code> ，然后 <code class="language-plaintext highlighter-rouge">Format data / factory reset</code> 清空全部数据 。</p>

<blockquote>
  <p>重要数据一定要记得提前备份！！！</p>
</blockquote>

<h4 id="刷入-lineageos-系统镜像">刷入 LineageOS 系统镜像</h4>

<p>在恢复模式下， 选择 <code class="language-plaintext highlighter-rouge">Apply Update</code> -&gt; <code class="language-plaintext highlighter-rouge">Apply from ADB</code> ， 然后在电脑上执行下面的命令刷入 LineageOS 系统镜像：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-d</span> sideload lineage-22.2-20251117-nightly-munch-signed.zip
</code></pre></div></div>

<blockquote>
  <p>通常情况下，adb 会报告 Total xfer: 1.00x ，但在某些情况下，即使进程成功，输出可能会停在 47%并显示 adb: failed to read command: Success 。在其他情况下，它可能会显示 adb: failed to read command: No error 或 adb: failed to read command: Undefined error: 0 ，这也是可以的。</p>
</blockquote>

<p>刷完 LineageOS 系统镜像之后， 先不要重启手机， 接下来再刷入谷歌服务， 如果不需要谷歌服务的话， 则不需要这一步。</p>

<h2 id="安装谷歌服务">安装谷歌服务</h2>

<p>从 <a href="https://github.com/MindTheGapps/15.0.0-arm64/releases/latest">MindTheGapps</a> 下载谷歌服务安装包， 目前的版本是 <code class="language-plaintext highlighter-rouge">MindTheGapps-15.0.0-arm64-20250812_214357.zip</code> ， 用 <code class="language-plaintext highlighter-rouge">adb sideload</code> 命令刷入：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-d</span> sideload MindTheGapps-15.0.0-arm64-20250812_214357.zip
</code></pre></div></div>

<blockquote>
  <p>手机会提示签名不正确， 这是正常的， 因为 MindTheGapps 没有使用 LineageOS 的签名。</p>
</blockquote>

<p>现在可以重启手机， 启动全新的 LineageOS 系统了。</p>

<table>
  <tr>
    <td>
      <img alt="" src="/assets/post-images/20251124225810.png" />
    </td>
    <td>
      <img alt="" src="/assets/post-images/20251124225936.png" />
    </td>
  </tr>
</table>

<h2 id="与原版-hyperos-的对比">与原版 HyperOS 的对比</h2>

<h3 id="感觉比较舒服的几点">感觉比较舒服的几点</h3>

<ul>
  <li>LineageOS 非常简洁， 完全无广告， 这一点足可以把 HyperOS 以及国内一众魔改安卓系统钉死在耻辱柱上；</li>
  <li>LineageOS 体积很小， 大概只有 HyperOS 的 1/3 到一半， 运行起来也非常的轻快， 感觉很流畅；
    <blockquote>
      <p>不排除是 LineageOS 动画时间短的原因；</p>
    </blockquote>
  </li>
  <li>可以 <strong>随意</strong> 安装 apk 文件， 不用担心弹出密码认证甚至短信/刷脸认证；</li>
  <li>肯定不用担心计划性报废， 因为 LineageOS 是开源的， 越升级越流畅， 至少 LineageOS 系统本身是这样的；</li>
  <li>没有 HyperOS 那些令人厌恶的后台服务， 包括但不限于 <code class="language-plaintext highlighter-rouge">快应用</code> 、<code class="language-plaintext highlighter-rouge">手机管家</code> 等无法关闭的后台服务；</li>
  <li>显示刷新率有 3 档， 分别是 60Hz、90Hz、120Hz ， 比 HyperOS 多了 90Hz 一档， 而且还可以选择 <code class="language-plaintext highlighter-rouge">流畅画面</code> (自动将某些内容的刷新频率提高到 120Hz ， 主要是动画和过渡)；</li>
  <li>谷歌输入法 Gboard 终于适配底部的导航栏了， 比在原生系统上舒服很多， 原生 HyperOS 简直就是故意恶心谷歌输入法；</li>
  <li>从 Play 市场下载的软件 (包括微信、QQ、 淘宝、 京东、 高德地图等) 在存储卡上乱拉屎的情况都已经没有了， 在应用属性页的电量管理中禁止后台之后也基本不会作妖了， 反观从 HyperOS 市场下载的软件， 简直都是垃圾；</li>
  <li>可以在 <code class="language-plaintext highlighter-rouge">设置</code> -&gt; <code class="language-plaintext highlighter-rouge">电池</code> 页面为安装的每个应用设置温控策略， 而不必安装什么游戏助手之类的额外软件；</li>
</ul>

<h3 id="感觉比较遗憾的几点">感觉比较遗憾的几点</h3>

<ul>
  <li>工作日闹钟： LineageOS 是类原生系统， 没有国内系统定制的工作日闹钟是最遗憾的一个功能， 而且好像还找不到替代品；</li>
  <li>NFC门禁卡： HyperOS 有小米钱包， 可以复制小区的门禁卡， 这个在 LineageOS 上暂时没有找到替代品， 这手机的 NFC 我就不知道还有啥用了；</li>
  <li>天气预报： HyperOS 的天气预报功能还是很贴心的， 不过装了谷歌的服务之后， 也有基于位置的天气预报，虽然没有那么及时， 但是凑合也能用；</li>
</ul>

<h3 id="意外惊喜">意外惊喜</h3>

<p>最后还有一个意外的惊喜， 那就是支持游戏手柄震动； 原来的 HyperOS 是基于 Android 14 的， 不支持手柄震动。 安装 LineageOS 之后，系统也升级到了 Android 15 ， 在使用手柄时突然发现可以震动了， 游戏手感可以说是上升了一个档次， 算是最意外的惊喜吧。</p>
]]></content>
 </entry>
 
 <entry>
   <title>Python 中的并发：多线程、多进程和Asyncio</title>
   <link href="https://beginor.github.io/2025/05/18/concurrency-in-python-threading-multiprocessing-asyncio.html"/>
   <updated>2025-05-18T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2025/05/18/concurrency-in-python-threading-multiprocessing-asyncio</id>
   <content type="html"><![CDATA[<h2 id="tldr">TL;DR</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">io_bound</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">io_very_slow</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Use Asyncio"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Use Threads"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Multi Processing"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="介绍">介绍</h2>

<p>并发是编程中的一个基本概念，它允许应用程序同时执行多个任务。Python 提供了多种用于管理并发的工具：多线程、多进程和异步编程（Python 中的 asyncio 模块）。每个都有独特的优势，适合不同类型的任务。本文深入探讨了这些并发模型，提供了清晰的示例和详细说明，以帮助您了解何时以及如何有效地使用它们。</p>

<h2 id="进程与线程">进程与线程</h2>

<h3 id="进程">进程</h3>

<p>进程是正在执行的程序的独立实例。每个进程都在自己的内存空间中运行，其自己的资源由作系统分配。进程不与其他进程共享内存，除非明确设计为通过进程间通信 （IPC） 共享内存。</p>

<h3 id="线程">线程</h3>

<p>线程是进程中的最小执行单位。同一进程中的多个线程共享相同的内存空间，使它们能够比单独的进程更高效地进行通信。但是，此共享内存可能会导致同步问题。</p>

<h3 id="示例在-python-中创建线程">示例：在 Python 中创建线程</h3>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">threading</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">print_numbers</span><span class="p">():</span>
    <span class="c1"># This function will run in a separate thread
</span>    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Thread: </span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># Simulate some work with sleep
</span>

<span class="c1"># Create a new thread object to run print_numbers()
</span><span class="n">thread</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">print_numbers</span><span class="p">)</span>
<span class="c1"># Start the thread
</span><span class="n">thread</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
<span class="c1"># Wait for the thread to finish before exiting the main program
</span><span class="n">thread</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Main thread: Execution finished"</span><span class="p">)</span>
</code></pre></div></div>

<p>解释：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">threading.Thread(target=print_numbers)</code>: 创建将运行 <code class="language-plaintext highlighter-rouge">print_numbers()</code> 函数的线程；</li>
  <li><code class="language-plaintext highlighter-rouge">thread.start()</code>：开始执行线程。</li>
  <li><code class="language-plaintext highlighter-rouge">thread.join（）</code>：确保主线程等待新线程完成，然后再继续。</li>
</ul>

<h2 id="多线程与多进程">多线程与多进程</h2>

<h3 id="多线程">多线程</h3>

<p>多线程允许多个线程在同一进程中并发运行。在 Python 中，多线程中的真正并行性受到全局解释器锁 （GIL） 的限制，该锁一次只允许一个线程执行 Python 字节码。但是，多线程处理对于 I/O 密集型任务仍然很有用，其中线程可以等待外部资源（如文件 I/O 或网络作），而其他线程可以继续执行。</p>

<h3 id="示例python-中的多线程">示例：Python 中的多线程</h3>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">threading</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">worker</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Worker </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> starting"</span><span class="p">)</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># Simulating I/O-bound work
</span>    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Worker </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> finished"</span><span class="p">)</span>


<span class="n">threads</span> <span class="o">=</span> <span class="p">[]</span>


<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">t</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">i</span><span class="p">,))</span>
    <span class="n">threads</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
    <span class="n">t</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

<span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">threads</span><span class="p">:</span>
    <span class="n">t</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>  <span class="c1"># Wait for all threads to complete
</span></code></pre></div></div>

<p>解释：</p>

<ul>
  <li>每个线程通过休眠 2 秒来模拟一些 I/O 绑定工作。</li>
  <li><code class="language-plaintext highlighter-rouge">thread.join()</code> 确保主线程等待所有工作线程完成。</li>
</ul>

<h3 id="多进程">多进程</h3>

<p>多进程涉及运行多个进程，每个进程都有自己的 Python 解释器和内存空间。这允许真正的并行性，使多进程成为 CPU 密集型任务的理想选择。</p>

<h3 id="示例python-中的多进程">示例：Python 中的多进程</h3>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">multiprocessing</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">worker</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Worker </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> starting"</span><span class="p">)</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># Simulate some work
</span>    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Worker </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> finished"</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">processes</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
        <span class="n">p</span> <span class="o">=</span> <span class="n">multiprocessing</span><span class="p">.</span><span class="n">Process</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">i</span><span class="p">,))</span>
        <span class="n">processes</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
        <span class="n">p</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">processes</span><span class="p">:</span>
        <span class="n">p</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>  <span class="c1"># Wait for all processes to finish
</span></code></pre></div></div>

<p>解释：</p>

<ul>
  <li>每个工作进程独立运行，从而实现跨 CPU 内核的真正并行性。</li>
  <li>多进程避免了 GIL，使其适合 CPU 密集型任务。</li>
</ul>

<h2 id="asyncio">Asyncio</h2>

<p>Asyncio 是一个 Python 库，用于使用 <code class="language-plaintext highlighter-rouge">async/await</code> 语法编写并发代码。它专为 I/O 密集型任务而设计，并使用事件循环来管理和计划任务。</p>

<h3 id="asyncio-中的关键概念">Asyncio 中的关键概念</h3>

<ol>
  <li><code class="language-plaintext highlighter-rouge">协程 (Coroutines)</code>: 使用 <code class="language-plaintext highlighter-rouge">async def</code> 定义的函数。这些是 asyncio 的构建块，表示可以暂停和恢复的任务;</li>
  <li><code class="language-plaintext highlighter-rouge">事件循环 (Event Loop)</code> ：asyncio 的核心，用于管理任务的执行;</li>
  <li><code class="language-plaintext highlighter-rouge">Tasks</code>：围绕在事件循环上调度的协程的包装器。</li>
  <li><code class="language-plaintext highlighter-rouge">await</code>：暂停协程的执行，将控制权交还给事件循环。</li>
</ol>

<h3 id="示例asyncio-基础">示例：Asyncio 基础</h3>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">task</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Task </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> starting"</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># Simulate an I/O-bound operation
</span>    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Task </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> finished"</span><span class="p">)</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="n">task</span><span class="p">(</span><span class="s">"A"</span><span class="p">),</span> <span class="n">task</span><span class="p">(</span><span class="s">"B"</span><span class="p">),</span> <span class="n">task</span><span class="p">(</span><span class="s">"C"</span><span class="p">))</span>


<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div></div>

<p>解释：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">await asyncio.sleep(2)</code>: 暂停协程，允许事件循环运行其他任务;</li>
  <li><code class="language-plaintext highlighter-rouge">asyncio.gather()</code>: 并发运行多个协程。</li>
</ul>

<h3 id="在-asyncio-中处理-cpu-密集型的任务">在 Asyncio 中处理 CPU 密集型的任务</h3>

<p>Asyncio 不太适合 CPU 密集型任务，因为它们会阻塞事件循环。但是，您可以使用 <code class="language-plaintext highlighter-rouge">asyncio.to_thread()</code> 或 <code class="language-plaintext highlighter-rouge">asyncio.run_in_executor()</code> 将 CPU 密集型任务卸载到单独的线程或进程。</p>

<h4 id="示例卸载-cpu-密集型任务">示例：卸载 CPU 密集型任务</h4>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">cpu_bound_task</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>  <span class="c1"># Simulating a CPU-bound task
</span>    <span class="k">return</span> <span class="n">n</span> <span class="o">*</span> <span class="n">n</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">to_thread</span><span class="p">(</span><span class="n">cpu_bound_task</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Result: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div></div>

<p>解释：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">asyncio.to_thread()</code>: 将 CPU 绑定的任务卸载到单独的线程，从而允许事件循环保持响应。</li>
</ul>

<h3 id="常见的误解和错误">常见的误解和错误</h3>

<h4 id="混合同步和异步代码">混合同步和异步代码</h4>

<p>不是需要所有内容都是异步的。可以使用 <code class="language-plaintext highlighter-rouge">asyncio.to_thread()</code> 或类似方法在异步代码中调用同步函数。</p>

<p>示例：</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">sync_task</span><span class="p">():</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="k">return</span> <span class="s">"Completed"</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">to_thread</span><span class="p">(</span><span class="n">sync_task</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>


<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div></div>

<h4 id="直接等待-cpu-密集型任务">直接等待 CPU 密集型任务</h4>

<p>直接等待 CPU 绑定的任务可能会阻止事件循环，始终将此类任务卸载到单独的线程或进程。</p>

<p><code class="language-plaintext highlighter-rouge">create_task()</code> 与 <code class="language-plaintext highlighter-rouge">await</code></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">await coroutine</code>: 运行协程并等待其完成；</li>
  <li><code class="language-plaintext highlighter-rouge">asyncio.create_task(coroutine)</code>： 安排协程与其他任务并发运行并立即返回。然后，您可以稍后等待该任务。</li>
</ul>

<p>示例：</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">my_coroutine</span><span class="p">():</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="k">return</span> <span class="s">"Done"</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">create_task</span><span class="p">(</span><span class="n">my_coroutine</span><span class="p">())</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Doing something else while waiting..."</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">task</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Task result: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div></div>

<p>解释：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">asyncio.create_task()</code>：当您想要启动协程并同时执行其他工作时，此功能非常有用。</li>
</ul>

<h2 id="何时使用哪种方法">何时使用哪种方法</h2>

<ol>
  <li>多线程：
    <ul>
      <li>最适合 I/O 密集型任务，如网络作或文件 I/O；</li>
      <li>当您需要在线程之间共享状态时使用；</li>
      <li>由于 Python 中的 GIL，因此不适合 CPU 密集型任务。</li>
    </ul>
  </li>
  <li>多进程：
    <ul>
      <li>非常适合需要真正并行性的 CPU 密集型任务；</li>
      <li>当您需要绕过 GIL 时使用；</li>
      <li>最适合繁重的计算工作负载。</li>
    </ul>
  </li>
  <li>asyncio:
    <ul>
      <li>非常适合具有许多并发作的 I/O 密集型任务。</li>
      <li>非常适合构建高性能网络服务器或具有大量 I/O 密集型任务的应用程序。</li>
      <li>不适合没有卸载的 CPU 密集型任务。</li>
    </ul>
  </li>
</ol>

<h2 id="示例fastapi-中的异步编程">示例：FastAPI 中的异步编程</h2>

<p>FastAPI 是一个现代 Web 框架，它利用 asyncio 来有效地处理并发请求。它使用 async/await 语法来管理 I/O 绑定作，而不会阻塞服务器。</p>

<h3 id="为什么-fastapi-使用-async">为什么 FastAPI 使用 Async</h3>

<ol>
  <li>可扩展性 ：异步代码允许 FastAPI 以最小的开销处理许多并发连接;</li>
  <li>性能 ：对于 I/O 密集型任务，异步可以胜过传统线程;</li>
  <li>简单性：与线程代码相比，异步代码通常更易于编写和推理。</li>
</ol>

<h3 id="在-fastapi-中卸载-cpu-密集型任务">在 FastAPI 中卸载 CPU 密集型任务</h3>

<p>FastAPI 可以通过将 CPU 密集型任务卸载到线程或进程池来处理这些任务。</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
<span class="kn">from</span> <span class="nn">concurrent.futures</span> <span class="kn">import</span> <span class="n">ProcessPoolExecutor</span>
<span class="kn">import</span> <span class="nn">asyncio</span>


<span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
<span class="n">process_pool</span> <span class="o">=</span> <span class="n">ProcessPoolExecutor</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">cpu_bound_task</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
    <span class="c1"># Simulate a CPU-bound task
</span>    <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
        <span class="n">total</span> <span class="o">+=</span> <span class="n">i</span> <span class="o">*</span> <span class="n">i</span>
    <span class="k">return</span> <span class="n">total</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/compute/{n}"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">compute</span><span class="p">(</span><span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="c1"># Offload the CPU-bound task to a separate process
</span>    <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">get_running_loop</span><span class="p">()</span>
    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="p">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="n">process_pool</span><span class="p">,</span> <span class="n">cpu_bound_task</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"result"</span><span class="p">:</span> <span class="n">result</span><span class="p">}</span>
</code></pre></div></div>

<p>解释：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ProcessPoolExecutor</code>: 创建一个 ProcessPoolExecutor 来将 CPU 绑定的任务卸载到单独的进程，这确保了主 FastAPI 事件循环保持响应，它的实现由 Uvicorn 内部处理;</li>
  <li><code class="language-plaintext highlighter-rouge">loop.run_in_executor()</code>: 此方法将 <code class="language-plaintext highlighter-rouge">cpu_bound_task</code> 卸载给 <code class="language-plaintext highlighter-rouge">executor</code>（在本例中为 <code class="language-plaintext highlighter-rouge">ProcessPoolExecutor）</code>，允许 FastAPI 服务器在并行处理 CPU 密集型任务的同时处理其他请求;</li>
  <li><code class="language-plaintext highlighter-rouge">await</code>: 通过使用 <code class="language-plaintext highlighter-rouge">await</code> 确保 FastAPI 处理程序在返回结果之前等待 CPU 绑定的任务完成。</li>
</ul>

<h2 id="为什么卸载很重要">为什么卸载很重要</h2>

<p>在 Web 应用程序中，响应能力是关键。如果你直接在 FastAPI 事件循环中运行 CPU 密集型任务，它会阻止服务器处理其他请求，直到任务完成。通过卸载到单独的进程或线程，服务器可以继续并发处理传入请求，从而获得更好的可扩展性和用户体验。</p>

<h2 id="结论">结论</h2>

<p>Python 中的并发是一个强大的工具，允许您编写高效且可扩展的应用程序。无论您是处理 I/O 密集型任务、CPU 密集型计算，还是两者兼而有之，Python 都提供了各种并发模型（多线程、多进程和 asyncio）来满足您的需求:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">多线程</code>： 最适合共享内存有用的 I/O 密集型任务，但由于 GIL 的原因，它不适合 CPU 密集型任务;</li>
  <li><code class="language-plaintext highlighter-rouge">多进程</code>：非常适合需要真正并行性的 CPU 密集型任务，避免了 GIL 的限制;</li>
  <li><code class="language-plaintext highlighter-rouge">Asyncio</code>：非常适合涉及大量并发作的 I/O 绑定任务，提供非阻塞并发。</li>
</ul>

<p>参考资料：</p>

<ul>
  <li><a href="https://medium.com/@ark.iitkgp/concurrency-in-python-understanding-threading-multiprocessing-and-asyncio-03bd92ca298b">Concurrency in Python: Understanding Threading, Multiprocessing, and Asyncio</a></li>
  <li><a href="https://stackoverflow.com/a/52498068">stackoverflow</a></li>
</ul>
]]></content>
 </entry>
 
 <entry>
   <title>使用 ANEMLL 在苹果芯片 (M1 Max) 的 NPU 上运行大模型</title>
   <link href="https://beginor.github.io/2025/02/24/run-llm-on-apple-npu-with-anemll.html"/>
   <updated>2025-02-24T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2025/02/24/run-llm-on-apple-npu-with-anemll</id>
   <content type="html"><![CDATA[<p>MacBook Pro 的笔记本都搭载了专门为 AI 设计核神经网络处理器(NPU) ，不过在运行 AI 大模型时， 一般都是通过显卡来运行， 几乎没有 NPU 什么事， 所以苹果的 NPU 芯片也得到了一个大模型电阻器的称号。</p>

<p>不过最近新发布的 <a href="https://github.com/anemll/anemll">ANEMLL</a> 项目， 号称可以在苹果 NPU 上运行大模型， 觉得非常好奇， 决定体验并记录下来。</p>

<h2 id="anemll-项目介绍">ANEMLL 项目介绍</h2>

<p><a href="https://github.com/anemll/anemll">ANEMLL</a> 的目标是在苹果 NPU 上运行现有的 HuggingFace 上的大模型， 目前最新版本是 <code class="language-plaintext highlighter-rouge">0.1.2-alpha</code> ， 暂时只支持 llama 架构的模型， 比如 LLAMA 架构的模型，Meta LLaMA 3.1 以及 DeepSeek 蒸馏过的 Llama 3.1 模型， 未来会增加更多架构的模型。</p>

<blockquote>
  <p>对 <a href="https://github.com/anemll/anemll">ANEMLL</a> 项目感兴趣的话， 可以在 github 上为作者加个 🌟 支持一下。</p>
</blockquote>

<p>接下来就按照 ANEMLL 项目的说明， 尝试将 <a href="https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct">Llama-3.2-1B-Instruct</a> 模型转换成 CoreML 格式， 在 NPU 上运行，然后再和 GPU 运行做个对比， 看看有什么优势。</p>

<h2 id="环境准备">环境准备</h2>

<p>将模型转换成 CoreML 格式， 必须安装的软件：</p>

<ul>
  <li><strong>XCode</strong> 命令行的苹果开发者工具是不够的， 可以从 AppStore 下载；</li>
  <li><strong>Python 3.9</strong> 刚好是 XCode 内置的 Python 版本， 估计是 XCode 兼容性比较好， 所以才会推荐使用这个版本 （实际测试， 从 HomeBrew 下载的 3.12 版本的 Python 也可以运行）。</li>
</ul>

<p>至于 Git 和 Git-LFS 则不是必须的， 因为下载模型也不一定用 git 嘛。</p>

<h2 id="转换模型格式">转换模型格式</h2>

<p>转换模型包括3部分： 嵌入 (Embeding) ， 前馈网络/层 (Feed Forward Network/layers) 和 LM 头 (LM Head) ， 要了解详情， 可以查看官方文档 <a href="https://github.com/Anemll/Anemll/blob/main/docs/ANE_converter.md">ANE_converter</a> 。</p>

<p>ANEMLL 提供了转换脚本 <code class="language-plaintext highlighter-rouge">convert_model.sh</code> 将 LLM 模型转换为 CoreML 模型， 用法如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./anemll/utils/convert_model.sh <span class="se">\</span>
    <span class="nt">--model</span> ../Meta-Llama-3.2-1B <span class="se">\</span>
    <span class="nt">--output</span> ./converted_models
</code></pre></div></div>

<p>然后就是等了， 大概需要5分钟左右的时间。</p>

<blockquote>
  <p>如果不想折腾， 也可以从 <a href="https://huggingface.co/anemll">https://huggingface.co/anemll</a> 下载作者转换好的模型来运行。</p>
</blockquote>

<h2 id="使用-npu-运行转换后的模型">使用 NPU 运行转换后的模型</h2>

<p>使用 <code class="language-plaintext highlighter-rouge">chat_full.py</code> 来和模型对话， 命令如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 chat_full.py <span class="nt">--meta</span> llama-3.2-1b-instruct/meta.yaml
</code></pre></div></div>

<p>聊天截图如下</p>

<p><img src="/assets/post-images/anemll-chat.png" alt="ANEMLL Chat" /></p>

<p>运行 llama-3.2-1b-instruct 模型， 输出速度是 35 t/s ， 不算快， 大概是 M1 Max 用 mlx 引擎运行 llama-3.2-1b-instruct 模型的 1/3 。</p>

<p>NPU 使用率如下图所示：</p>

<p><img src="/assets/post-images/anemll-chat-npu-usage.png" alt="ANEMLL Npu Usage" /></p>

<p>首先， NPU 功耗确实很低， 只有 2.8W ，如果是用 mlx 来运行大模型， 显卡功耗差不多有 30W 了， 功耗只需要显卡的 1/10 。</p>

<p>有一点比较奇怪， NPU 使用率只有 30% 左右， 于是在 X 上问作者是什么原因， 作者回复说主要是因为 NPU 带宽不够， 在等数据加载， 也就是说带宽限制了 NPU 的发挥。</p>

<p><img src="/assets/post-images/anemll-chat-npu-usage-reason.png" alt="Why NPU usage is 30%" /></p>

<blockquote>
  <p>估计这也是为什么苹果发布会只说带了多少核心的 NPU ， 其它参数却只字不提的原因吧。</p>
</blockquote>

<h2 id="总结">总结</h2>

<p>总的来说， 以 NPU 运行大模型还是不错的， 相比显卡运行来说， 1/10 的功耗， 1/3 速度， 对于移动设备，笔记本来说还是非常友好的。</p>

<p>如果这个项目以后能够让 NPU 全速运行速度， 速度提高3倍和 GPU 差不多了， 就算功耗也增加3倍， 相比 GPU 还是有很大的优势。</p>

<p>真心希望未来能够在 NPU 上运行各种模型， 就像现在在显卡上运行一样。</p>

<video controls="" style="width: 100%;">
  <source src="/assets/post-images/anemll-chat.mp4" type="video/mp4" />
</video>
]]></content>
 </entry>
 
 <entry>
   <title>使用 Vite 处理项目中的 glsl 文件</title>
   <link href="https://beginor.github.io/2025/01/10/handle-glsl-file-with-vite.html"/>
   <updated>2025-01-10T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2025/01/10/handle-glsl-file-with-vite</id>
   <content type="html"><![CDATA[<p>项目中有一些 WebGL 相关的着色器 (shader) 代码， 后缀名为 <code class="language-plaintext highlighter-rouge">.glsl</code> 。 目录文件结构为：</p>

<pre><code class="language-txt">- src
- src/layers/particle-layer.ts
- src/layers/particle-layer.fragment.glsl
- src/layers/particle-layer.vertex.glsl
</code></pre>

<p>原来用 <code class="language-plaintext highlighter-rouge">esbuild</code> 进行转译和打包， 配置起来非常的容易， 直接使用内置的 <code class="language-plaintext highlighter-rouge">loader</code> 就可以处理， <code class="language-plaintext highlighter-rouge">esbuild</code> 的配置如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
  <span class="na">entryPoints</span><span class="p">:</span> <span class="dl">'</span><span class="s1">./src/main.ts</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">outdir</span><span class="p">:</span> <span class="dl">'</span><span class="s1">./dist</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">splitting</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">chunkNames</span><span class="p">:</span> <span class="dl">'</span><span class="s1">chunks/[name]-[hash]</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">tsconfig</span><span class="p">:</span> <span class="dl">'</span><span class="s1">./tsconfig.json</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">loader</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">.glsl</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>上面的配置就是告诉 esbuild 将 <code class="language-plaintext highlighter-rouge">glsl</code> 作为文本来加载， 在 ts 或 js 文件中可以直接 import 导入使用，代码如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">vertex</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./particle-layer.vertex.glsl</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">fragment</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./particle-layer.fragment.glsl</span><span class="dl">'</span>
</code></pre></div></div>

<p>现在切换到 Vite 做转译和打包工具， 发现没有类似 esbuild 这样指定文件类型的处理方式。 虽然 Vite 有调用 esbuild ， 其配置文件也支持 <a href="https://vite.dev/config/shared-options.html#esbuild">esbuild</a> 选项， 但是只是 esbuild 的 <a href="https://esbuild.github.io/api/#transform">transform</a> 选项， 不是完整的 build 选项， 因此不能像 esbuild 那样简单指定 loader 来解决。</p>

<p>查看了 Vite 配置文件的 <code class="language-plaintext highlighter-rouge">resolve</code> 选项， 发现更多是关于路径方面的配置， 而不是文件内容相关的配置， 也无法解决这个问题。</p>

<p>也尝试了 Vite 配置文件的 <a href="https://vite.dev/config/dep-optimization-options.html#optimizedeps-esbuildoptions">optimizeDeps.esbuildOptions</a> ， 虽然是完整的 esbuild 选项， 能够指定 loader ， 运行时会出错。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
  <span class="na">base</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
  <span class="na">publicDir</span><span class="p">:</span> <span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">server</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">host</span><span class="p">:</span> <span class="dl">'</span><span class="s1">127.0.0.1</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">port</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">optimizeDeps</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">esbuildOptions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">loader</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">'</span><span class="s1">.glsl</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text</span><span class="dl">'</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<p>运行时错误信息如下：</p>

<pre><code class="language-txt">vite v6.0.7 building for production...
✓ 38 modules transformed.
x Build failed in 530ms
error during build:
src/layers/particle-layer/particle-layer-vertex.glsl (1:8): Expected ';', '}' or &lt;eof&gt; (Note that you need plugins to import files that are not JavaScript)
file: ~/Developer/javascript//wind-demo/src/layers/particle-layer/particle-layer-vertex.glsl:1:8

1: #define SHADER_NAME particle-layer-vertex-shader
           ^
2: #ifdef GL_ES
3: precision highp float;
</code></pre>

<p>这看起来应该是直接把 <code class="language-plaintext highlighter-rouge">glsl</code> 文件的内容当作 js 了。</p>

<p>那看起来应该是不能通过配置来实现 esbuild 原来的功能， 只能写一个插件来解决吧， 好在插件比较简单， 很容易实现， 代码如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">glslPlugin</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">glsl-plugin</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">transform</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">id</span><span class="p">.</span><span class="nx">endsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">.glsl</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
          <span class="na">code</span><span class="p">:</span> <span class="dl">'</span><span class="s1">export default `</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">code</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">`;</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">map</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="p">};</span>
      <span class="p">}</span>
    <span class="p">},</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>当然， 插件也可以写的更加复杂一些， 比如去除 glsl 文件中的空格/注视，甚至混淆等， 不过这些都可以后期再处理， 先能用再说。</p>

<p>最终的 Vite 配置文件如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">base</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
  <span class="na">publicDir</span><span class="p">:</span> <span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">server</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">host</span><span class="p">:</span> <span class="dl">'</span><span class="s1">127.0.0.1</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">port</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">glslPlugin</span><span class="p">()</span>
  <span class="p">],</span>
  <span class="na">build</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">target</span><span class="p">:</span> <span class="dl">'</span><span class="s1">esnext</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
]]></content>
 </entry>
 
 <entry>
   <title>GDAL 3.10 中的线程安全的只读栅格数据集</title>
   <link href="https://beginor.github.io/2024/11/26/raster-dataset-read-only-thread-safety-with-gdal-310.html"/>
   <updated>2024-11-26T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/11/26/raster-dataset-read-only-thread-safety-with-gdal-310</id>
   <content type="html"><![CDATA[<p>GDAL 最近发布了 3.10 版本， 其中最重要的一个更新就是 <a href="https://gdal.org/en/latest/development/rfc/rfc101_raster_dataset_threadsafety.html">栅格数据集只读线程安全</a> 。 许多栅格算法，都需要独立和并发的方式读取栅格数据， 在以前的 GDAL 版本中， 由于 Dataset 不是线程安全的， 这些操作需要在单个线程中处理 I/O ， 或者通过互斥锁来防止并发实用， 或者每个工作线程打开一个单独的 GDALDataset ， 实现起来都会比较复杂。 因此 GDAL 在 3.10 版本中实现了 <code class="language-plaintext highlighter-rouge">栅格数据集只读线程安全</code> ， 提供可以从多个线程安全使用的特殊 GDALDataset 实例， 不需要用户再考虑线程安全的问题， 多线程读取栅格数据的操作将大大简化。</p>

<p>在 GDAL 的文档中， 已经详细介绍了 C/C++ 语言增加的 <a href="https://gdal.org/en/latest/development/rfc/rfc101_raster_dataset_threadsafety.html#c-and-c-api-extensions">函数和使用方法</a> ，本文就不再介绍。 接下来主要介绍一下如何 C# 和 Python 两种语言中如何使用这一功能。</p>

<h2 id="在-c-中线程安全的读取栅格数据">在 C# 中线程安全的读取栅格数据</h2>

<p>GDAL 在 C# 语言绑定中， 为栅格数据集 <code class="language-plaintext highlighter-rouge">Dataset</code> 增加了 <code class="language-plaintext highlighter-rouge">IsThreadSafe</code> 和 <code class="language-plaintext highlighter-rouge">GetThreadSafeDataset</code> 两个成员函数， 定义如下：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Dataset</span> <span class="p">:</span> <span class="n">MajorObject</span> <span class="p">{</span>
  
  <span class="k">public</span> <span class="n">Dataset</span> <span class="nf">GetThreadSafeDataset</span><span class="p">(</span><span class="kt">int</span> <span class="n">nScopeFlags</span><span class="p">)</span> <span class="p">{}</span>

  <span class="k">public</span> <span class="kt">bool</span> <span class="nf">IsThreadSafe</span><span class="p">(</span><span class="kt">int</span> <span class="n">nScopeFlags</span><span class="p">)</span> <span class="p">{}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>这样我们就可以判断一个栅格数据集是否是线程安全的，如果不是，则再打开一个线程安全的数据集，代码如下：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 从第三方类库获取一个已经打开的数据集</span>
<span class="kt">var</span> <span class="n">dataset</span> <span class="p">=</span> <span class="nf">GetDatasetFromOtherLibs</span><span class="p">();</span>
<span class="c1">// 判断是不是线程安全的</span>
<span class="kt">var</span> <span class="n">isThreadSafe</span> <span class="p">=</span> <span class="n">dataset</span><span class="p">.</span><span class="nf">IsThreadSafe</span><span class="p">(</span><span class="n">GDAL_OF_RASTER</span><span class="p">);</span>
<span class="c1">// 如果不是，再打开一个线程安全的栅格数据集</span>
<span class="k">if</span> <span class="p">(!</span><span class="n">isThreadSafe</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">dataset</span> <span class="p">=</span> <span class="n">dataset</span><span class="p">.</span><span class="nf">GetThreadSafeDataset</span><span class="p">(</span><span class="n">GDAL_OF_RASTER</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>当然， 如果能够自行打开栅格数据， 则推荐使用 <code class="language-plaintext highlighter-rouge">Gdal.OpenEx</code> 方法直接打开线程安全的数据集：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">GdalExtensions</span> <span class="p">{</span>
  <span class="c1">// GDAL 并没有为在 C# 绑定中定义这些常量， 自己定义一下需要的常量</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">GDAL_OF_READONLY</span> <span class="p">=</span> <span class="m">0x00</span><span class="p">;</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">GDAL_OF_RASTER</span> <span class="p">=</span> <span class="m">0x02</span><span class="p">;</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">GDAL_OF_THREAD_SAFE</span> <span class="p">=</span> <span class="m">0x800</span><span class="p">;</span>

  <span class="k">public</span> <span class="k">static</span> <span class="n">Dataset</span> <span class="nf">OpenThreadSafeDataset</span><span class="p">(</span><span class="kt">string</span> <span class="n">tiffFile</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// reffer [GDALOpenEx](https://gdal.org/en/latest/api/raster_c_api.html#gdal_8h_1a9cb8585d0b3c16726b08e25bcc94274a)</span>
    <span class="kt">var</span> <span class="n">threadSafeDataset</span> <span class="p">=</span> <span class="n">Gdal</span><span class="p">.</span><span class="nf">OpenEx</span><span class="p">(</span>
      <span class="n">tiffFile</span><span class="p">,</span>
      <span class="p">(</span><span class="kt">uint</span><span class="p">)(</span><span class="n">GDAL_OF_RASTER</span> <span class="p">|</span> <span class="n">GDAL_OF_READONLY</span> <span class="p">|</span> <span class="n">GDAL_OF_THREAD_SAFE</span><span class="p">),</span>
      <span class="k">null</span><span class="p">,</span>
      <span class="k">null</span><span class="p">,</span>
      <span class="k">null</span>
    <span class="p">);</span>
    <span class="k">return</span> <span class="n">threadSafeDataset</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <ol>
    <li><code class="language-plaintext highlighter-rouge">GDAL_OF_XXX</code> 常量的值在 <a href="https://github.com/OSGeo/gdal/blob/master/gcore/gdal.h">gdal.h</a> 文件中可以找到；</li>
    <li><code class="language-plaintext highlighter-rouge">OpenEx</code> 函数个参数的意义参考 C 语言函数 <a href="https://gdal.org/en/latest/api/raster_c_api.html#gdal_8h_1a9cb8585d0b3c16726b08e25bcc94274a">GDALOpenEx</a> ；</li>
  </ol>
</blockquote>

<p>可以通过上面定义的扩展函数直接打开一个线程安全的只读栅格数据集：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">dataset</span> <span class="p">=</span> <span class="n">GdalExtensions</span><span class="p">.</span><span class="nf">OpenThreadSafeDataset</span><span class="p">(</span><span class="s">"mydata.tif"</span><span class="p">);</span>
</code></pre></div></div>

<p>这样获取到线程安全的 dataset 之后，可以轻松实现一个栅格数据切片服务，示例代码如下：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MaxRev.Gdal.Core</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">OSGeo.GDAL</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">SkiaSharp</span><span class="p">;</span>

<span class="n">GdalBase</span><span class="p">.</span><span class="nf">ConfigureAll</span><span class="p">();</span>
<span class="n">Gdal</span><span class="p">.</span><span class="nf">UseExceptions</span><span class="p">();</span>

<span class="c1">// 墨卡托坐标系的 GeoTiff 文件</span>
<span class="kt">var</span> <span class="n">tiffFile</span> <span class="p">=</span> <span class="s">"mydata.tif"</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"api/tile/{z:int}/{y:int}/{x:int}"</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span> <span class="n">z</span><span class="p">,</span> <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="n">Dataset</span><span class="p">?</span> <span class="n">dataset</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="kt">var</span> <span class="n">tile</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Tile</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">);</span>
        <span class="n">dataset</span> <span class="p">=</span> <span class="n">GdalExtensions</span><span class="p">.</span><span class="nf">OpenThreadSafeDataset</span><span class="p">(</span><span class="n">tiffFile</span><span class="p">);</span>
        <span class="c1">// 参照 https://cogeotiff.github.io/rio-tiler/ 实现了一个 ReadTile 函数，</span>
        <span class="c1">// 从 GeoTiff 文件读取按墨卡托坐标系下的地图切片对应的图片</span>
        <span class="kt">var</span> <span class="n">image</span> <span class="p">=</span> <span class="n">dataset</span><span class="p">.</span><span class="nf">ReadTile</span><span class="p">(</span><span class="n">tile</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">image</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>  <span class="p">{</span>
            <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="kt">var</span> <span class="n">buffer</span> <span class="p">=</span> <span class="n">image</span><span class="p">.</span><span class="nf">Encode</span><span class="p">(</span>
            <span class="n">SKEncodedImageFormat</span><span class="p">.</span><span class="n">Png</span><span class="p">,</span> <span class="m">90</span>
        <span class="p">).</span><span class="nf">ToArray</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">File</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="s">"image/png"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">ex</span><span class="p">);</span>
        <span class="k">throw</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">finally</span> <span class="p">{</span>
        <span class="n">dataset</span><span class="p">?.</span><span class="nf">Dispose</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}).</span><span class="nf">WithName</span><span class="p">(</span><span class="s">"GetTile"</span><span class="p">);</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="在-python-中线程安全的读取栅格数据">在 Python 中线程安全的读取栅格数据</h2>

<p>Python 和 C# 都是 GDAL 官方支持的，因此用法也基本上是一样的。</p>

<p>判断一个栅格数据集是否是线程安全的，如果不是，则再获取一个新的线程安全的数据集：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">osgeo</span> <span class="kn">import</span> <span class="n">gdal</span>

<span class="n">gdal</span><span class="p">.</span><span class="n">UseExceptions</span><span class="p">()</span>

<span class="n">dataset</span><span class="p">:</span> <span class="n">gdal</span><span class="p">.</span><span class="n">Dataset</span> <span class="o">=</span> <span class="n">open_dataset_from_other_lib</span><span class="p">()</span>

<span class="n">is_thread_safe</span> <span class="o">=</span> <span class="n">dataset</span><span class="p">.</span><span class="n">IsThreadSafe</span><span class="p">(</span><span class="n">gdal</span><span class="p">.</span><span class="n">OF_RASTER</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'is thread safe: </span><span class="si">{</span><span class="n">is_thread_safe</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

<span class="k">if</span> <span class="ow">not</span> <span class="n">is_thread_safe</span><span class="p">:</span>
    <span class="n">safe_dataset</span><span class="p">:</span> <span class="n">gdal</span><span class="p">.</span><span class="n">Dataset</span> <span class="o">=</span> <span class="n">dataset</span><span class="p">.</span><span class="n">GetThreadSafeDataset</span><span class="p">(</span><span class="n">gdal</span><span class="p">.</span><span class="n">OF_RASTER</span><span class="p">)</span>
    <span class="n">is_thread_safe</span> <span class="o">=</span> <span class="n">safe_dataset</span><span class="p">.</span><span class="n">IsThreadSafe</span><span class="p">(</span><span class="n">gdal</span><span class="p">.</span><span class="n">OF_RASTER</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'is thread safe: </span><span class="si">{</span><span class="n">is_thread_safe</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="n">safe_dataset</span><span class="p">.</span><span class="n">Release</span><span class="p">()</span>

<span class="n">dataset</span><span class="p">.</span><span class="n">Release</span><span class="p">()</span>
</code></pre></div></div>

<p>也可以通过 <code class="language-plaintext highlighter-rouge">gdal.OpenEx</code> 直接打开线程安全的栅格数据集：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">osgeo</span> <span class="kn">import</span> <span class="n">gdal</span>

<span class="n">gdal</span><span class="p">.</span><span class="n">UseExceptions</span><span class="p">()</span>

<span class="n">dataset</span><span class="p">:</span> <span class="n">gdal</span><span class="p">.</span><span class="n">Dataset</span> <span class="o">=</span> <span class="n">gdal</span><span class="p">.</span><span class="n">OpenEx</span><span class="p">(</span>
    <span class="s">'mydata.tif'</span><span class="p">,</span>
    <span class="n">gdal</span><span class="p">.</span><span class="n">OF_READONLY</span> <span class="o">|</span> <span class="n">gdal</span><span class="p">.</span><span class="n">OF_RASTER</span> <span class="o">|</span> <span class="n">gdal</span><span class="p">.</span><span class="n">OF_THREAD_SAFE</span>
<span class="p">)</span>

<span class="n">is_thread_safe</span> <span class="o">=</span> <span class="n">dataset</span><span class="p">.</span><span class="n">IsThreadSafe</span><span class="p">(</span><span class="n">gdal</span><span class="p">.</span><span class="n">OF_RASTER</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'is thread safe: </span><span class="si">{</span><span class="n">is_thread_safe</span><span class="si">}</span><span class="s">'</span><span class="p">)</span> <span class="c1"># True
</span>
<span class="n">dataset</span><span class="p">.</span><span class="n">Release</span><span class="p">()</span>
</code></pre></div></div>

<blockquote>
  <p>GDAL 的 Python 绑定定义的常量比 C# 多一些， 但是类型提示则大多都是 <code class="language-plaintext highlighter-rouge">Any</code> 。</p>
</blockquote>
]]></content>
 </entry>
 
 <entry>
   <title>扩展 NHibernate 支持 PostgreSQL 的数组类型</title>
   <link href="https://beginor.github.io/2024/09/16/add-support-of-postgresql-array-type-for-nhibernate.html"/>
   <updated>2024-09-16T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/09/16/add-support-of-postgresql-array-type-for-nhibernate</id>
   <content type="html"><![CDATA[<p>PostgreSQL 数据库的一大特征就是数组类型， 使用起来非常的方便， 但是 NHibernate 却一直没有添加对数组类型的支持，因此有必要扩展 NHibernate 以添加对数组类型的支持。</p>

<h2 id="定义数据库方言-dialect">定义数据库方言 (Dialect)</h2>

<p>NHibernate 对不同提供了相应的数据库方言 (Dialect) ，要添加数组类型支持，自然要从数据库方言(Dialect)开始：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">NpgSqlDialect</span> <span class="p">:</span> <span class="n">NHibernate</span><span class="p">.</span><span class="n">Dialect</span><span class="p">.</span><span class="n">PostgreSQLDialect</span> <span class="p">{</span>
  
  <span class="k">public</span> <span class="nf">NpgSqlDialect</span><span class="p">()</span> <span class="p">{}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>在 NHibernate 配置文件中添加下面的配置使用这个方言：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;property</span> <span class="na">name=</span><span class="s">"dialect"</span><span class="nt">&gt;</span>NHibernate.Extensions.NpgSql.NpgSqlDialect,NHibernate.Extensions.NpgSql<span class="nt">&lt;/property&gt;</span>
</code></pre></div></div>

<h2 id="定义用户数据类型-usertype">定义用户数据类型 (UserType)</h2>

<p>在 NHibernate 中扩展自定义数据类型， 需要实现 <code class="language-plaintext highlighter-rouge">NHibernate.UserTypes.IUserType</code> 接口, 因此需要定义一个 <code class="language-plaintext highlighter-rouge">ArrayType</code> 并实现 <code class="language-plaintext highlighter-rouge">IUserType</code> 接口， 部分代码如下:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ArrayType</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="p">:</span> <span class="n">IUserType</span> <span class="p">{</span>

  <span class="k">public</span> <span class="n">SqlType</span><span class="p">[]</span> <span class="n">SqlTypes</span> <span class="p">=&gt;</span> <span class="p">[</span><span class="nf">GetNpgSqlType</span><span class="p">()];</span>

  <span class="k">public</span> <span class="n">System</span><span class="p">.</span><span class="n">Type</span> <span class="n">ReturnedType</span> <span class="p">=&gt;</span> <span class="k">typeof</span><span class="p">(</span><span class="n">T</span><span class="p">[]);</span>
  
  <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsMutable</span> <span class="p">=&gt;</span> <span class="k">false</span><span class="p">;</span>
  
  <span class="k">public</span> <span class="kt">object</span> <span class="nf">Assemble</span><span class="p">(</span><span class="kt">object</span> <span class="n">cached</span><span class="p">,</span> <span class="kt">object</span> <span class="n">owner</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="kt">object</span> <span class="nf">Disassemble</span><span class="p">(</span><span class="kt">object</span> <span class="k">value</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="kt">object</span><span class="p">?</span> <span class="nf">DeepCopy</span><span class="p">(</span><span class="kt">object</span> <span class="k">value</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="k">new</span> <span class="kt">bool</span> <span class="nf">Equals</span><span class="p">(</span><span class="kt">object</span><span class="p">?</span> <span class="n">x</span><span class="p">,</span> <span class="kt">object</span><span class="p">?</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="kt">int</span> <span class="nf">GetHashCode</span><span class="p">(</span><span class="kt">object</span><span class="p">?</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="kt">object</span><span class="p">?</span> <span class="nf">NullSafeGet</span><span class="p">(</span><span class="n">DbDataReader</span> <span class="n">rs</span><span class="p">,</span> <span class="kt">string</span><span class="p">[]</span> <span class="n">names</span><span class="p">,</span> <span class="n">ISessionImplementor</span> <span class="n">session</span><span class="p">,</span> <span class="kt">object</span> <span class="n">owner</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
  <span class="k">public</span> <span class="kt">object</span> <span class="nf">Replace</span><span class="p">(</span><span class="kt">object</span> <span class="n">original</span><span class="p">,</span> <span class="kt">object</span> <span class="n">target</span><span class="p">,</span> <span class="kt">object</span> <span class="n">owner</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
  
<span class="p">}</span>
</code></pre></div></div>

<p>为了减少冗余的代码， 将 <code class="language-plaintext highlighter-rouge">ArrayType</code> 定义成范型类型。 如果想了解全部实现代码，请查看 <a href="https://github.com/beginor/nhibernate-extensions/blob/master/src/NHibernate.Extensions.NpgSql/UserTypes/ArrayType.cs">ArrayType.cs</a> 的源代码。</p>

<p>接下来在上面定义的 <code class="language-plaintext highlighter-rouge">NpgSqlDialect</code> 中，注册常用的数组类型（以 <code class="language-plaintext highlighter-rouge">int[]</code> 和 <code class="language-plaintext highlighter-rouge">string[]</code> 为例）：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">NpgSqlDialect</span> <span class="p">:</span> <span class="n">NHibernate</span><span class="p">.</span><span class="n">Dialect</span><span class="p">.</span><span class="n">PostgreSQLDialect</span> <span class="p">{</span>
  
  <span class="k">public</span> <span class="nf">NpgSqlDialect</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">RegisterUserTypes</span><span class="p">();</span>
  <span class="p">}</span>
  
  <span class="k">private</span> <span class="k">void</span> <span class="nf">RegisterUserTypes</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">TypeFactory</span><span class="p">.</span><span class="nf">RegisterType</span><span class="p">(</span>
      <span class="k">typeof</span><span class="p">(</span><span class="kt">int</span><span class="p">[]),</span>
      <span class="n">NHibernateUtil</span><span class="p">.</span><span class="nf">Custom</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">ArrayType</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;)),</span>
      <span class="p">[</span><span class="s">"int[]"</span><span class="p">]</span>
    <span class="p">);</span>
    <span class="n">TypeFactory</span><span class="p">.</span><span class="nf">RegisterType</span><span class="p">(</span>
      <span class="k">typeof</span><span class="p">(</span><span class="kt">string</span><span class="p">[]),</span>
      <span class="n">NHibernateUtil</span><span class="p">.</span><span class="nf">Custom</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">ArrayType</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;)),</span>
      <span class="p">[</span><span class="s">"string[]"</span><span class="p">]</span>
    <span class="p">);</span>
  <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>现在，就可以在配置和sql查询中使用数组类型了，在实体映射中这样使用：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Class</span><span class="p">(</span><span class="n">Schema</span> <span class="p">=</span> <span class="s">"public"</span><span class="p">,</span> <span class="n">Table</span> <span class="p">=</span> <span class="s">"arr_test"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ArrTestEntity</span> <span class="p">{</span>
    <span class="p">[</span><span class="nf">Id</span><span class="p">(</span><span class="n">Name</span> <span class="p">=</span> <span class="k">nameof</span><span class="p">(</span><span class="n">Id</span><span class="p">),</span> <span class="n">Column</span> <span class="p">=</span> <span class="s">"id"</span><span class="p">,</span> <span class="n">Type</span> <span class="p">=</span> <span class="s">"long"</span><span class="p">,</span> <span class="n">Generator</span> <span class="p">=</span> <span class="s">"trigger-identity"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">virtual</span> <span class="kt">long</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">[</span><span class="nf">Property</span><span class="p">(</span><span class="n">Column</span> <span class="p">=</span> <span class="s">"int_arr"</span><span class="p">,</span> <span class="n">Type</span> <span class="p">=</span> <span class="s">"int[]"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">virtual</span> <span class="kt">int</span><span class="p">[]</span> <span class="n">IntArr</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">[</span><span class="nf">Property</span><span class="p">(</span><span class="n">Column</span> <span class="p">=</span> <span class="s">"str_arr"</span><span class="p">,</span> <span class="n">Type</span> <span class="p">=</span> <span class="s">"string[]"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">virtual</span> <span class="kt">string</span><span class="p">[]</span> <span class="n">StrArr</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>或者使用 xml 映射：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;class</span> <span class="na">table=</span><span class="s">"arr_test"</span> <span class="na">schema=</span><span class="s">"public"</span> <span class="na">name=</span><span class="s">"UnitTest.ArrTestEntity,UnitTest"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;id</span> <span class="na">name=</span><span class="s">"Id"</span> <span class="na">type=</span><span class="s">"long"</span> <span class="na">column=</span><span class="s">"id"</span> <span class="na">generator=</span><span class="s">"trigger-identity"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;property</span> <span class="na">name=</span><span class="s">"IntArr"</span> <span class="na">type=</span><span class="s">"int[]"</span> <span class="na">column=</span><span class="s">"int_arr"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;property</span> <span class="na">name=</span><span class="s">"StrArr"</span> <span class="na">type=</span><span class="s">"string[]"</span> <span class="na">column=</span><span class="s">"str_arr"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/class&gt;</span>
</code></pre></div></div>

<p>使用 SQL 进行查询过滤：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">string</span><span class="p">[]</span> <span class="n">strArr</span> <span class="p">=</span> <span class="p">[</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">];</span>
<span class="kt">var</span> <span class="n">sqlQuery</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="nf">CreateSQLQuery</span><span class="p">(</span>
  <span class="s">$"select * from public.arr_test where strArr = any(:</span><span class="p">{</span><span class="k">nameof</span><span class="p">(</span><span class="n">strArr</span><span class="p">)}</span><span class="s">)"</span>
<span class="p">);</span>
<span class="n">sqlQuery</span><span class="p">.</span><span class="nf">SetParameter</span><span class="p">(</span>
  <span class="k">nameof</span><span class="p">(</span><span class="n">strArr</span><span class="p">),</span>
  <span class="n">strArr</span><span class="p">,</span>
  <span class="n">NHibernateUtil</span><span class="p">.</span><span class="nf">Custom</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">ArrayType</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;))</span>
<span class="p">);</span>
<span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="n">sqlQuery</span><span class="p">.</span><span class="n">List</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>
</code></pre></div></div>

<p>当然，仅支持属性类型映射和 SQL 查询过滤是不够的， 因为最常用的是 Linq 查询， 接下来继续让 Linq 查询也支持数组类型过滤。</p>

<h2 id="定义-hql-数组过滤函数">定义 HQL 数组过滤函数</h2>

<p>数组条件过滤函数最常用的有两个：</p>

<ol>
  <li>数组是否包含某一个元素，SQL 查询表达式为： <code class="language-plaintext highlighter-rouge">element = ANY(array)</code> ;</li>
  <li>两个数组是否有共同的元素， SQL 查询表达式为： <code class="language-plaintext highlighter-rouge">array1 &amp;&amp; array2</code> ；</li>
</ol>

<p>NHibernate Linq 是基于 HQL 的， 因此需要先让 HQL 能够支持数组过滤， HQL 支持比较容易实现， 只需要在上面定义的 <code class="language-plaintext highlighter-rouge">NpgSqlDialect</code> 中添加对应的 SQL 函数模板：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">NpgSqlDialect</span> <span class="p">:</span> <span class="n">NHibernate</span><span class="p">.</span><span class="n">Dialect</span><span class="p">.</span><span class="n">PostgreSQLDialect</span> <span class="p">{</span>
  
  <span class="k">public</span> <span class="nf">NpgSqlDialect</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">RegisterFunctions</span><span class="p">()</span>
  <span class="p">}</span>
  
  <span class="k">private</span> <span class="k">void</span> <span class="nf">RegisterFunctions</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// array_contains(arr, 3) =&gt; :num = any(arr)</span>
    <span class="nf">RegisterFunction</span><span class="p">(</span>
      <span class="s">"array_contains"</span><span class="p">,</span>
      <span class="k">new</span> <span class="nf">SQLFunctionTemplate</span><span class="p">(</span><span class="n">NHibernateUtil</span><span class="p">.</span><span class="n">Boolean</span><span class="p">,</span> <span class="s">"?2 = any(?1)"</span><span class="p">)</span>
    <span class="p">);</span>
    <span class="c1">// array_intersects =&gt; ?1 &amp;&amp; ?2</span>
    <span class="nf">RegisterFunction</span><span class="p">(</span>
      <span class="s">"array_intersects"</span><span class="p">,</span>
      <span class="k">new</span> <span class="nf">SQLFunctionTemplate</span><span class="p">(</span><span class="n">NHibernateUtil</span><span class="p">.</span><span class="n">Boolean</span><span class="p">,</span> <span class="s">"?1 &amp;&amp; ?2"</span><span class="p">)</span>
    <span class="p">);</span>
  <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>注册了 SQL 函数模板之后，就可以在 HQL 查询中使用数组类型进行过滤：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">string</span><span class="p">[]</span> <span class="n">strArr</span> <span class="p">=</span> <span class="p">[</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">];</span>
<span class="kt">var</span> <span class="n">query1</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="nf">CreateQuery</span><span class="p">(</span>
  <span class="s">$"from ArrTestEntity e where array_intersects(e.StrArr, :</span><span class="p">{</span><span class="k">nameof</span><span class="p">(</span><span class="n">strArr</span><span class="p">)}</span><span class="s">)"</span>
<span class="p">);</span>
<span class="n">query1</span><span class="p">.</span><span class="nf">SetParameter</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">strArr</span><span class="p">),</span> <span class="n">strArr</span><span class="p">,</span> <span class="n">NHibernateUtil</span><span class="p">.</span><span class="nf">Custom</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">StringArrayType</span><span class="p">)));</span>
<span class="kt">var</span> <span class="n">data1</span> <span class="p">=</span> <span class="n">query1</span><span class="p">.</span><span class="n">List</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data1</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>
</code></pre></div></div>

<p>对应生成的的 SQL 语句为：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span>
  <span class="n">arrtestent0_</span><span class="p">.</span><span class="n">id</span> <span class="k">as</span> <span class="n">id1_2_</span><span class="p">,</span>
  <span class="n">arrtestent0_</span><span class="p">.</span><span class="n">int_arr</span> <span class="k">as</span> <span class="n">int2_2_</span><span class="p">,</span>
  <span class="n">arrtestent0_</span><span class="p">.</span><span class="n">str_arr</span> <span class="k">as</span> <span class="n">str3_2_</span> 
<span class="k">from</span>
  <span class="k">public</span><span class="p">.</span><span class="n">arr_test</span> <span class="n">arrtestent0_</span> 
<span class="k">where</span>
  <span class="n">arrtestent0_</span><span class="p">.</span><span class="n">str_arr</span> <span class="o">&amp;&amp;</span> <span class="p">:</span><span class="n">p0</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="定义-linq-扩展查询">定义 Linq 扩展查询</h2>

<p>终于来到了最关键的一步，有点儿复杂， 但是也不难。 先定义两个针对数据类型的扩展函数， 分别对应上面定义的 <code class="language-plaintext highlighter-rouge">array_contains</code> 和 <code class="language-plaintext highlighter-rouge">array_intersects</code> HQL 扩展函数，代码如下：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">ArrayExtensions</span> <span class="p">{</span>

  <span class="k">public</span> <span class="k">static</span> <span class="kt">bool</span> <span class="n">ArrayContains</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="k">this</span> <span class="n">T</span><span class="p">[]</span> <span class="n">array</span><span class="p">,</span> <span class="n">T</span> <span class="n">element</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">array</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="n">element</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">public</span> <span class="k">static</span> <span class="kt">bool</span> <span class="n">ArrayIntersects</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="k">this</span> <span class="n">T</span><span class="p">[]</span> <span class="n">array</span><span class="p">,</span> <span class="n">T</span><span class="p">[]</span> <span class="n">other</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">array</span><span class="p">.</span><span class="nf">Intersect</span><span class="p">(</span><span class="n">other</span><span class="p">).</span><span class="nf">Any</span><span class="p">();</span>
  <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>这里只需要函数定义即可，与如何实现没有关系，因为会被转换成对应的 HQL 查询， 不会真正执行这两个表达式。</p>
</blockquote>

<p>定义数据类型生成器 (Generator)，也就是将 Linq 表达式转换成对应的 HQL 表达式：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ArrayHqlGenerator</span> <span class="p">:</span> <span class="n">BaseHqlGeneratorForMethod</span> <span class="p">{</span>

  <span class="k">public</span> <span class="nf">ArrayHqlGenerator</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">SupportedMethods</span> <span class="p">=</span> <span class="p">[</span>
      <span class="n">ReflectHelper</span><span class="p">.</span><span class="n">GetMethodDefinition</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">[</span><span class="k">]&gt;</span><span class="p">(</span>
        <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">ArrayContains</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
      <span class="p">),</span>
      <span class="n">ReflectHelper</span><span class="p">.</span><span class="n">GetMethodDefinition</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">[</span><span class="k">]&gt;</span><span class="p">(</span>
        <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">ArrayIntersects</span><span class="p">(</span><span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;())</span>
      <span class="p">),</span>
    <span class="p">];</span>
  <span class="p">}</span>

  <span class="k">public</span> <span class="k">override</span> <span class="n">HqlTreeNode</span> <span class="nf">BuildHql</span><span class="p">(</span>
    <span class="n">MethodInfo</span> <span class="n">method</span><span class="p">,</span>
    <span class="n">Expression</span> <span class="n">targetObject</span><span class="p">,</span>
    <span class="n">ReadOnlyCollection</span><span class="p">&lt;</span><span class="n">Expression</span><span class="p">&gt;</span> <span class="n">arguments</span><span class="p">,</span>
    <span class="n">HqlTreeBuilder</span> <span class="n">treeBuilder</span><span class="p">,</span>
    <span class="n">IHqlExpressionVisitor</span> <span class="n">visitor</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="kt">var</span> <span class="n">hqlMethod</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">linqMethod</span> <span class="p">=</span> <span class="n">method</span><span class="p">.</span><span class="n">Name</span><span class="p">;</span>

    <span class="n">hqlMethod</span> <span class="p">=</span> <span class="n">linqMethod</span> <span class="k">switch</span> <span class="p">{</span>
      <span class="s">"ArrayContains"</span> <span class="p">=&gt;</span> <span class="s">"array_contains"</span><span class="p">,</span>
      <span class="s">"ArrayIntersects"</span> <span class="p">=&gt;</span> <span class="s">"array_intersects"</span><span class="p">,</span>
      <span class="n">_</span> <span class="p">=&gt;</span> <span class="n">hqlMethod</span>
    <span class="p">};</span>
    <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">hqlMethod</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">HibernateException</span><span class="p">(</span><span class="s">$"Method </span><span class="p">{</span><span class="n">method</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s"> not found"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">treeBuilder</span><span class="p">.</span><span class="nf">BooleanMethodCall</span><span class="p">(</span>
      <span class="n">hqlMethod</span><span class="p">,</span>
      <span class="n">arguments</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">visitor</span><span class="p">.</span><span class="n">Visit</span><span class="p">).</span><span class="n">Cast</span><span class="p">&lt;</span><span class="n">HqlExpression</span><span class="p">&gt;()</span>
    <span class="p">);</span>
  <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>最后，定一个 <code class="language-plaintext highlighter-rouge">LinqToHqlGeneratorsRegistry</code> 将上面定义的 <code class="language-plaintext highlighter-rouge">ArrayHqlGenerator</code> 合并进默认的 <code class="language-plaintext highlighter-rouge">DefaultLinqToHqlGeneratorsRegistry</code> ， 代码如下：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">LinqToHqlGeneratorsRegistry</span> <span class="p">:</span> <span class="n">DefaultLinqToHqlGeneratorsRegistry</span> <span class="p">{</span>

    <span class="k">public</span> <span class="nf">LinqToHqlGeneratorsRegistry</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nf">Merge</span><span class="p">(</span><span class="k">new</span> <span class="nf">ArrayHqlGenerator</span><span class="p">());</span>
    <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>修改 nhibernate.config 配置文件， 使用新定义的 <code class="language-plaintext highlighter-rouge">LinqToHqlGeneratorsRegistry</code> ：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;property</span> <span class="na">name=</span><span class="s">"linqtohql.generatorsregistry"</span><span class="nt">&gt;</span>NHibernate.Extensions.NpgSql.LinqToHqlGeneratorsRegistry,NHibernate.Extensions.NpgSql<span class="nt">&lt;/property&gt;</span>
</code></pre></div></div>

<p>就可以在 Linq 查询中使用数组过滤表达式了:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">string</span><span class="p">[]</span> <span class="n">strArr</span> <span class="p">=</span> <span class="p">[</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">];</span>
<span class="kt">var</span> <span class="n">query1</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="n">Query</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;().</span><span class="nf">Where</span><span class="p">(</span>
    <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">StrArr</span><span class="p">.</span><span class="nf">ArrayIntersects</span><span class="p">(</span><span class="n">strArr</span><span class="p">)</span>
<span class="p">);</span>
<span class="kt">var</span> <span class="n">data1</span> <span class="p">=</span> <span class="k">await</span> <span class="n">query1</span><span class="p">.</span><span class="nf">ToListAsync</span><span class="p">();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data1</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>

<span class="kt">int</span><span class="p">[]</span> <span class="n">intArr</span> <span class="p">=</span> <span class="p">[</span><span class="m">1</span><span class="p">,</span> <span class="m">3</span><span class="p">];</span>
<span class="kt">var</span> <span class="n">query2</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="n">Query</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;().</span><span class="nf">Where</span><span class="p">(</span>
    <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">IntArr</span><span class="p">.</span><span class="nf">ArrayIntersects</span><span class="p">(</span><span class="n">intArr</span><span class="p">)</span>
<span class="p">);</span>
<span class="kt">var</span> <span class="n">data2</span> <span class="p">=</span> <span class="k">await</span> <span class="n">query2</span><span class="p">.</span><span class="nf">ToListAsync</span><span class="p">();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data2</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">query3</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="n">Query</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;().</span><span class="nf">Where</span><span class="p">(</span>
    <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">StrArr</span><span class="p">.</span><span class="nf">ArrayIntersects</span><span class="p">(</span><span class="n">strArr</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="n">IntArr</span><span class="p">.</span><span class="nf">ArrayIntersects</span><span class="p">(</span><span class="n">intArr</span><span class="p">)</span>
<span class="p">);</span>
<span class="kt">var</span> <span class="n">data3</span> <span class="p">=</span> <span class="k">await</span> <span class="n">query3</span><span class="p">.</span><span class="nf">ToListAsync</span><span class="p">();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data3</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>
</code></pre></div></div>

<p>而且， 还可以反向使用 <code class="language-plaintext highlighter-rouge">ArrayContains</code> ：</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">idArr</span> <span class="p">=</span> <span class="n">idList</span><span class="p">.</span><span class="nf">ToArray</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">query2</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="n">Query</span><span class="p">&lt;</span><span class="n">ArrTestEntity</span><span class="p">&gt;().</span><span class="nf">Where</span><span class="p">(</span>
    <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">idArr</span><span class="p">.</span><span class="nf">ArrayContains</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="n">Id</span><span class="p">)</span>
<span class="p">);</span>
<span class="kt">var</span> <span class="n">data2</span> <span class="p">=</span> <span class="n">query2</span><span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">data2</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="n">Not</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>
</code></pre></div></div>

<p>最后要特别感谢 NHibernate 核心成员 <a href="https://github.com/fredericDelaporte">@fredericDelaporte</a> 和 <a href="https://github.com/hazzik">hazzik</a> ，在实现的过程中提供了不少帮助。</p>
]]></content>
 </entry>
 
 <entry>
   <title>搭建本地运行的人工智能代码助理</title>
   <link href="https://beginor.github.io/2024/07/12/ai-code-assistant-with-local-llm.html"/>
   <updated>2024-07-12T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/07/12/ai-code-assistant-with-local-llm</id>
   <content type="html"><![CDATA[<p>现在 AI 非常的流行， 各方面的应用都非常多。 比如在代码提示这个赛道上， 就有很多基于 AI 大模型的代码提示工具， 比如最著名的 <a href="https://github.com/features/copilot/">GitHub Copilot</a> ， 再比如阿里的 <a href="https://tongyi.aliyun.com/lingma">通义灵码</a> ， 还有今天要重点介绍的 <a href="https://github.com/continuedev/continue">Continue</a> 。</p>

<p>与其它类似的 AI 智能代码提示工具不同， Continue 是开源的， 支持调用本地部署的大模型服务， 可以在企业内部甚至是安全隔离的局域网中运行， 并且提供了完善本地运行的文档。如果是私有代码库， 无法使用基于互联网的 AI 智能提示， 那么使用 Continue 搭建本地的智能代码提示， 也能达到比较好的效果 （当然不能与收费的 GitHub Copilot 媲美）。</p>

<h2 id="continue-介绍">Continue 介绍</h2>

<blockquote>
  <p>以下功能介绍搬运自 Contine 的官方代码库。</p>
</blockquote>

<p><a href="https://github.com/continuedev/continue">Continue</a> 是领先的开源代码助手。您可以连接任何模型和任何上下文，以在 VS Code 和 JetBrains 中构建自定义自动完成和聊天体验， 主要功能有：</p>

<p><strong>更容易地理解代码片段</strong> 利用 AI 来解释代码段， 理解更容易。</p>

<p><img src="https://raw.github.com/continuedev/continue/main/docs/static/img/understand.gif" alt="Easily understand code sections" /></p>

<p><strong>自动完成代码建议</strong> 利用 AI 理解代码上下文， 提供智能提示， 按 Tab 键自动补全。</p>

<p><img src="https://raw.github.com/continuedev/continue/main/docs/static/img/autocomplete.gif" alt="Tab to autocomplete code suggestions" /></p>

<p><strong>随时重构</strong> 利用 AI 随时随地进行重构。</p>

<p><img src="https://raw.github.com/continuedev/continue/main/docs/static/img/inline.gif" alt="Refactor functions where you are coding" /></p>

<p><strong>代码库问答</strong> 利用 AI 基于你的代码库进行问答。</p>

<p><img src="https://raw.github.com/continuedev/continue/main/docs/static/img/codebase.gif" alt="Ask questions about your codebase" /></p>

<p><strong>快速文档上下文</strong> 快速使用框架的文档作为问答上下文。</p>

<p><img src="https://raw.github.com/continuedev/continue/main/docs/static/img/docs.gif" alt="Quickly use documentation as context" /></p>

<h2 id="模型选择">模型选择</h2>

<p>Continue 支持的模型非常多，具体可以看 <a href="https://github.com/continuedev/continue/blob/main/docs/docs/setup/select-model.md">选择模型</a> 这篇文档， 根据这篇文档的建议， 需要运行两个模型实例：</p>

<ul>
  <li><strong>问答：</strong> 建议使用 30B 以上参数的模型， 文档给的建议是 <code class="language-plaintext highlighter-rouge">llama-3</code> ：
    <ul>
      <li>算力充足 <code class="language-plaintext highlighter-rouge">llama-3-70B</code> ；</li>
      <li>算力有限 <code class="language-plaintext highlighter-rouge">llama-3-8B</code> ；</li>
    </ul>
  </li>
  <li><strong>代码提示：</strong> 建议使用 1～15B 参数即可， 文档给的建议是:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">DeepSeek Coder</code>：
        <ul>
          <li>算力充足 <code class="language-plaintext highlighter-rouge">deepseek-coder-v2:16b</code> ；</li>
          <li>算力有限 <code class="language-plaintext highlighter-rouge">deepseek-coder:6.7b</code> 或者 <code class="language-plaintext highlighter-rouge">deepseek-coder:1.3b</code> ；</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">StarCoder 2</code> ：
        <ul>
          <li>算力充足 <code class="language-plaintext highlighter-rouge">starcoder-2-7b</code> ；</li>
          <li>算力有限 <code class="language-plaintext highlighter-rouge">starcoder-2-3b</code> ；</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>经过实际测试， 建议的本地运行模型为：</p>

<ul>
  <li>问答模型， 选择 <code class="language-plaintext highlighter-rouge">llama-3-8B</code> 或者同级别的模型就可以了， 一般不会达到运行 <code class="language-plaintext highlighter-rouge">llama-3-70B</code> 的硬件；</li>
  <li>代码提示模型， 如果你有一张不是太旧的独立显卡，比如 12G 显存的 3060 ， 就可以流畅运行 <code class="language-plaintext highlighter-rouge">starcoder-2-7b</code> 了， 安装了 cude 之后， 体验非常好； 如果没有， 则可以运行 <code class="language-plaintext highlighter-rouge">starcoder-2-3b</code> ， 也能体验到不错的效果；</li>
</ul>

<blockquote>
  <p>如果算力有限， 优先运行代码提示模型， 因为这个使用的频率非常高， 在输入代码的同时， 会频繁的调用。 问答模型用的频率比较低， 因为需要用户主动提问。</p>
</blockquote>

<h2 id="llamacpp">llama.cpp</h2>

<p>建议使用 <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> 来运行大模型， 因为 llama.cpp 提供了非常灵活的选项， 对硬件支持也比较完善。 不管你是 Windows 系统还是 M1 芯片的 Mac 系统， 独立显卡还是集成显卡，甚至是 CPU 是否支持 AVX 指令， 都有特定的预编译版本， 根据自己电脑的硬件信息下载预编译的 <a href="https://github.com/ggerganov/llama.cpp/releases">llama.cpp</a> 二进制文件即可。</p>

<p>当然也可以根据 llama.cpp 的 <a href="https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#build">说明文档</a> ， 拉取源代码， 根据自身的硬件信息进行编译， 以获得最佳性能。</p>

<blockquote>
  <p>关于 llama.cpp 的使用， 可以参考文章 <a href="https://beginor.github.io/2024/04/11/run-llam-locally-on-macbook-m1.html">在 Macbook M1 上运行 AI 大模型 LLAMA</a> ， 文中也介绍了如何下载并转换模型文件。</p>
</blockquote>

<h3 id="运行代码提示模型">运行代码提示模型</h3>

<p>下载 <a href="https://huggingface.co/second-state/StarCoder2-7B-GGUF">starcoder2-7b</a> 或者 <a href="https://huggingface.co/second-state/StarCoder2-3B-GGUF">starcoder2-3b</a> 作为代码提示模型， 使用 llama.cpp 的 <code class="language-plaintext highlighter-rouge">llama-server</code> 运行， 命令如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/llama-server <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> 28080 <span class="se">\</span>
  <span class="nt">--threads</span> 8 <span class="nt">--parallel</span> 1 <span class="nt">--gpu-layers</span> 999 <span class="se">\</span>
  <span class="nt">--ctx-size</span> 8192 <span class="nt">--n-predict</span> 1024 <span class="nt">--defrag-thold</span> 1 <span class="se">\</span>
  <span class="nt">--model</span> models/starcoder2-3b-q5_k_m.gguf
</code></pre></div></div>

<p>如果只是个人使用的话， 对于代码提示来说， 3b 就足够了。 当然， 如果 GPU 算力充足的话， 也可以运行 7b 或者更高的模型。</p>

<blockquote>
  <p>经过测试， <code class="language-plaintext highlighter-rouge">starcoder</code> 提供的提示效果比 <code class="language-plaintext highlighter-rouge">deepseek-coder</code> 要好很多。</p>
</blockquote>

<h3 id="运行问答模型">运行问答模型</h3>

<p>下载 <a href="https://huggingface.co/QuantFactory/Meta-Llama-3-8B-Instruct-GGUF">llama-3-8b</a> 作为问答模型， 同样使用 llama.cpp 的 <code class="language-plaintext highlighter-rouge">llama-server</code> 运行， 命令如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/llama-server <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> 8080 <span class="se">\</span>
  <span class="nt">--threads</span> 8 <span class="nt">--parallel</span> 1 <span class="nt">--gpu-layers</span> 999 <span class="se">\</span>
  <span class="nt">--ctx-size</span> 8192 <span class="nt">--n-predict</span> 1024 <span class="nt">--defrag-thold</span> 1 <span class="se">\</span>
  <span class="nt">--model</span> models/meta-llama-3-8b-instruct.fp16.gguf
</code></pre></div></div>

<h2 id="continue-安装与配置">Continue 安装与配置</h2>

<p>Continue 提供了 Jetbrains IDE 以及 VSCode 的插件， 以 VSCode 为例， 只需要在 VSCode 的扩展窗口中搜索 <code class="language-plaintext highlighter-rouge">Continue.continue</code> , 下载并安装即可。</p>

<p><img src="/assets/post-images/20240711183314.png" alt="Continue VSCode Extension" /></p>

<p>安装之后， 可以直接跳过 Continue 的向导提示， 然后编辑它的配置文件 <code class="language-plaintext highlighter-rouge">~/.continue/config.json</code> ， 直接复制粘贴下面的 json 内容：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LLaMA"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llama.cpp"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llama3-8b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://127.0.0.1:8080"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"tabAutocompleteModel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LLaMA"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llama.cpp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"starcoder2-3b"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://127.0.0.1:28080"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"allowAnonymousTelemetry"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"embeddingsProvider"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"transformers.js"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>保存配置文件， Continue 插件会自动根据配置文件自动更新。</p>

<p>starcoder 模型支持10多种常见的开发语言， 因此只要配置好了 Continue 插件， 不管是写前端代码还是后端代码， 都可以享受 AI 带来的智能提示。</p>

<p>如果你的电脑 GPU 算力充足， 还可以把这个配置分享内网的小伙伴， 一起分享 AI 带来的便利。</p>

<h2 id="总结">总结</h2>

<p>本地运行的优势就不依赖互联网网络， 几乎没有什么网络延时， 也不需要注册什么账户之类的操作， 没有任何敏感代码泄漏的风险。 主要是显卡的负载， CPU 负载不高， 所以也几乎感觉不到卡顿。</p>
]]></content>
 </entry>
 
 <entry>
   <title>Vite 多 SPA 应用插件</title>
   <link href="https://beginor.github.io/2024/05/16/multi-spa-fallback-plugin-for-vite.html"/>
   <updated>2024-05-16T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/05/16/multi-spa-fallback-plugin-for-vite</id>
   <content type="html"><![CDATA[<p><a href="https://vitejs.dev/">Vite</a> 是目前非常流行的前端的构建工具， 越来越多的项目开始使用 Vite 作为构建工具， 基于插件的扩展机制， 支持多种前端项目 (React、 Vue 等) 都构建， 甚至连 Angular 17+ 都开始采用 Vite 作为开发服务器。</p>

<p>Vite 对于单个前端项目的路由支持的非常好， 不用任何配置，就会自动回落 (Fallback) 到默认页面 <code class="language-plaintext highlighter-rouge">/index.html</code> ， 因此对于单个前端项目来说非常的友好。</p>

<p>但是， 如果前端项目中采用了工作区 (monorepo) ， 通常会有多个前端应用，这时 Vite 的支持就没那么好了。 以 pnpm 的工作区为例， 目录结构如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- root/
  - apps/
    - app1/
      - index.html
      - ...
    - app2/
      - index.html
      - ...
  - packages/
    - lib1/
    - lib2/
  - package.json
  - pnpm-workspace.yaml
  - vite.config.js
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">app1</code> 和 <code class="language-plaintext highlighter-rouge">app2</code> 分别是两个独立的前端应用，分别有自己的路由， <code class="language-plaintext highlighter-rouge">app1/xxx</code> 应该回落到 <code class="language-plaintext highlighter-rouge">app1/index.html</code> ， <code class="language-plaintext highlighter-rouge">app2/yyy</code> 应该回落到 <code class="language-plaintext highlighter-rouge">app2/index.html</code> 。 <code class="language-plaintext highlighter-rouge">lib1</code> 和 <code class="language-plaintext highlighter-rouge">lib2</code> 分别是两个共享的类库项目。</p>

<p>Vite 支持这种情景下的构建， 根据 Vite 官方文档中的<a href="https://cn.vitejs.dev/guide/build.html#multi-page-app">多页面应用模式</a>， 进行如下配置即可：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// vite.config.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">resolve</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">path</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vite</span><span class="dl">'</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">build</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">rollupOptions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">input</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">app1</span><span class="p">:</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="dl">'</span><span class="s1">apps/app1/index.html</span><span class="dl">'</span><span class="p">),</span>
        <span class="na">app2</span><span class="p">:</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="dl">'</span><span class="s1">apps/app2/index.html</span><span class="dl">'</span><span class="p">),</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>有了上面的配置， 执行 <code class="language-plaintext highlighter-rouge">vite build</code> 命令，就可以同时编译两个前端应用。</p>

<p>但是在开发时，Vite 却不能同时处理两个前端应用的路由，即将 <code class="language-plaintext highlighter-rouge">app1/xxx</code> 回落到 <code class="language-plaintext highlighter-rouge">app1/index.html</code> ， <code class="language-plaintext highlighter-rouge">app2/yyy</code> 回落到 <code class="language-plaintext highlighter-rouge">app2/index.html</code> 。 而且翻看 Vite 的文档， 也没有找到相关的配置项。</p>

<p>其实这个问题很容易处理， 只要给 vite 内置的开发服务器 (dev server) 添加一个中间件， 修改一下浏览器的请求的路径即可。 不过 Vite 不像 <a href="https://browsersync.io/">browser-sync</a> 那样， 没有直接给开发服务器配置<a href="https://browsersync.io/docs/options#option-middleware">中间件</a>的选项，只能通过插件 API 对内部的开发服务器进行<a href="https://cn.vitejs.dev/guide/api-plugin.html#configureserver">配置</a>，来添加处理 http 请求的中间件， 那就只能开发一个简单的插件来实现了， 代码如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// spafallback-plugin.js</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 使用正则表达式定义一些 fallback 规则， 将 apps/app1/ 下除了 assets 目录之外</span>
<span class="c1">// 的请求都回落到 apps/app1/index.html</span>
<span class="kd">const</span> <span class="nx">fallbackRules</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span> <span class="na">pattern</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^/apps/app1/(?!assets/).*</span><span class="dl">'</span><span class="p">,</span> <span class="na">fallback</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/apps/app1/index.html</span><span class="dl">'</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">pattern</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^/apps/app2/(?!assets/).*</span><span class="dl">'</span><span class="p">,</span> <span class="na">fallback</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/apps/app2/index.html</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">];</span>

<span class="cm">/** 定义一个 spaFallback 插件并导出 */</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">spaFallbackPlugin</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Vite 默认的 public 目录</span>
  <span class="kd">let</span> <span class="nx">publicDir</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">;</span>
  <span class="c1">// 用正则表达式定义一些不需要处理的路径规则， 包括</span>
  <span class="c1">// 其它插件的路径， 源代码路径， node_modules 目录下的文件等，</span>
  <span class="c1">// 这些 URL 不需要在这个中间件中进行处理。</span>
  <span class="c1">// 如果还有其它的插件， 则下面的表达式可能还需要进行相应的修改。</span>
  <span class="kd">const</span> <span class="nx">bypassRegex</span> <span class="o">=</span> <span class="sr">/@vite|@react-refresh|</span><span class="se">\/</span><span class="sr">src</span><span class="se">\/</span><span class="sr">|</span><span class="se">\/</span><span class="sr">node_modules</span><span class="se">\/</span><span class="sr">|</span><span class="se">\.</span><span class="sr">map$/</span><span class="p">;</span>
  <span class="c1">// 定义一个处理请求的中间件， API 形式和 browser-sync 的中间件一致， </span>
  <span class="c1">// vite 使用了相同的库 [connect](https://github.com/senchalabs/connect)</span>
  <span class="c1">// 来处理 http 请求。</span>
  <span class="kd">function</span> <span class="nx">spaFallbackMiddleware</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">baseURL</span> <span class="o">=</span>  <span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">http</span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">://</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">host</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">uri</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span><span class="nx">baseURL</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">pathname</span> <span class="o">=</span> <span class="nx">uri</span><span class="p">.</span><span class="nx">pathname</span><span class="p">;</span>
    <span class="c1">// 如果是不需要处理的 URL 的话， 直接调用 next 并返回；</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">fs</span><span class="p">.</span><span class="nx">existsSync</span><span class="p">(</span><span class="nx">__dirname</span> <span class="o">+</span> <span class="nx">pathname</span><span class="p">)</span>
        <span class="o">||</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">existsSync</span><span class="p">(</span><span class="nx">publicDir</span> <span class="o">+</span> <span class="nx">pathname</span><span class="p">)</span>
        <span class="o">||</span> <span class="nx">bypassRegex</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">pathname</span><span class="p">)</span>
    <span class="p">)</span> <span class="p">{</span>
      <span class="nx">next</span><span class="p">();</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// 接下来就是根据上面定义的回落规则进行匹配， 如果请求的 URL 被某一条规则匹配到，</span>
    <span class="c1">// 修改当前请求的 URL 为对应的回落地址。</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">rule</span> <span class="k">of</span> <span class="nx">fallbackRules</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">regex</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">rule</span><span class="p">.</span><span class="nx">pattern</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">regex</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
        <span class="kd">let</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">rule</span><span class="p">.</span><span class="nx">fallback</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">uri</span><span class="p">.</span><span class="nx">search</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">url</span> <span class="o">+=</span> <span class="nx">uri</span><span class="p">.</span><span class="nx">search</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="c1">// 向控制台输出一下修改的路径信息，便于调试</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">pathname</span><span class="p">}</span><span class="s2"> change to: </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="nx">req</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="nx">next</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="c1">// 返回 vite 插件定义 </span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">spa-fallback</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">enforce</span><span class="p">:</span> <span class="dl">'</span><span class="s1">pre</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">apply</span><span class="p">:</span> <span class="dl">'</span><span class="s1">serve</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">configureServer</span><span class="p">:</span> <span class="p">(</span><span class="nx">server</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="c1">// 为 dev server 添加中间件， 这一步非常重要。</span>
      <span class="nx">publicDir</span> <span class="o">=</span> <span class="nx">server</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">publicDir</span><span class="p">;</span>
      <span class="nx">server</span><span class="p">.</span><span class="nx">middlewares</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">spaFallbackMiddleware</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>上面的代码不算很难， 而且注释也很全面，就不再解释了。 接下来就是在 <code class="language-plaintext highlighter-rouge">vite.config.js</code> 中使用这个插件， 代码如下：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// vite.config.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">resolve</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">path</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vite</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">react</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@vitejs/plugin-react</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 导入上面定义的 spaFallback 插件</span>
<span class="k">import</span> <span class="nx">spaFallback</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./spafallback-plugin.js</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">build</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">rollupOptions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">input</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">app1</span><span class="p">:</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="dl">'</span><span class="s1">apps/app1/index.html</span><span class="dl">'</span><span class="p">),</span>
        <span class="na">app2</span><span class="p">:</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="dl">'</span><span class="s1">apps/app2/index.html</span><span class="dl">'</span><span class="p">),</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">spaFallback</span><span class="p">(),</span> <span class="c1">//  注意把 spaFallback 插件放在最前面</span>
    <span class="nx">react</span><span class="p">(),</span>
  <span class="p">]</span>
<span class="p">})</span>
</code></pre></div></div>

<p>现在 <code class="language-plaintext highlighter-rouge">vite serve</code> 应该就可以正确的处理多个前端项目的路由了！</p>

<p>最后，感觉 Vite 是把 esbuild 、 rollup 以及 browser-sync 这三个流行的工具整合到了一起，基本上做到了开箱可用， 确实节省了很多配置的工作，但是有些高级选项 (比如本文用到的 http 中间件配置) ，却不能直接进行配置， 只能通过插件 API 进行配置。</p>
]]></content>
 </entry>
 
 <entry>
   <title>在 Macbook M1 上运行 AI 大模型 LLAMA</title>
   <link href="https://beginor.github.io/2024/04/11/run-llam-locally-on-macbook-m1.html"/>
   <updated>2024-04-11T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/04/11/run-llam-locally-on-macbook-m1</id>
   <content type="html"><![CDATA[<p><img src="/assets/post-images/20240411160148.png" alt="llama" /></p>

<h2 id="环境准备">环境准备</h2>

<p>在 MacBook 上本地运行大模型， 如要准备 Python 和 Xcode 以及 Git ， 如果还没有安装的话， 按照下面的命令安装即可， 如果已经安装好了， 就可以跳过这两个步骤。</p>

<h3 id="python">Python</h3>

<p>Python 目前建议安装 3.10 版本， 各方面支持都比较完善。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>python@3.10
</code></pre></div></div>

<p>Python 安装好之后， 再安装 <code class="language-plaintext highlighter-rouge">torch</code> <code class="language-plaintext highlighter-rouge">torchaudio</code> <code class="language-plaintext highlighter-rouge">torchvision</code> ， 命令如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip3.10 <span class="nb">install </span>torch torchaudio torchvision
</code></pre></div></div>

<h3 id="xcode">Xcode</h3>

<p>Xcode 也是必须的， 因为接下来要从源代码编译 llama.cpp。 如果还没有安装 Xcode ， 只要 Xcode 的命令行版本就可以， 在终端中输入下面的命令， 根据提示操作即可。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xcode-select <span class="nt">--install</span>
</code></pre></div></div>

<h3 id="git">Git</h3>

<p>除了基本的 Git 之外， 下载模型文件还需要 Git LFS ， 可以用下面的命令一起安装：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>git git-lfs
</code></pre></div></div>

<p>完成之后， 输入下面的命令初始化 Git LFS :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git lfs <span class="nb">install</span>
</code></pre></div></div>

<h2 id="下载-llamacpp-源代码并编译">下载 llama.cpp 源代码并编译</h2>

<p><a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> 对 M1 系列的 CPU 进行了专门的优化， 不仅可以充分发挥苹果 M1 芯片统一内存的优势， 而且能够调用 M1 芯片的显卡， 所以在 MacBook 上运行大模型， llama.cpp 是首选。</p>

<p>虽然 <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> 提供编译好的二进制文件下载， 但是很多脚本和示例都在源代码中，因此还是需要克隆源代码下来并编译。</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:ggerganov/llama.cpp.git
</code></pre></div></div>

<p>在 macOS 系统上， 只需要进入到 <code class="language-plaintext highlighter-rouge">llama.cpp</code> 目录， 然后执行 <code class="language-plaintext highlighter-rouge">make</code> 命令即可：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>llama.cpp
make
</code></pre></div></div>

<p>llama.cpp 很活跃， 经常更新， 可以通过下面的命令更新并编译：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>llama.cpp
git pull
make
</code></pre></div></div>

<blockquote>
  <p>其它系统可以参照 llama.cpp 的 <a href="https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#build">说明</a> 进行编译。</p>
</blockquote>

<h2 id="下载模型">下载模型</h2>

<p>模型就要根据自己电脑的配置有选择的下载， 对于个人电脑来说， 一般是 7b/13b/34b 参数的模型， 再多参数的模型就没必要下载了， 不仅体积庞大，费时费力， 而且在个人电脑上几乎无法运行。</p>

<p>我的 MacBook 的配置是 M1 Max 64G + 1T ， 最多可将 50G 左右的内存做显存使用， 最终保留了下面几个模型文件， 仅供参考。</p>

<ul>
  <li><a href="https://huggingface.co/codellama/CodeLlama-7b-Instruct-hf">CodeLlama-7b-Instruct-hf</a></li>
  <li><a href="https://huggingface.co/codellama/CodeLlama-13b-Instruct-hf">CodeLlama-13b-Instruct-hf</a></li>
  <li><a href="https://huggingface.co/codellama/CodeLlama-13b-Python-hf">CodeLlama-13b-Python-hf</a></li>
  <li><a href="https://huggingface.co/Phind/Phind-CodeLlama-34B-v2">Phind/Phind-CodeLlama-34B-v2</a></li>
  <li><a href="https://huggingface.co/01-ai/Yi-34B-200K">01-ai/Yi-34B-200K</a></li>
</ul>

<p>以 CodeLlama-7b-Instruct-hf 为例， 下载命令为：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 确认 git lfs 已经安装</span>
git lfs <span class="nb">install</span>

<span class="c"># 在中国大陆从 huggingface 下载模型需要代理</span>
<span class="nb">export </span><span class="nv">HTTPS_PROXY</span><span class="o">=</span>socks5://127.0.0.1:1080
<span class="nb">export </span><span class="nv">HTTP_PROXY</span><span class="o">=</span>socks5://127.0.0.1:1080
<span class="nb">export </span><span class="nv">ALL_PROXY</span><span class="o">=</span>socks5://127.0.0.1:1080

<span class="c"># 下载模型文件</span>
git clone <span class="nt">--progress</span> git@hf.co:codellama/CodeLlama-7b-Instruct-hf codellama-7b-instruct-hf
</code></pre></div></div>

<p>接下来就是等待， 考验代理的稳定性与速度的时刻到了。 如果中途下载失败， 输入下面的命令可以继续， 不需要重新开始：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>codellama-7b-instruct-hf
git git restore <span class="nt">--progress</span> <span class="nt">--source</span><span class="o">=</span>HEAD :/
</code></pre></div></div>

<h2 id="转换格式以及量化">转换格式以及量化</h2>

<p>将模型转换为 llama.cpp 支持的 <code class="language-plaintext highlighter-rouge">gguf</code> 格式， 在 llama.cpp 目录下， 执行命令：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3.10 convert.py models/codellama-7b-instruct-hf
</code></pre></div></div>

<p>完成之后会在 codellama-7b-instruct-hf 目录下生成对应的 gguf 文件。</p>

<h3 id="量化">量化</h3>

<p>我的理解， 量化其实就是对模型就行适当的简化， 这篇文章 <a href="https://towardsdatascience.com/quantize-llama-models-with-ggml-and-llama-cpp-3612dfbcc172">Quantize Llama models with GGUF and llama.cpp</a> 说的很清楚， 建议使用 <code class="language-plaintext highlighter-rouge">q5_k_m</code> 模式进行量化。</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/quantize models/codellama-7b-instruct-hf.f16.gguf models/codellama-7b-instruct-hf.q4_0.gguf q5_k_m
</code></pre></div></div>

<blockquote>
  <p>如果机器配置够高的话， 也可以不做量化，直接运行。 在 M1 Max 64G 内存的配置上， 可以直接运行 7b 参数级别的模型， 更多参数的模型则需要量化之后才能运行。</p>
</blockquote>

<h2 id="运行模型">运行模型</h2>

<h3 id="命令行">命令行</h3>

<p>在默认情况下， 将会调用模型输出一段文字，然后退出。</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/main <span class="nt">-m</span> ./models/chinese-llama-2-13b-hf.q5_k_m.gguf
</code></pre></div></div>

<p>输出结果：</p>

<pre><code class="language-txt">她将去中国，到一个没有互联网的地方。“我要去那里工作一段时间，把网络生活放在一边。”她说。 [end of text]
</code></pre>

<p>如果要交互式聊天， 需要添加一些参数， 示例如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/main <span class="nt">-m</span> ./models/chinese-llama-2-13b-hf.q5_k_m.gguf <span class="nt">-c</span> 512 <span class="nt">-b</span> 1024 <span class="nt">-n</span> 256 <span class="nt">--keep</span> 48 <span class="se">\</span>
    <span class="nt">--repeat_penalty</span> 1.0 <span class="nt">--color</span> <span class="nt">-i</span> <span class="se">\</span>
    <span class="nt">-r</span> <span class="s2">"User:"</span> <span class="nt">-f</span> prompts/chat-with-teacher.txt
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/main  <span class="nt">--interactive-first</span> <span class="se">\</span>
  <span class="nt">--model</span> ./models/chinese-llama-2-13b-hf.q5_k_m.gguf <span class="se">\</span>
  <span class="nt">--temp</span> 0.2 <span class="se">\</span>
  <span class="nt">--keep</span> <span class="nt">-1</span> <span class="se">\</span>
  <span class="nt">-f</span> prompts/chat-with-baichuan.txt <span class="se">\</span>
  <span class="nt">-r</span> <span class="s2">"用户:"</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/main  <span class="nt">--interactive-first</span> <span class="se">\</span>
  <span class="nt">--model</span> ./models/chinese-llama-2-13b-hf.q5_k_m.gguf <span class="se">\</span>
  <span class="nt">--temp</span> 0.2 <span class="se">\</span>
  <span class="nt">--keep</span> <span class="nt">-1</span> <span class="se">\</span>
  <span class="nt">-f</span> prompts/chat-with-teacher.txt <span class="se">\</span>
  <span class="nt">-r</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">学生:"</span>
</code></pre></div></div>

<p>运行模型的时候， 在 GPU 历史窗口， 可以看到显卡是拉满的， 截图如下：</p>

<p><img src="/assets/post-images/20240411155919.png" alt="Apple M1 Max" /></p>

<h3 id="服务端">服务端</h3>

<p>llama.cpp 还提供了与 open-ai 兼容的服务端 <code class="language-plaintext highlighter-rouge">llama.cpp/server</code>， 使用示例如下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>llama.cpp/server <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> 8080 <span class="se">\</span>
  <span class="nt">--ctx-size</span> 2048 <span class="se">\</span>
  <span class="nt">--n-predict</span> <span class="nt">-1</span> <span class="se">\</span>
  <span class="nt">--model</span> ./models/codellama-7b-instruct-hf.f32.gguf
</code></pre></div></div>

<p>服务端运行起来之后， 就可以脱离命令行， 进行 API 调用或者使用 Postman 之类的 http 测试工具进行测试。</p>

<h3 id="web-客户端">Web 客户端</h3>

<p>llama.cpp/server 默认的界面非常简单， 只能说是测试用， 用浏览器访问 <code class="language-plaintext highlighter-rouge">http://localhost:8080/</code> 可以看到， 这里就不截图了， 太丑。</p>

<p>但是， llama.cpp/server 提供的 API 是与 open-ai 兼容的， 很多第三方的 ChatGPT 客户端都可以使用， 比如 <a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web">ChatGPT-Next-Web</a> ， 稍微查看了它的说明， 只要调整一下参数就可以直接运行它的 Docker 镜像， 无需任何修改：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--name</span> chatgpt-next-web <span class="se">\</span>
  <span class="nt">--publish</span> 3000:3000 <span class="se">\</span>
  <span class="nt">--env</span> <span class="nv">OPENAI_API_KEY</span><span class="o">=</span>1234567890 <span class="se">\</span>
  <span class="nt">--env</span> <span class="nv">BASE_URL</span><span class="o">=</span>http://192.168.3.232:8080 <span class="se">\</span>
  yidadaa/chatgpt-next-web
</code></pre></div></div>

<ul>
  <li>
    <p>服务端使用运行 <code class="language-plaintext highlighter-rouge">codellama-7b-instruct</code> ， 在客户端询问代码相关的问题， 截图如下：</p>

    <p><img src="/assets/post-images/20240411163453.png" alt="Chat with codellama" /></p>
  </li>
  <li>
    <p>在服务端运行 <code class="language-plaintext highlighter-rouge">01-ai/Yi-34B-200K</code> ， 在客户端询问常规问题， 截图如下：</p>

    <p><img src="/assets/post-images/20240411164210.png" alt="chat with 01-ai" /></p>
  </li>
</ul>
]]></content>
 </entry>
 
 <entry>
   <title>GNU Screen 命令简介</title>
   <link href="https://beginor.github.io/2024/04/08/introduction-to-gnu-screen-command.html"/>
   <updated>2024-04-08T00:00:00+00:00</updated>
   <id>https://beginor.github.io/2024/04/08/introduction-to-gnu-screen-command</id>
   <content type="html"><![CDATA[<p><img src="/assets/post-images/20240408103805.png" alt="GNU Screen" /></p>

<p>在操作 Linux 服务器时， 经常需要同时使用多个命令， 比如开着 <code class="language-plaintext highlighter-rouge">htop</code> 查看服务器负载， 同时调整配置等。虽然可以使用高级的客户端终端 (<code class="language-plaintext highlighter-rouge">iTerm2</code>, <code class="language-plaintext highlighter-rouge">Microsoft Terminal</code>) 同时建立多个连接到服务器， 但是有些服务器需要二次认证， 甚至不允许一个帐号进行多个连接，同时连接有的时候也会很不方便。 所以在服务器的终端内进行分屏也是有一定需要的。 <a href="https://www.gnu.org/software/screen/">GNU Screen</a> 可以说是终端分屏/多任务的利器， 而且大多数的 Linux 服务器默认安装， 至少也是在默认的软件源中， 不需要添加第三方源， 安装与使用非常方便。</p>

<h2 id="工作模式">工作模式</h2>

<p><code class="language-plaintext highlighter-rouge">Screen</code> 的工作模式类似于 <code class="language-plaintext highlighter-rouge">vim/vi</code> 编辑器， 可分为操作模式和命令模式：</p>

<ul>
  <li>操作模式： 在此模式下，可以正常的输入各种指令， 和普通的 Shell 差不多；</li>
  <li>命令模式： 在操作模式下， 按 <code class="language-plaintext highlighter-rouge">Ctrl + A</code> 就会进入命令模式， 可以实现终端分屏、断开会话、重连会话以及在多个会话中切换等操作；</li>
</ul>

<h2 id="后台命令">后台命令</h2>

<p><code class="language-plaintext highlighter-rouge">Screen</code> 最常用的操作之一是执行后台命令， 常见的数据备份与同步命令， 往往执行的时间会比较长， 就可以使用 <code class="language-plaintext highlighter-rouge">Screen</code> 来执行， 示例步骤为：</p>

<ol>
  <li>在终端中直接输入 <code class="language-plaintext highlighter-rouge">screen</code> ， 就会自动创建一个新的会话， 并进入这个会话；</li>
  <li>在会话可以执行任意 shell 命令， 比如 <code class="language-plaintext highlighter-rouge">htop</code> ；</li>
  <li>在 Screen 会话中按快捷键 <code class="language-plaintext highlighter-rouge">Ctrl + A</code> <code class="language-plaintext highlighter-rouge">d</code> ，即可断开当前会话， 第 2 步输入的命令会继续在后台执行， 当前窗口恢复到输入 screen 命令之前的状态；</li>
  <li>
    <p>输入 <code class="language-plaintext highlighter-rouge">screen -ls</code> 可以查看所有的会话， 输出如下：</p>

    <p><img src="/assets/post-images/20240116175423.png" alt="screen list" /></p>
  </li>
  <li>再次输入 <code class="language-plaintext highlighter-rouge">screen -r [session name]</code> 即可恢复对应的会话状态；</li>
</ol>

<p>利用 screen 来执行后台命令非常的方便，而且可以随时恢复会话状态， 查看执行状况。</p>

<blockquote>
  <p>如果不需要查看执行状态， 单纯的是后台命令则可以使用 Sub Shell 来执行。</p>
</blockquote>

<h2 id="终端分区">终端分区</h2>

<p>终端分区才是 screen 的强大之处， 可以将一个终端分成多个区域。 现代化的软件 (iTerm2, Microsoft Terminal, VSCode, Sublime 等) 都提供了分屏的功能, 让用户可以同时处理多个文档或多个任务。 screen 则可以在同一个终端内实现屏幕分区， 且各个分区相互独立， 互不干扰。</p>

<p>要使用屏幕分区， 就要掌握 screen 的命令模式， 这个与 vi/vim 的工作模式很类似， 如果熟悉 vi/vim 的话， 将会有非常熟悉的感觉。</p>

<ol>
  <li>
    <p>准备工作</p>

    <p>打开命令终端， 输入 <code class="language-plaintext highlighter-rouge">screen</code> 命令， 得到如下提示，</p>

    <p><img src="/assets/post-images/20240408100712.png" alt="screen greeting" /></p>

    <p>然后按空格键或者回车键即可。</p>
  </li>
  <li>
    <p>水平分区</p>

    <p>在 screen 创建的终端中， 按 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> ， 再按 <code class="language-plaintext highlighter-rouge">Shift+S</code> ， 即可水平分区， 如下图所示：</p>

    <p><img src="/assets/post-images/20240408101635.png" alt="horizontal split" /></p>
  </li>
  <li>
    <p>垂直分区</p>

    <p>在 screen 终端中， 按 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> ， 再按 <code class="language-plaintext highlighter-rouge">Shift+\</code> ， 即可垂直分区， 如下图所示：</p>

    <p><img src="/assets/post-images/20240408101927.png" alt="vertical split" /></p>
  </li>
  <li>
    <p>切换活动分区</p>

    <p>现在创建了多个分区， 但是光标还在第一个分区内， 要切换分区， 则需要快捷键 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> ， 再按 <code class="language-plaintext highlighter-rouge">Tab</code> ， 就会切换到下一个分区， 如果要继续切换， 则需要再次按 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> 和 <code class="language-plaintext highlighter-rouge">Tab</code> 。</p>

    <p><img src="/assets/post-images/20240408102440.png" alt="switch active pane" /></p>

    <p>如果切换到的分区还没有创建会话， 则需要按快捷键 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> ， 再按 <code class="language-plaintext highlighter-rouge">c</code> 即可创建会话。</p>

    <p><img src="/assets/post-images/20240408102533.png" alt="create new session in pane" /></p>
  </li>
  <li>
    <p>关闭分区</p>

    <p>推出会话之后， 分区还在， 如果要关闭分区， 则需要按快捷键 <code class="language-plaintext highlighter-rouge">Ctrl+A</code> ， 再按 <code class="language-plaintext highlighter-rouge">Shift+X</code> 即可。</p>
  </li>
</ol>

<h2 id="配置文件">配置文件</h2>

<p>可以使用 <code class="language-plaintext highlighter-rouge">$HOME/.screenrc</code> 文件对 screen 命令进行自定义配置， 而且网上已经有很多网友分享的配置文件。</p>

<p>GNU Sceren 是一个在终端内分屏的软件， Win/Lin/Mac 都可以运行， 熟练了之后，感觉可以把 iTerm2 给删掉了。</p>
]]></content>
 </entry>
 

</feed>
