惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
有赞技术团队
有赞技术团队
博客园_首页
H
Help Net Security
GbyAI
GbyAI
aimingoo的专栏
aimingoo的专栏
V
Visual Studio Blog
The Cloudflare Blog
腾讯CDC
Jina AI
Jina AI
Last Week in AI
Last Week in AI
月光博客
月光博客
博客园 - 叶小钗
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
Blog — PlanetScale
Blog — PlanetScale
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
Y
Y Combinator Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 聂微东
爱范儿
爱范儿
N
Netflix TechBlog - Medium
F
Fortinet All Blogs

A/B's Blog

自制操作系统(36):kilo移植,tty能力加强 – A/B's Blog vllm源码阅读:scheduler与pd分离 – A/B's Blog 自制操作系统(35):Ext2文件系统驱动——写入支持 – A/B's Blog vLLM vs SGLang 性能实测:5090、Qwen2.5 7B、吞吐与 p99 延迟 – A/B's Blog 自制操作系统(34):Ext2文件系统驱动——目录遍历,路径分量解析,块、inode分配器,缓存刷新 – A/B's Blog 从 static batching 到 continuous batching:一文看懂 LLM 推理吞吐量优化 – A/B's Blog 自制操作系统(33):Ext2文件系统驱动——inode解析,打开、读取文件 – A/B's Blog 从Attention讲到如何计算你家的显卡能塞下多大的大模型 – A/B's Blog PagedAttention 是什么?从 OS 分页机制看懂 vLLM 的吞吐量优化 – A/B's Blog 自制操作系统(32):Ext2文件系统驱动——Ext2挂载,超级块解析 – A/B's Blog 自制操作系统(31):Ext2文件系统驱动——ATA PIO驱动读写扇区,块设备抽象 – A/B's Blog zmoe.com WSL下启动的VSCode,Cline、Roo code等插件无法访问网络的问题 – A/B's Blog 自制操作系统(28):TCP(五)——HTTP、TELNET – A/B's Blog
初探ollama源码 – A/B's Blog
B分之A 这家伙很懒,什么都没写 返回 · 2026-05-29 · via A/B's Blog

从readme.md 开始

readme.md中,说明了ollama是一个能够帮助你拉取、本地部署大模型,或者接入大模型服务提供商的服务,除此之外,他还能够作为网关,帮助你对外在网络上开放大模型的RESTful API。

llama.cpp

readme中还介绍了llama.cpp,这是一个后端项目,意味着你可以只把ollama作为前端网关使用,后端替换成llama.cpp。

client, server 和 model

ollama可以大致分为三个部分:前端的client,后端的server和model。

client

client的代码:这部分的代码在api/clent.go中。从代码可以看到,client有着Generate、Chat、Pull、Push等丰富的方法,以generate为例:

func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
    return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
        var resp GenerateResponse
        if err := json.Unmarshal(bts, &resp); err != nil {
            return err
        }

        return fn(resp)
    })
}

从上面的代码可以看到,generate最终会调用私有的stream函数,而这个函数实际上是通过rest请求去调用server的命令。实际上,仔细观察你会发现,client大部分的方法都会调用dostream函数,这两个函数都会去发送rest请求到server侧,只不过是是否为流式返回响应的差别。这种许多函数的调用链最终汇聚到一两个函数的调用的“调用收敛”模式很常见,而其中的核心函数被称为”窄腰”(narrow waist)。

而当我们在命令行输入ollama run llama3时,这个命令本身就会启动一个client。做这个命令分发的代码在cmd/cmd.go中,它使用了corba来做命令分发。

cmd.go的树状组织

cmd.go里面,你可以看到rootCmd,然后在这个根的基础上,发展出许多二级的cmd:

    rootCmd.AddCommand(
        serveCmd,
        createCmd,
        showCmd,
        runCmd,
        stopCmd,
        pullCmd,
        pushCmd,
        signinCmd,
        loginCmd,
        signoutCmd,
        logoutCmd,
        listCmd,
        psCmd,
        copyCmd,
        deleteCmd,
        runnerCmd,
        launch.LaunchCmd(checkServerHeartbeat, runInteractiveTUI),
    )

我没有细看下去,但是也许会有三级的cmd——至少有三级的flag。因此可以用树状的方式去展开这些命令的匹配模式。

corba的“一鱼两吃”

corba的命令是这样子注册的,拿runCmd举例:

    runCmd := &cobra.Command{
        Use:     "run MODEL [PROMPT]",
        Short:   "Run a model",
        Args:    cobra.MinimumNArgs(1),
        PreRunE: checkServerHeartbeat,
        RunE:    RunHandler,
    }

这里对应的是ollama run这条命令,我初看的时候,纳闷是在哪个参数注册的run,后面才发现,原来在Use这里——Use: “run MODEL [PROMPT]”;corba对use参数做了一鱼两吃,既能用它输出帮助文档,又对这个use做模式提取,把里面的第一个词提取出来注册为子命令。。。真的不得不佩服很有想法。

server

今天不深究server的代码,但是我们可以先看看server是如何启动的。

cmd/start.go

//go:build darwin || windows

package cmd

import (
    "context"
    "errors"
    "time"

    "github.com/ollama/ollama/api"
)

func waitForServer(ctx context.Context, client *api.Client) error {
    // wait for the server to start
    timeout := time.After(5 * time.Second)
    tick := time.Tick(500 * time.Millisecond)
    for {
        select {
        case <-timeout:
            return errors.New("timed out waiting for server to start")
        case <-tick:
            if err := client.Heartbeat(ctx); err == nil {
                return nil // server has started
            }
        }
    }
}

start.go的代码如上,可以看到start.go有一个神奇的顶部注释:

//go:build darwin || windows

这是什么意思呢?这个是Go 的 build tag,意味着这个文件只会在macOS和Windows上编译。虽然这不是我们想看的linux下如何启动background server的代码,这里是指定平台会用到的公共工具,但是我们还是可以看看waitForServer这个函数,用了两个定时器和一个select,总体的意思是每500ms通过调用客户端的心跳函数,检查其对应服务端是否健康,5s后超时退出。

话说回来,那我们想看的linux相关代码在哪呢?原来还在cmd目录下。

多平台

我们可以在cmd目录下看到一系列的文件:

cmd/start.go            ← darwin || windows
cmd/start_darwin.go     ← 只在 macOS
cmd/start_default.go    ← 其他平台
cmd/start_windows.go    ← 只在 Windows
cmd/background_unix.go  ← unix 类

我们可以查看linux系统相关的start_default.go和background_unix.go文件:

aoverb@BA:~/mygo/ollama$ cat cmd/start_default.go cmd/background_unix.go
//go:build !windows && !darwin
package cmd
import (
        "context"
        "errors"
        "github.com/ollama/ollama/api"
)
func startApp(ctx context.Context, client *api.Client) error {
        return errors.New("could not connect to ollama server, run 'ollama serve' to start it")
}
//go:build !windows
package cmd
import "syscall"
// backgroundServerSysProcAttr returns SysProcAttr for running the server in the background on Unix.
// Setpgid prevents the server from being killed when the parent process exits.
func backgroundServerSysProcAttr() *syscall.SysProcAttr {
        return &syscall.SysProcAttr{
                Setpgid: true,
        } 

可以看到linux不自动启动background server,需要我们用ollama server去手动拉起。

而第二个Unix下对于background server启动的配置,可以看到代码调用了setpgid这个系统调用,意思是把它从当前进程组摘取出来,自己作为一个新进程组的组长。这样在原进程组结束的时候自己也不会被波及而退出了。

与client的串联

看着上面的start.go和background.go,可能有点摸不着头脑,尤其,我们看的还是linux相关的代码,实际上却什么都没干,这更让人迷惑了,因此这里有必要把上面的知识都串联起来,从ollama run ollama3这句命令的执行去看上面这些文件的作用:

执行后,cobra 分发到 RunHandler(上面看的cmd.go),它的 PreRunE: checkServerHeartbeat 先 ping 一下 server 在不在。
不在 → 调 startApp(第一组):

Mac/Win:唤起 GUI app 自动起 server,然后用 waitForServer(start.go 里那个,第一组的辅助)轮询等它 ready。
Linux:startApp 直接返回 error,流程中断,提示你 ollama serve。

而无论谁去真正启动 server 进程,执行 exec 时都会用 backgroundServerSysProcAttr()(第二组)给那个进程套上”后台独立存活”的系统属性。

model

整合层级:

aoverb@BA:~/mygo/ollama$ grep -rn "exec.Command\|exec.Cmd\|os/exec" llm/
llm/server.go:17:       "os/exec"
llm/server.go:89:       cmd       *exec.Cmd
llm/server.go:334:func StartRunner(ollamaEngine bool, modelPath string, gpuLibs []string, out io.Writer, extraEnvs map[string]string) (cmd *exec.Cmd, port int, err error) {
llm/server.go:383:      cmd = exec.Command(exe, params...)
llm/server.go:388:              // os/exec serializes Write calls when shared
llm/status.go:14:       // os/exec serializes Write calls in that case. 

从这里可以判断ollama是将后端的推理引擎(前面介绍的llama.cpp也好,ollama自己的go engine也好)当作子进程去启动,然后通过端口来进行通信。

遗留问题

今天对ollama的代码是匆匆一瞥,还遗留了一些问题,比如,ollama与它的model子进程通信的细节,模型拉起和并发请求对于模型进程的调度策略,或者更深层的go engine本身的代码,都还需要我们进一步探索…