GitHub Copilot 如何每天处理 4 亿次补全请求
GitHub Copilot 是全球最大的基于大语言模型的代码补全服务。每天处理数亿次请求,平均响应时间不到 200ms。本文解析到底如何构建这项服务的。
1 GitHub Copilot是啥?
GitHub Copilot(后文简称GC)代码补全、聊天、交互式重构等功能,大致使用相同架构和基础设施。可作为扩展程序安装在IDE,包括VS Code、Visual Studio、IntelliJ IDE 系列、Neovim,最近还宣布 Xcode 支持。
欧洲下午和美国工作时间高峰期,峰值约 8000 QPS。这时期,平均响应时间不到 200ms。内置的 IDE 补全功能显示在方框内,而 Copilot 的补全建议以灰色显示,通常称为“幽灵文本”,因为它看起来灰色且虚幻。每当停止输入或稍作停顿,Copilot 就会接管。可编写一个函数,添加一个描述你想要实现的功能的注释,Copilot 会尽力帮你完成。特别擅长识别模式。
2 构建云托管的自动补全服务
咋构建后端服务以实现 IDE 中的交互式代码补全?与大多 IDE 内置交互式自动补全功能竞争,如基于 LSP 的功能、Code Sense、IntelliSense 等。这是一个高要求,因为这些在本地运行的功能无需考虑网络延迟。它们无需处理:
- 共享服务器资源
- 应对不可避免的云服务中断
因此面临很高标准。为具竞争力,需做到:
尽量减少请求之间的延迟
由于这是一个网络服务,需尽量分摊设置成本,这是网络服务的特性。一定程度,需尽量避免网络延迟,因为本地 IDE 竞争对手无需承担这部分开销。最后一点是,代码补全响应的长度和生成时间与请求的大小密切相关,而请求的大小是完全可变的。不会等整个请求完成后再将其发送回用户,而是流式处理。无论请求多长,都在开始时立即开始流式传输。该特性也解锁其他优化。
连接设置成本高昂
由于这是网络服务,GC使用TCP。TCP使用三次握手SYN、SYN-ACK、ACK。此外,因为这是互联网,如今所有内容都需通过 TLS 加密。TLS 需额外进行 5 到 7 次握手来协商双向密钥。这些步骤中有些可并行处理。为减少这些设置成本,做了很多工作,将 TLS 握手与 TCP 握手叠加。这些优化很好,但并非万能。无法将网络成本降低到零。因此,最终需在你和服务器之间进行五到六次往返才能建立新的服务连接。
每次往返的持续时间与距离高度相关。每次往返大约需要 50 毫秒。当你把所有这些加起来,进行五到六次往返,就会使连接设置变得非常昂贵。你希望只做一次连接,并尽可能长时间地保持它。
3 GitHub Copilot演变
一开始在 VS Code 中有个扩展程序。早期用户需 OpenAI 注册账号,将他们密钥添加到一个特殊组,然后在 IDE 中插入该密钥。这对一个早期产品效果很好。可扩展到几十名用户。但当涉及用户管理,每个人都厌倦了。OpenAI 不想涉足了解GC用户身份的业务,GC方也不想。GC想要的是服务提供商关系。你获得一个密钥来访问服务器资源。每当有人使用该密钥时,就产生账单。谁被允许在什么情况下使用它,这是产品团队的工作。GC面临问题是,如何管理这个服务密钥?先看
3.1 错误做法
在GC提供给用户的扩展程序中以某种方式对密钥进行编码,以便它可被服务提取和使用,但对偶然的或恶意的旁观者不可见。这不可能。这充其量只是通过隐藏来实现安全。它不起作用,GC找到的解决方案是构建一个认证代理,它位于这个网络事务的中间。产品名 copilot-proxy(内部名称)。后续把它称为代理。在发布 alpha 版本后不久就被添加进来,使GC从用户提供的密钥阶段过渡到更可扩展的认证机制。
3.2 现在的工作流程
像往常一样在 IDE 中安装扩展程序,并像往常一样对 GitHub 进行身份验证。这会创建一种 OAuth 关系,即每个用户在特定机器上登录时,都有一个 OAuth 密钥与之对应。IDE 现可用这个 OAuth 凭据去 GitHub 交换短期代码补全令牌。这令牌像一张火车票,它只是一个在短时间内使用的授权,经过签名。当请求到达代理时,只需检查签名是否有效,有效则用实际 API 服务密钥替换它,将请求转发过去,并将结果流式传输回来。无需进行任何其他进一步验证。这很重要,因为这意味着对每个请求,无需再调用外部认证服务。短期令牌就是认证。
3.3 客户端角度
他们仍认为自己在与模型对话,并像往常一样获得响应。令牌有效期约几min,10、20 或 30 分钟。这主要是为了限制责任,以防被盗。被滥用时,能关闭一个账户,因此不再生成新令牌。这就是令牌具有短寿命主要原因。在后台,客户端知道它获得的令牌的到期时间,并在到期前几分钟启动刷新周期,获取新令牌,然后替换掉旧的,继续运转。
4 Copilot何时接管?
解决了访问,就解决了一半问题。产品设计角度,GC没有自动补全键。Eclipse 中,你用命令 + 空格之类快捷键触发自动补全或重构工具。每当停止输入时,Copilot 就会接管。这就提出一个问题,它应该何时接管?何时应该从用户输入切换到 Copilot 输入?这不是简单问题。
可能采用的一些解决方案是
设置一个固定定时器
捕捉键盘输入,每次按键后,启动一个定时器。如在没有新的按键输入的情况下定时器到期,说明准备好发出请求并进入补全模式。
这很好,因为它为我们的等待时间设定一个上限,而这种等待是额外延迟。缺点是,它设定了一个下限。总是至少要等待这么长,即使那是用户做的最后一次按键输入。
可尝试一些
更科学方法
使用一个小型预测模型查看输入的字符流,并预测他们是否接近单词的结尾或是否正处于单词的中间,从而相应调整定时器。还可采用盲猜。每次有按键输入时,就假设这是用户的最后一次输入,不再有来自用户的输入,然后总是发出补全请求。实际采用了所有这些策略的混合。
这又引出了下一个问题,尽管投入大量工作和调整,但发出的请求中约一半是“输入后无效”请求。别忘了正在进行的是自动补全。如你在GC发出请求后继续输入,你就偏离了GC所拥有的数据,GC的请求现在就过时了,不能使用那个结果。
可尝试一些方法来解决这问题。GC可等待更长时间再发出请求。这可能减少GC发出请求然后又不得不放弃结果的次数。但这种额外延迟,这种额外等待,会惩罚那些已停止输入并等待的用户。当然,如GC等待太久,用户可能会认为 Copilot 出问题,因为它不再向他们提供任何反馈。相反,GC构建了一个系统,允许GC发出请求后取消请求。
5 取消 HTTP 请求
在浏览器中,决定不再等待那页面加载,你会咋做?
- 按停止按钮
- 关闭浏览器标签
- 断开网络连接
- 把PC吃掉
都是果断行为。它们意味着你取消请求是因为你已完成操作。要么是你已不想用这网站,要么是你感到沮丧而放弃。这是一种终结行为,你并不打算再发出另一个请求。在底层,它们在网络行为一样。你会重置 TCP 流。
这是在浏览器端的情况,考虑服务器端,无论应用层还是你的网络框架,这种取消的概念在 Web 框架内部不常见。
若用户在使用你的应用程序时按下浏览器的停止按钮,或者他们使用 cURL 命令时按下 Ctrl-C,底层的行为会转化为 TCP 连接的重置。在另一端,也就是你的服务器代码中,你什么时候能看到这个信号?你什么时候能看到他们已经这样做了?一般来说,你可以在读取请求体时发现 TCP 连接已被重置,也就是在请求的早期,当你正在读取头部和正文时,或者稍后当你开始向回写响应时。
对LLM,这是大问题,因为请求的成本,即在你生成第一个token之前的初始推理,占据了大部分成本。这发生在你产生任何输出前。所有这些工作都已完成。GC已进行了推理,准备好开始流式传输标记。只有在那时,才发现用户关闭了socket并离开。大约 45% 请求都这样。一半时间,GC都在进行推理,然后把结果扔掉,造成巨大金钱、时间和能源浪费。
HTTP取消是通过关闭连接实现。GC用 TCP 网络与代理通信情况下,GC取消请求的原因是因为想立即发出另一个请求。为立即发出那个请求,GC已经没有连接了。必须支付五到六次往返的代价来建立一个新 TCP TLS 连接。在这种天真想法中,在这种正常用法中,平均每个请求都会发生一次取消。这意味着用户不断关闭并重新建立 TCP 连接,这种延迟远远超过仅让那个我们不需要的请求运行到完成,然后忽略它的成本。
6 HTTP/2 及其重要性
前面大部分内容适用HTTP/1,采用一个连接一个请求模型。如今HTTP 版本号高于 1,可达 2 和 3。copilot-proxy 是 Go 编写,因它有一个强大 HTTP 库。为GC提供了实现这个产品部分所需的 HTTP/2 支持和控制。
HTTP/2 更像 SSH,而不是老式的 HTTP/1 加上 TLS。像 SSH 一样,HTTP/2 是一个隧道连接。你有一个单一的连接,在其上可多路复用多个请求。在 SSH 和 HTTP/2 中,它们都被称为流。一个单一的网络连接可以承载多个流,每个流就是一个请求。在客户端和代理之间使用 HTTP/2,因为这允许我们建立一次连接并反复重用它。
本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
发布者:admin,转转请注明出处:http://www.yc00.com/web/1748103364a4731304.html
评论列表(0条)