从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大部分的方法都会调用do或stream函数,这两个函数都会去发送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本身的代码,都还需要我们进一步探索…

参与讨论
(Participate in the discussion)
参与讨论
没有发现评论
暂无评论