深入讲解 Ollama 的源码

发布于:2025-06-14 ⋅ 阅读:(21) ⋅ 点赞:(0)

Ollama 是一个非常受欢迎的项目,它极大地简化了在本地运行大型语言模型(LLM)的流程。它的源码设计巧妙,将复杂的底层实现封装在了一个简单易用的工具中。

下面我将从几个层面为你深入讲解 Ollama 的源码。

1. 核心理念与架构

理解 Ollama 的最好方式是将它类比为 Docker

  • Docker:管理和运行容器镜像

    • Dockerfile -> 定义如何构建镜像。

    • docker pull ubuntu -> 从 Hub 拉取镜像。

    • docker run ubuntu -> 运行一个容器实例。

    • dockerd (daemon) -> 后台服务,管理镜像和容器。

  • Ollama:管理和运行大语言模型

    • Modelfile -> 定义如何构建一个模型(例如,设置系统提示、温度等参数)。

    • ollama pull llama3 -> 从 Hub 拉取模型。

    • ollama run llama3 -> 运行一个模型实例并进行交互。

    • Ollama Server (daemon) -> 后台服务,管理模型下载、加载和推理。

核心架构分为三层:

  1. 命令行/客户端 (CLI / Client):用户与之交互的界面 (ollama 命令)。它是一个 Go 程序,负责解析用户命令,并向后台的 Ollama Server 发送 REST API 请求。

  2. 服务端 (Server / Daemon):项目的核心,也是一个 Go 程序。它作为后台守护进程运行,负责:

    • 监听 API 请求(例如,生成文本、拉取模型)。

    • 管理模型的下载、存储和版本。

    • 加载模型到内存中。

    • 调用底层的 C++ 推理引擎来执行计算。

  3. 推理引擎 (Inference Engine):真正执行 LLM 计算的部分。Ollama 内嵌了一个高度优化的 llama.cpp 版本。这部分代码是 C++ 写的,通过 CGo 与上层的 Go 服务端进行交互。它负责模型的实际运算,并利用 CPU 或 GPU (CUDA/Metal) 进行加速。

2. 技术栈

  • 主要语言Go。用于构建 CLI、Server 和 API。Go 的并发能力和静态编译特性非常适合构建这种服务端应用。

  • 核心计算C++。直接使用了 llama.cpp 的源码,并做了一些修改以适应 Ollama 的需求。这是性能的关键。

  • GPU 加速CUDA (NVIDIA), Metal (Apple), ROCm (AMD)。这些是 llama.cpp 自带的,Ollama 负责在编译和运行时正确地检测和链接它们。

  • API 框架Gin。一个轻量级的 Go Web 框架,用于构建 REST API。

  • CLI 框架Cobra。一个强大的 Go 库,用于创建现代化的 CLI 应用。

3. 源码目录结构讲解

现在我们来深入看 github.com/ollama/ollama 的目录结构,这是理解代码的关键。

.
├── api/              # 定义了所有 REST API 的数据结构 (Go structs)
├── app/              # 桌面应用相关(如系统托盘图标)
├── cmd/              # `ollama` 命令的入口和实现
├── llm/              # 【核心】C++ 推理引擎 (llama.cpp) 和 Go 的绑定
├── server/           # 【核心】Ollama 后台服务端的实现
├── gpu/              # GPU 相关信息的检测和处理
├── parser/           # `Modelfile` 的解析器
├── format/           # GGUF 等模型格式的处理
├── scripts/          # 构建、安装、打包脚本
├── docs/             # 项目文档
└── examples/         # API 使用示例

content_copydownload

Use code with caution.

llm/ - 推理引擎核心

这是最底层、最关键的部分。

  • llm/ 目录下的大量 *.cpp, *.h, *.cu, *.m 文件: 这些是直接从 llama.cpp 项目中引入的源码。ggml.c 是核心张量库,ggml-alloc.c 负责内存管理,ggml-cuda.cu 和 ggml-metal.m 分别是 NVIDIA 和 Apple GPU 的后端实现。

  • llama_binding.cpp 和 llama_binding.h: 这是 Go 和 C++ 之间的桥梁 (CGo)。server/ 中的 Go 代码通过 CGo 调用这里定义的 C++ 函数,如 llama_predict、llama_load_model 等。反过来,C++ 代码也可以通过回调函数将生成的 token 等数据传回给 Go。

  • CMakeLists.txt: 这是 C++ 部分的构建脚本。在编译 Ollama 时,会先用 CMake 构建 llm 目录,生成一个静态库或动态库,然后 Go 程序在编译时链接这个库。

server/ - 后台服务

这是项目的“大脑”,负责所有的逻辑调度。

  • server.go: 服务的入口。它初始化 Gin 路由器、加载模型、启动 HTTP 服务器。

  • routes.go: 定义所有的 API 路由。例如,/api/generate 路由到 GenerateHandler 函数,/api/pull 路由到 PullModelHandler 函数。

  • model.go: 定义了 Model 结构体,封装了模型的元数据(名字、路径、参数等)。

  • generate.go: 实现了 /api/generate 的处理逻辑。它接收请求,找到或加载指定的模型,然后调用 llm/ 里的 CGo 函数来执行推理,并通过 HTTP Chunked Encoding 将生成的 token 流式返回给客户端。

  • pull.go, create.go: 分别实现了拉取模型 (ollama pull) 和根据 Modelfile 创建模型 (ollama create) 的逻辑。它们会与 Ollama Hub (registry.ollama.ai) 进行交互,下载模型的 manifest 和 blobs (层)。

cmd/ - 命令行工具

这是用户直接接触的部分。

  • cmd/main.go: 程序的入口点。它使用 Cobra 库设置根命令 ollama。

  • cmd/run.go, cmd/pull.go, cmd/serve.go: 分别实现了 ollama run, ollama pull, ollama serve 等子命令。这些命令的实现逻辑非常简单:解析命令行参数,然后构造一个对本地 Ollama Server 的 API 请求。例如,ollama run llama3 会向 http://127.0.0.1:11434/api/generate 发送一个 POST 请求。

gpu/ - GPU 检测
  • gpu.go: 定义了 GPU 信息的接口。

  • gpu_info_cuda.go, gpu_info_rocm.go: 使用 CGo 调用 NVIDIA 的 NVML 库或 AMD 的 RVSMI 库来获取 GPU 的信息(如显存、驱动版本等)。这部分代码用于在启动时检测硬件,并决定推理时使用哪个后端。

4. 一个典型流程:ollama run llama3 "你好"

让我们把所有部分串起来,看看这个命令是如何执行的:

  1. 用户输入: 在终端执行 ollama run llama3 "你好"。

  2. CLI 解析 (cmd/): ollama 程序启动,Cobra 库识别出是 run 命令。cmd/run.go 中的代码被执行。

  3. API 请求: CLI 客户端向本地运行的 Ollama Server (http://127.0.0.1:11434) 发送一个 /api/generate 的 POST 请求。请求体 (body) 中包含 { "model": "llama3", "prompt": "你好" }。

  4. Server 接收 (server/): server/routes.go 中的路由将请求交给 GenerateHandler (server/generate.go)。

  5. 模型加载:

    • GenerateHandler 检查 llama3 模型是否已经加载到内存中。

    • 如果没有,它会检查模型文件是否存在于磁盘上(通常在 ~/.ollama/models)。

    • 如果磁盘上也没有,它会触发一次 pull 操作,从 Ollama Hub 下载模型。

    • 找到模型文件后,它会调用一个函数(最终会通过 CGo 调用到 llm/ 目录中)来将模型文件加载到 RAM 或 VRAM 中。

  6. 调用 C++ 推理 (llm/):

    • Go 代码通过 CGo 调用 llm/llama_binding.cpp 中封装的 llama_predict 之类的函数,并将 prompt "你好" 传递过去。

    • C++ 代码接收到 prompt,利用 llama.cpp 的核心逻辑进行计算。它会根据检测到的硬件自动选择使用 CPU 或 GPU。

  7. 流式返回:

    • llama.cpp 每生成一个 token(例如,“你”、“好”、“,”、“世”、“界”)就会通过一个注册的回调函数将这个 token 从 C++ 层传回给 Go 层

    • Go Server (server/generate.go) 收到这个 token 后,立即通过 HTTP 流(Chunked Transfer Encoding)将其发送给 CLI 客户端。

  8. CLI 显示: CLI 客户端 (cmd/run.go) 接收到 HTTP 响应流,每收到一个 token 就立即打印到用户的终端上,实现了打字机效果。

  9. 结束: 当模型生成结束符 (EOS token) 或达到最大长度时,C++ 代码通知 Go 层,Go Server 关闭 HTTP 连接,流程结束。

总结

Ollama 的源码是一个分层清晰、职责明确的优秀范例。

  • 它用 Go 语言处理所有高层逻辑:API 服务、模型管理、用户交互,充分利用了 Go 在网络编程和并发方面的优势。

  • 它将性能敏感的计算密集型任务完全委托给 C++ (llama.cpp),并巧妙地通过 CGo 作为胶水层,这是整个项目高性能的关键。

  • 类 Docker 的设计思想 (Modelfile, pull, run) 极大地降低了用户的使用门槛,是其大获成功的核心原因之一。

如果你想深入研究,建议从 cmd/run.go 和 server/generate.go 开始,顺着函数调用链一路往下,直到 llm/llama_binding.cpp,这样你就能清晰地理解整个请求的生命周期。