第3章:Ollama源码获取与结构分析

好,咱们直接进入正题。这一章我带你亲手把Ollama源码拉下来,然后拆开看看里面到底装了些什么东西。说实话,我第一次看Ollama源码时也有点懵——项目结构不算复杂,但几个核心模块之间的关系需要理清楚。

3.1 从GitHub拉取源码

拉代码这事儿,看似简单,但有几个坑我得提前跟你说。

注意:Ollama的仓库体积不小,尤其是带了llama.cpp的submodule。如果你网络不太好,建议用浅克隆。

我个人习惯这样操作:

# 标准克隆方式
git clone https://github.com/ollama/ollama.git
cd ollama

# 如果你只想看最新代码,不想下载全部历史
git clone --depth=1 https://github.com/ollama/ollama.git

# 别忘了初始化子模块!llama.cpp是作为submodule引入的
git submodule update --init --recursive

嗯,这里要注意——子模块一定要拉。我遇到过有人只拉了主仓库,结果编译时一堆头文件找不到,折腾了半天才发现是submodule没初始化。

拉完代码后,看看分支情况:

git branch -a

目前Ollama主要维护在main分支。我个人建议你切到最新的稳定tag,而不是直接跟main走——main分支有时候会有实验性改动,不一定稳定。

小技巧:可以用 git tag | tail -10 查看最近的版本号。我一般选最新的非rc版本。

3.2 目录结构解析

拉完代码,咱们来看看这个项目的骨架。用tree -L 2看一眼大概:

ollama/
├── api/              # API定义层
├── cmd/              # 命令行入口
├── docs/             # 文档
├── examples/         # 示例代码
├── llm/              # 大模型推理核心(关键!)
├── parser/           # 模型文件解析器
├── progress/         # 进度条相关
├── server/           # HTTP服务端
├── types/            # 类型定义
├── version/          # 版本信息
├── go.mod            # Go模块文件
├── go.sum            # Go依赖校验
└── Makefile          # 构建脚本

说实话,这个目录结构设计得挺清晰的。Go项目该有的都有,没有多余的垃圾。咱们重点看几个核心目录:

目录/文件 作用 我的评价
server/ HTTP API服务,处理用户请求 入口层,代码量不大
llm/ 模型加载、推理、卸载 核心中的核心
parser/ 解析GGUF等模型文件格式 底层支撑,容易被忽略
api/ 定义请求/响应结构体 协议层,跨语言调用用得上

你想想看,一个AI推理服务,本质上就三件事:接收请求 → 加载模型 → 推理返回。Ollama的目录结构就是按这个逻辑拆的。

3.3 核心模块功能说明

咱们一个一个拆开看。我会结合我在嵌入式移植中踩过的坑来讲。

3.3.1 server模块

server/目录下主要是一个HTTP服务,用的是Go标准库的net/http。它负责:

  • 接收用户的RESTful请求(比如/api/generate
  • 解析请求参数
  • 调用llm模块进行推理
  • 把结果返回给客户端

代码入口在server/routes.go,里面定义了所有路由。我刚开始看的时候觉得这层挺简单的,后来才发现——嵌入式移植时,server层反而是最容易出问题的地方。为什么?因为嵌入式设备的网络栈、内存限制、并发模型跟x86完全不同。

核心文件:
  • server/routes.go - 路由注册
  • server/routes_internal.go - 内部API
  • server/routes_test.go - 测试用例

3.3.2 llama.cpp模块

这个模块藏在llm/目录下,实际上是Ollama的推理引擎。它通过cgo调用C++写的llama.cpp库。

目录结构大致是:

llm/
├── gen_server/       # 生成服务的Go封装
├── llama.cpp/        # 子模块,真正的C++推理代码
├── llm.go            # Go层的接口定义
└── llm_test.go       # 测试

我记得第一次移植时,最头疼的就是llama.cpp/这个子模块。它本身是一个独立的C++项目,Ollama把它作为submodule嵌进来,然后通过cgo调用。这意味着:

  • 编译时需要交叉编译C++代码
  • 链接时需要处理各种依赖库
  • 运行时需要关注内存对齐、ABI兼容性
避坑指南:我曾经在ARM64平台上编译llama.cpp时,因为没设置正确的-march参数,导致推理速度慢得离谱。后来发现是编译器没启用NEON指令集优化。所以移植时一定要确认目标平台的SIMD支持情况。

3.3.3 model模块

model模块其实分散在几个地方:

  • types/model.go - 模型元数据结构体
  • parser/ - 模型文件解析器(GGUF格式)
  • llm/ - 模型加载和卸载逻辑

模型加载的流程大概是:

  1. 用户指定模型名称(比如llama3.2:1b
  2. server层解析请求,传给llm层
  3. llm层从本地缓存或远程拉取模型文件
  4. parser层解析GGUF文件头,获取模型参数
  5. llama.cpp加载模型权重到内存
  6. 返回就绪状态

嗯,这里有个关键点——GGUF格式。Ollama用的是GGUF(GPT-Generated Unified Format),这是llama.cpp社区推的一种模型文件格式。它把模型权重、分词器、超参数都打包在一个文件里,方便分发和加载。

个人经验:在嵌入式设备上,模型加载这一步最容易OOM(内存溢出)。我建议你在移植时,先搞清楚目标板子的可用内存,然后选择对应大小的模型。比如1B参数的模型大概需要2GB内存,3B的需要6GB左右。

3.4 核心架构图

说了这么多,咱们用一张图把整体架构串起来。这张图是我自己画的,把Ollama的核心模块和调用关系展示清楚:

Ollama核心架构图 用户/客户端 HTTP请求 server/ (HTTP API服务) 路由注册 · 请求解析 · 响应封装 调用推理接口 llm/ (推理核心) 模型加载 · 推理调度 · 内存管理 └─ llama.cpp (C++推理引擎) 解析模型文件 parser/ (GGUF解析) 类型定义 types/ (数据结构) 数据流向:用户 → server → llm → llama.cpp → 推理结果返回

这张图你看懂了吗?说白了,Ollama就是一个Go写的胶水层,把C++的推理引擎包装成HTTP服务。移植到嵌入式平台时,我们主要关注llm/llama.cpp/这两块——因为它们是真正的性能瓶颈所在。

3.5 本章小结

这一章我们干了三件事:

  • 拉代码:用git clone把Ollama源码搞到手,别忘了子模块
  • 看结构:server是入口,llm是核心,parser是底层支撑
  • 拆模块:server管通信,llama.cpp管推理,model管数据

我个人觉得,理解Ollama的架构是移植成功的一半。你想想看,如果你连代码怎么组织的都不知道,怎么改?怎么优化?

下一章咱们就开始动手编译了——我会带你配置交叉编译环境,把Ollama编译成ARM64的可执行文件。嗯,那才是真正考验功力的时候。


公众号:蓝海资料掘金营,微信deep3321