<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 20 章 AI 推理与服务 on Go 语言原本</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/</link><description>Recent content in 第 20 章 AI 推理与服务 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/index.xml" rel="self" type="application/rss+xml"/><item><title>20.1 推理运行时与 FFI</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/runtime/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/runtime/</guid><description>&lt;h1 id="201-推理运行时与-ffi"&gt;20.1 推理运行时与 FFI&lt;/h1&gt;
&lt;p&gt;第 18、19 章反复出现的那道 FFI 边界，到了大模型这里有了它最当下的一副面孔。训练大模型几乎是
Python 与 CUDA 的天下，可当一个模型训好、要被部署去&lt;strong&gt;服务&lt;/strong&gt;千万次请求时，舞台换了主角，
Go 在这一层站得很稳。这一章讲 Go 如何承担 AI 的推理与服务，而第一节要先解决最底层的问题:
Go 自己并不做矩阵乘，它得接入一个本地推理运行时,而这次接入，又是第 18 章那道边界。&lt;/p&gt;
&lt;h2 id="2011-训练在-python推理与服务在哪"&gt;20.1.1 训练在 Python，推理与服务在哪&lt;/h2&gt;
&lt;p&gt;先把分工看清。&lt;strong&gt;训练&lt;/strong&gt;是一件研究性的事:试不同的结构、调超参、看损失曲线，要的是表达的灵活与
生态的丰富，Python 加 PyTorch、再加 CUDA，是它无可争议的家园。Go 在训练这一侧没有位置，
也不必有。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;推理与服务&lt;/strong&gt;是另一种事。模型已经冻结，权重不再变，剩下的问题清一色是&lt;strong&gt;系统问题&lt;/strong&gt;:怎么用
高吞吐、低延迟地服务大量并发请求;怎么把一个几 GB 的模型稳定地装进内存、喂给设备;怎么做成一个
能一键部署、抗住生产流量、好监控好运维的服务。把这串问题念一遍，会发现它正是 Go 被设计出来要
解决的那一类:静态编译的单文件部署、天生的并发、可控的内存、成熟的网络栈。所以「训练归 Python、
推理与服务归 Go」不是偶然的站队，而是两种工作的性质把两门语言各自吸到了它擅长的那一层。
当下最流行的本地大模型服务 Ollama,正是用 Go 写就的。&lt;/p&gt;
&lt;h2 id="2012-接入本地推理运行时又是那道边界"&gt;20.1.2 接入本地推理运行时：又是那道边界&lt;/h2&gt;
&lt;p&gt;Go 站在服务层，可矩阵乘、注意力、量化这些真正吃算力的活儿，Go 并不亲自做,它把这些交给一个
&lt;strong&gt;本地推理运行时&lt;/strong&gt;:&lt;code&gt;llama.cpp&lt;/code&gt;/&lt;code&gt;ggml&lt;/code&gt;(C/C++)、ONNX Runtime、或厂商的运行时。这些运行时是
用 C/C++ 写的高度优化的张量计算库，能调度 CPU 的 SIMD、也能驱动 GPU。Go 要用它们，
靠的还是 cgo。&lt;/p&gt;
&lt;p&gt;于是第 18 章的整套机制原样回来了。Ollama 的文档说得很直白:它「包含用 CGO 编译的原生代码」,
原生推理引擎用 CMake 构建，按需编译出 CUDA、ROCm、Vulkan 等后端。这意味着 15.6 与第 18 章讲过的
代价一并继承:构建期需要 C/C++ 工具链、失去纯 Go 的轻便（&lt;a href="../../../part5toolchain/ch15compile/cgo"&gt;15.6.4&lt;/a&gt;),
运行期每次跨界要付那笔状态转换的税。Ollama 文档甚至点出了一个 15.6 没细说的脆弱处:Go 与 C
两侧共享的数据结构「可能不同步，导致意外崩溃」,这正是 cgo 把两个世界缝在一起时，缝合处最阴险的
一类 bug。&lt;/p&gt;</description></item><item><title>20.2 分词与张量</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/tokenize/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/tokenize/</guid><description>&lt;h1 id="202-分词与张量"&gt;20.2 分词与张量&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././runtime"&gt;20.1&lt;/a&gt; 把权重与张量的家安顿在了原生运行时一侧，Go 只在边界上递句柄、搬小数据。
这一节深入那「小数据」本身:文本怎么变成模型能吃的数字，数字又怎么变回文本。这件看似琐碎的事，
藏着一个 Go 程序员会心一笑的细节,它几乎就是第 5 章「字符串是一段不可变字节」的直接应用，
而且一旦疏忽，就会在流式输出时吐出乱码。&lt;/p&gt;
&lt;h2 id="2021-分词文本与模型之间的翻译层"&gt;20.2.1 分词：文本与模型之间的翻译层&lt;/h2&gt;
&lt;p&gt;模型不认识文本。它的输入和输出都是&lt;strong&gt;整数&lt;/strong&gt;,词表里的 token 编号。把人类的文本与模型的整数互相
翻译的这一层，叫&lt;strong&gt;分词器&lt;/strong&gt;(tokenizer)。它做两件互逆的事:把输入字符串切成一串 token id（编码),
把模型生成的 token id 拼回字符串（解码)。&lt;/p&gt;
&lt;p&gt;现代大模型几乎都用&lt;strong&gt;字节对编码&lt;/strong&gt;(Byte-Pair Encoding, BPE)或其变体。它的思路是数据驱动的:
从最细的单位出发，统计语料里最常一起出现的相邻对，把高频对合并成一个新 token，反复合并，
最终得到一张几万项的词表，常见词是一个完整 token，生僻词则被拆成几个子词片段。这样既控制了
词表大小，又能表示任何输入，不会遇到「未登录词」。&lt;/p&gt;
&lt;h2 id="2022-为什么是字节而不是字符第-5-章的回响"&gt;20.2.2 为什么是字节，而不是字符：第 5 章的回响&lt;/h2&gt;
&lt;p&gt;这里有一个对 Go 程序员格外亲切的关键:当代主流的 BPE，是&lt;strong&gt;字节级&lt;/strong&gt;(byte-level)的。它的最小
单位不是 Unicode 字符（码点)，而是 &lt;strong&gt;UTF-8 字节&lt;/strong&gt;。词表里的合并，发生在字节序列上。&lt;/p&gt;
&lt;p&gt;这正是第 5 章反复强调的那件事:&lt;a href="../../../part2lang/ch05data/string"&gt;5.2&lt;/a&gt; 说过，Go 的字符串
本质是一段&lt;strong&gt;不可变的字节序列&lt;/strong&gt;,&lt;code&gt;range&lt;/code&gt; 一个字符串得到的是码点（rune），而下标索引得到的是字节。
字节级 BPE 与 Go 的字符串模型严丝合缝:两者都把文本看作字节。于是把一段 Go 字符串喂给字节级
分词器，概念上不需要任何「字符」的中间层,它处理的就是字符串底层那串字节。&lt;/p&gt;
&lt;p&gt;但字节级也埋了一个雷，而这个雷恰好踩在 Go 的痛点上:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个 token 的边界，未必落在一个完整 UTF-8 字符的边界上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个中文字符在 UTF-8 里占 3 个字节，一个 emoji 可能占 4 个。字节级 BPE 完全可能把这 3 个字节
&lt;strong&gt;拆进两个相邻的 token&lt;/strong&gt;。这在编码时无所谓，可在&lt;strong&gt;逐 token 解码&lt;/strong&gt;时就出事了:当模型先吐出
半个字符的那个 token，你拿到的是一串&lt;strong&gt;不完整的 UTF-8 字节&lt;/strong&gt;,它还不构成一个合法的码点，
要等下一个 token 到达、补齐剩下的字节，才拼得出那个字符。&lt;/p&gt;</description></item><item><title>20.3 服务、批处理与流式</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/serving/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch20inference/serving/</guid><description>&lt;h1 id="203-服务批处理与流式"&gt;20.3 服务、批处理与流式&lt;/h1&gt;
&lt;p&gt;前两节把单次推理的底层铺好了:&lt;a href=".././runtime"&gt;20.1&lt;/a&gt; 经 cgo 接入运行时、安顿好权重与张量,
&lt;a href=".././tokenize"&gt;20.2&lt;/a&gt; 讲清了 token 进、token 出。可一个真实的服务，要同时伺候成千上万条这样的
请求，每条都在持续地吐 token。怎么把它们高效、稳定地组织起来，是一个彻头彻尾的&lt;strong&gt;并发与调度&lt;/strong&gt;
问题,而这正是 Go 的主场。这一节把第 10 章的通道、第 7 章的 context，落到大模型服务上。&lt;/p&gt;
&lt;h2 id="2031-一个请求的一生一条-token-流"&gt;20.3.1 一个请求的一生：一条 token 流&lt;/h2&gt;
&lt;p&gt;先看清一次生成的形状。大模型是&lt;strong&gt;自回归&lt;/strong&gt;的:它一次只生成一个 token，把这个 token 接回输入，
再算下一个，循环往复，直到生成结束符或达到长度上限。所以从时间轴上看，&lt;strong&gt;一个请求不是一次
请求-响应，而是一条随时间流出的 token 流&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个形状和 Go 的并发模型天造地设:一个 goroutine 跑生成循环，每算出一个 token 就往一个通道里送,
下游从通道里收，正是第 10 章「用通信共享内存」的标准句式。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 一个请求 = 一个生成 goroutine，把 token 逐个送进通道&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 一次 cgo 调用，算一个 token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 送给下游（HTTP handler）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 客户端走了，立刻收手&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEOS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段骨架已经埋下了本节三个主题的种子:&lt;code&gt;out&lt;/code&gt; 通道是&lt;strong&gt;流式&lt;/strong&gt;,&lt;code&gt;ctx.Done()&lt;/code&gt; 是&lt;strong&gt;取消&lt;/strong&gt;,
而当 &lt;code&gt;out&lt;/code&gt; 满、&lt;code&gt;out &amp;lt;- tok&lt;/code&gt; 阻塞时就是&lt;strong&gt;背压&lt;/strong&gt;。下面逐一展开。&lt;/p&gt;</description></item></channel></rss>