目录
创建“CodeProject.AI测试”页(和资源管理器 UI)
完整的代码可以在我们的GitHub存储库中找到,网址为 CodeProject.AI-Server/src/modules/LlamaChat的main · codeproject/CodeProject.AI-Server (github.com)
介绍
本文将向您展示如何在桌面上本地运行类似ChatGPT的大型语言模型(LLM)。你这样做的原因有很多:确保你的敏感数据留在你的网络内,避免高深莫测的托管费用,在你没有互联网的情况下可以访问LLM。我们的理由是:因为他们很酷,我们想和他们一起玩。
我们将使用CodeProject.AI Server来处理所有烦人的设置、部署和生命周期管理,以便我们可以专注于代码。而且确实没有那么多代码,因为魔法被捆绑在令人痛苦的大型模型中。一个感兴趣的点(除了LLM本身相当重要的点)是处理Server中长时间运行CodeProject.AI推理调用。
传统上,CodeProject.AI Server的模块运行假设任何推理(针对AI模型运行数据)都足够快,以便在对服务器进行典型的HTTP API调用的超时内返回结果。对于LLM来说,情况并非如此。发送提示,模型咀嚼一下,然后逐个返回继续或聊天响应。这可能需要一段时间,因此我们将在CodeProject.AI Server中引入长时间运行的进程。
开始
我们假设你已阅读 CodeProject.AI模块创建:Python完整演练。我们将以完全相同的方式创建一个模块,但我们将展示如何处理长时间运行的进程。
我们还假定您已按照安装开发环境的指导设置CodeProject.AI开发环境。
我们将使用的库是Llama-cpp,用python(llama-cpp-python)包装,模型将是Mistral 7B Instruct v0.2 - GGUF,这是一个73亿参数的模型,具有32K上下文窗口和令人印象深刻的桌面级硬件功能。该模型专为Meta AI于2023年发布的LLM Llama设计。Llama-cpp-python是Llama模型的C++接口的Python包装器。这一切都运行得非常顺利,这证明了Mistral 7b模型和Georgi Gerganov在llama-cpp上的工作。
如果您正在寻找其他兼容Llama的模型进行实验,那么我建议您尝试Hugging Face,尤其是由TheBloke(Tom Jobbins)量化的模型。由于几乎每天都有新的和改进的模型发布,您可以使用LLM排行榜来选择适合您的用例和硬件的LLM。
编写任何CodeProject.AI服务器模块都是一个简单的过程。我们的SDK可以完成大部分繁重的工作,让您只需担心为您的用例实现功能。在我们的模块创建冒险中将采取的步骤是:
- 在CodeProject.AI Server解决方案中创建项目。
- 创建一个modulesettings.json文件来定义有关运行模块并与之通信所需的模块的元数据。
- 编写模块的Python代码。
- 编写一个包装llma-cpp-python包的Python文件,以公开所需的功能,通常作为Class。
- 编写一个Python文件,该文件实现将CodeProject.AI Server连接到上述包装器的适配器。这就是奇迹发生的地方。
- 创建模块安装所需的文件
- 创建安装脚本
- 创建PIP requirements.txt文件。
- 创建CodeProject.AI仪表板的UI。
如何创建打包脚本以便将模块部署到CodeProject.AI模块存储库中详细介绍了如何将自己的 Python模块添加到CodeProject.AI 中,本文将不对此进行讨论。
本文也没有讨论该模块的测试,但可以在将您自己的Python模块添加到CodeProject.AI中找到有关此的建议。
创建项目
CodeProject.AI Server已在当前代码库中包含此模块,但我们将逐步完成此模块的创建过程,就好像它不存在一样。为此,我们首先需要在CodeProject.AI Server解决方案的/src/modules中创建一个文件夹“Llama”。
创建modulesettings.json文件
同样,请确保已查看Python和ModuleSettings文件中的完整演练。我们的modulesettings文件非常基本,有趣的部分是:
- 我们的适配器将用于启动模块,llama_chat_adapter.py
- 我们将在python3.8下运行
- 我们将设置一些环境变量来指定包含模型的文件夹的位置以及模型名称
- 我们将定义一个路由“text/chat”,它接受一个命令“prompt”,该命令接受字符串“prompt”并返回字符串“reply”
{
"Modules": {
"LlamaChat": {
"Name": "LlamaChat",
"Version": "1.0.0",
"PublishingInfo" : {
...
},
"LaunchSettings": {
"FilePath": "llama_chat_adapter.py",
"Runtime": "python3.8",
},
"EnvironmentVariables": {
"CPAI_MODULE_LLAMA_MODEL_DIR": "./models",
"CPAI_MODULE_LLAMA_MODEL_FILENAME": "mistral-7b-instruct-v0.2.Q4_K_M.gguf"
// fallback to loading pretrained
"CPAI_MODULE_LLAMA_MODEL_REPO": "@TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"CPAI_MODULE_LLAMA_MODEL_FILEGLOB": "*.Q4_K_M.gguf",
},
"GpuOptions" : {
...
},
"InstallOptions" : {
...
},
"RouteMaps": [
{
"Name": "LlamaChat",
"Route": "text/chat",
"Method": "POST",
"Command": "prompt",
"MeshEnabled": false,
"Description": "Uses the Llama LLM to answer simple wiki-based questions.",
"Inputs": [
{
"Name": "prompt",
"Type": "Text",
"Description": "The prompt to send to the LLM"
}
],
"Outputs": [
{
"Name": "success",
"Type": "Boolean",
"Description": "True if successful."
},
{
"Name": "reply",
"Type": "Text",
"Description": "The reply from the model."
},
...
]
}
]
}
}
}
编写模块代码
为CodeProject.AI Server创建模块的全部意义在于获取现有代码,对其进行包装,并允许API服务器将其公开给所有人。这是通过两个文件完成的,一个用于包装包或示例代码,另一个适配器用于连接此包装器CodeProject.AI服务器。
包装llama-cpp-python包
我们将要包装的代码将位于llama_chat.py中。这个简单的Python模块有两种方法:__init__构造函数,用于创建Llama对象,以及do_chat接受提示并返回文本。从do_chat返回的文本要么是一个CreateChatCompletionResponse对象,要么是一个Iterator[CreateChatCompletionStreamResponse]对象。该**kwargs参数允许将任意附加参数传递给LLM create_chat_completion函数。请参阅 llama-cpp-python 文档,详细了解哪些参数可用以及它们的作用。
# This model uses the llama_cpp_python library to interact with the LLM.
# See https://llama-cpp_python.readthedocs.io/en/latest/ for more information.
import os
from typing import Iterator, Union
from llama_cpp import ChatCompletionRequestSystemMessage, \
ChatCompletionRequestUserMessage, \
CreateCompletionResponse, \
CreateCompletionStreamResponse, \
CreateChatCompletionResponse, \
CreateChatCompletionStreamResponse, \
Llama
class LlamaChat:
def __init__(self, repo_id: str, fileglob:str, filename:str, model_dir:str, n_ctx: int = 0,
verbose: bool = True) -> None:
try:
# This will use the model we've already downloaded and cached locally
self.model_path = os.path.join(model_dir, filename)
self.llm = Llama(model_path=self.model_path,
n_ctx=n_ctx,
n_gpu_layers=-1,
verbose=verbose)
except:
try:
# This will download the model from the repo and cache it locally
# Handy if we didn't download during install
self.model_path = os.path.join(model_dir, fileglob)
self.llm = Llama.from_pretrained(repo_id=repo_id,
filename=fileglob,
n_ctx=n_ctx,
n_gpu_layers=-1,
verbose=verbose,
cache_dir=model_dir,
chat_format="llama-2")
except:
self.llm = None
self.model_path = None
def do_chat(self, prompt: str, system_prompt: str=None, **kwargs) -> \
Union[CreateChatCompletionResponse, Iterator[CreateChatCompletionStreamResponse]]:
"""
Generates a response from a chat / conversation prompt
params:
prompt:str The prompt to generate text from.
system_prompt: str=None The description of the assistant
max_tokens: int = 128 The maximum number of tokens to generate.
temperature: float = 0.8 The temperature to use for sampling.
grammar: Optional[LlamaGrammar] = None
functions: Optional[List[ChatCompletionFunction]] = None,
function_call: Optional[Union[str, ChatCompletionFunctionCall]] = None,
stream: bool = False Whether to stream the results.
stop: [Union[str, List[str]]] = [] A list of strings to stop generation when encountered.
"""
if not system_prompt:
system_prompt = "You're a helpful assistant who answers questions the user asks of you concisely and accurately."
completion = self.llm.create_chat_completion(
messages=[
ChatCompletionRequestSystemMessage(role="system", content=system_prompt),
ChatCompletionRequestUserMessage(role="user", content=prompt),
],
**kwargs) if self.llm else None
return completion
正如你所看到的,实现这个文件不需要很多代码。
创建适配器
适配器是从类派生的ModuleRunner类。ModuleRunner完成所有繁重的工作:
- 从服务器检索命令
- 在派生类上调用相应的重载函数
- 将响应返回给服务器
- 日志
- 定期向服务器发送模块状态更新。
我们的适配器位于模块llama_chat_adapter.py中,包含 Python完整演练中讨论的重写方法。可以在GitHub存储库的源代码中完整查看该文件。
重要提示:来自调用llm.create_chat_completion(即调用LLM)的响应可以是单个响应,也可以是流式响应。两者都需要时间,但我们会选择将响应作为流返回,从而允许我们以增量方式构建回复。我们将通过CodeProject.AI Server中的长进程机制来执行此操作。这意味着我们将使用llama_chat.py中的代码向LLM发出请求,并遍历累积LLM生成的回复的返回值。要在向CodeProject.AI Server发出初始请求后显示累积回复,客户端可以轮询命令状态。
我们将讨论文件的每个部分,解释每个部分的作用。可以在GitHub存储库中查看完整的文件。
序言
前导码设置了SDK的包搜索路径,包含文件所需的导入,并定义了LlamaChat_adapter类。
# Import the CodeProject.AI SDK. This will add to the PATH for future imports
sys.path.append("../../SDK/Python")
from common import JSON
from request_data import RequestData
from module_runner import ModuleRunner
from module_options import ModuleOptions
from module_logging import LogMethod, LogVerbosity
# Import the method of the module we're wrapping
from llama_chat import LlamaChat
class LlamaChat_adapter(ModuleRunner):
initialis()
该initialise()函数从基ModuleRunner类重载,并在启动时初始化适配器。在本模块中,它:
- 读取定义用于处理提示的LLM模型的环境变量。
- 使用指定的模型创建LlamaChat类的实例。
def initialise(self) -> None:
self.models_dir = ModuleOptions.getEnvVariable("CPAI_MODULE_LLAMA_MODEL_DIR", "./models")
# For using llama-cpp.from_pretrained
self.model_repo = ModuleOptions.getEnvVariable("CPAI_MODULE_LLAMA_MODEL_REPO", "TheBloke/Llama-2-7B-Chat-GGUF")
self.models_fileglob = ModuleOptions.getEnvVariable("CPAI_MODULE_LLAMA_MODEL_FILEGLOB", "*.Q4_K_M.gguf")
# fallback loading via Llama()
self.model_filename = ModuleOptions.getEnvVariable("CPAI_MODULE_LLAMA_MODEL_FILENAME", "mistral-7b-instruct-v0.2.Q4_K_M.gguf")
verbose = self.log_verbosity != LogVerbosity.Quiet
self.llama_chat = LlamaChat(repo_id=self.model_repo,
fileglob=self.models_fileglob,
filename=self.model_filename,
model_dir=self.models_dir,
n_ctx=0,
verbose=verbose)
if self.llama_chat.model_path:
self.log(LogMethod.Info|LogMethod.Server, {
"message": f"Using model from '{self.llama_chat.model_path}'",
"loglevel": "information"
})
else:
self.log(LogMethod.Error|LogMethod.Server, {
"message": f"Unable to load Llama model",
"loglevel": "error"
})
self.reply_text = ""
self.cancelled = False
process()
当收到不是常见模块命令之一的命令时,将调用该process()函数。它将执行以下两项操作之一:
- 处理请求并返回响应。这是用于短期处理(如对象检测)的模式。
- 返回将在后台执行的Callable,以处理请求的命令并创建响应。这就是我们将要运行的模式。
对于这个模块,我们只返回LlamaChat_adapter.long_process函数来表示这是一个长时间运行的过程。此名称是常规名称。
def process(self, data: RequestData) -> JSON:
return self.long_process
有趣的说明:当你从process返回一个Callable时,向CodeProject.AI Server发出请求的客户端实际上不会获得Callable作为响应。那将是奇怪和无益的。ModuleRunner将注意到正在返回Callable,并将当前请求的命令ID和模块ID传回客户端,然后客户端可以使用这些ID进行与此请求相关的状态调用。
long_process()
这是使用 llama_chat.py 文件的功能实际完成工作的地方。该long_process方法调用Llama_chat.py代码并传递stream=True给do_chat。这会导致返回响应的迭代器,每个响应都在循环中处理并添加到我们的最终结果中。在每次迭代中,我们都会检查是否被要求取消操作。取消信号位于在cancel_command_task方法中切换的self.cancelled类变量中(如下所述)。
客户端可以通过向服务器发送此模块的get_command_status命令来轮询累积结果,并显示响应的reply属性。(如下所述)。
def long_process(self, data: RequestData) -> JSON:
self.reply_text = ""
stop_reason = None
prompt: str = data.get_value("prompt")
max_tokens: int = data.get_int("max_tokens", 0) #0 means model default
temperature: float = data.get_float("temperature", 0.4)
try:
start_time = time.perf_counter()
completion = self.llama_chat.do_chat(prompt=prompt, max_tokens=max_tokens,
temperature=temperature, stream=True)
if completion:
try:
for output in completion:
if self.cancelled:
self.cancelled = False
stop_reason = "cancelled"
break
# Using the raw result from the llama_chat module. In
# building modules we don't try adn rewrite the code we
# are wrapping. Rather, we wrap the code so we can take
# advantage of updates to the original code more easily
# rather than having to re-apply fixes.
delta = output["choices"][0]["delta"]
if "content" in delta:
self.reply_text += delta["content"]
except StopIteration:
pass
inferenceMs : int = int((time.perf_counter() - start_time) * 1000)
if stop_reason is None:
stop_reason = "completed"
response = {
"success": True,
"reply": self.reply_text,
"stop_reason": stop_reason,
"processMs" : inferenceMs,
"inferenceMs" : inferenceMs
}
except Exception as ex:
self.report_error(ex, __file__)
response = { "success": False, "error": "Unable to generate text" }
return response
command_status()
我们有一个long_process方法,它在从process返回时被调用,但我们需要的是一种查看这个漫长过程结果的方法。请记住,我们正在将聊天完成的结果累积回self.reply_text变量中,因此在我们的command_status()函数中,我们将返回到目前为止收集到的内容。
调用command_status()是发送原始聊天命令的客户端应用在发送命令后应执行的操作。调用是通过 /v1/LlamaChat/get_command_status 端点进行的,这将导致服务器向模块发送消息,该消息将反过来导致调用command_status()
并将结果返回给客户端。
def command_status(self) -> JSON:
return {
"success": True,
"reply": self.reply_text
}
然后,客户端应该(或可以)显示“回复”,每个后续调用(希望)都会显示更多来自LLM的响应。
cancel_command_task()
当服务器从服务器接收到cancel_command命令时,调用cancel_command_task()。每当服务器收到 v1/LlamaChat/cancel_command 请求时,都会发生这种情况。此函数设置一个标志,告诉长进程终止。它还设置self.force_shutdown为False以告诉ModuleRunner基类,此模块将正常终止长进程,并且不需要强制终止后台任务。
def cancel_command_task(self):
self.cancelled = True # We will cancel this long process ourselves
self.force_shutdown = False # Tell ModuleRunner not to go ballistic
main
最后,如果此文件是从Python命令行执行的,我们需要为LlamaChat_adapter启动asyncio循环。
if __name__ == "__main__":
LlamaChat_adapter().start_loop()
这就是实现该模块所需的所有Python代码。安装过程需要一些标准文件,将在下一节中讨论。
编写安装和设置
安装和设置模块的过程需要几个文件。这些用于构建执行环境和运行模块。本节将介绍这些文件。
创建安装脚本
该模块需要两个安装脚本,install.bat个用于Windows的脚本,install.sh个用于Linux和MacOS。对于此模块,所有这些文件所做的只是下载LLM模型文件作为安装过程的一部分,以确保该模块可以在没有连接到Internet的情况下运行。您可以在GitHub存储库的源代码中查看这些文件的内容。
有关创建这些文件的详细信息,请参阅 CodeProject.AI模块创建:Python完整演练和为CodeProject.AI Server编写安装脚本。
创建requirements.txt文件
模块安装过程使用requirements.txt文件来安装模块所需的Python包。如果不同的操作系统、体系结构和硬件需要不同的软件包或软件包版本,则此文件可能会有变体。有关变体的详细信息,请参阅 Python requirements.txt文件和源代码。对于此模块,主requirements.txt文件为:
#! Python3.7
huggingface_hub # Installing the huggingface hub
diskcache>=5.6.1 # Installing disckcache for Disk and file backed persistent cache
numpy>=1.20.0 # Installing NumPy, a package for scientific computing
# --extra-index-url https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/basic/cpu
# --extra-index-url https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/AVX/cpu
# --extra-index-url https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/AVX512/cpu
--extra-index-url=https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/AVX2/cpu
--prefer-binary
llama-cpp-python # Installing simple Python bindings for the llama.cpp library
# last line empty
创建“CodeProject.AI测试”页(和资源管理器 UI)
CodeProject.AI资源管理器中显示的UI是在explore.html文件中定义的。下面是存储库中内容的精简版本,可让您查看重要部分。
单击_MID_queryBtn时,_MID_onLlamaChat将调用该提示,该提示将接收用户提供的提示并将其发布到/v1/text/chat 端点。该调用返回的数据包括“谢谢,我们现在开始了一个漫长的过程”消息,以及发送请求的命令和模块的ID。
然后,我们立即启动一个循环,该循环将每250毫秒轮询一次模块状态。为此,我们调用/v1/llama_chat/get_command_status,传递我们从呼叫process收到的命令ID和模块ID。对于每个响应,对于每个响应,我们显示results.reply。
这样做的结果是你输入一个提示,点击发送,几秒钟之内,响应开始在结果框中累积。纯粹的魔法。
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
...
</head>
<body class="dark-mode">
<form method="post" action="" enctype="multipart/form-data" id="myform">
<div class="form-group">
<label class="form-label text-end">How can I help you?</label>
<div class="input-group mt-1">
<textarea id="_MID_promptText"></textarea>
<input id="_MID_queryBtn" type="button" value="Send"
onclick="_MID_onLlamaChat(_MID_promptText.value, _MID_maxTokens.value, _MID_temperature.value)">
<input type="button" value="Stop" id="_MID_stopBtn"onclick="_MID_onLlamaStop()" />
</div>
</div>
<div class="mt-2">
<div id="_MID_answerText"></div>
</div>
<div class="mt-3">
<div id="results" name="results" </div>
</div>
</form>
<script type="text/javascript">
let chat = '';
let commandId = '';
async function _MID_onLlamaChat(prompt, maxTokens, temperature) {
if (!prompt) {
alert("No text was provided for Llama chat");
return;
}
let params = [
['prompt', prompt],
['max_tokens', maxTokens],
['temperature', temperature]
];
setResultsHtml("Sending prompt...");
let data = await submitRequest('text', 'chat', null, params)
if (data) {
_MID_answerText.innerHTML = "<div class='text-muted'>Answer will appear here...</div>";
// get the commandId to so we can poll for the results
commandId = data.commandId;
moduleId = data.moduleId;
params = [['commandId', commandId], ['moduleId', moduleId]];
let done = false;
while (!done) {
await delay(250);
let results = await submitRequest('LlamaChat', 'get_command_status', null, params);
if (results) {
if (results.success) {
done = results.commandStatus == "completed";
let html = "<b>You</b>: " + prompt + "<br><br><b>Assistant</b>: "
+ results.reply.replace(/[\u00A0-\u9999<>\&]/g, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
}
_MID_answerText.innerHTML = html
}
}
else {
done = true;
}
}
}
}
}
async function _MID_onLlamaStop() {
let params = [['commandId', commandId], ['moduleId', 'LlamaChat']];
let result = await submitRequest('LlamaChat', 'cancel_command', null, params);
}
</script>
</div>
</body>
</html>
结论
我们已经证明,通过包装现有示例或库代码并创建适配器,可以很容易地创建一个执行复杂且长时间运行的进程的模块。
您现在面临的挑战是创建或修改模块以支持您的特定需求。如果您这样做,我们鼓励您分享它。CodeProject.AI Server致力于帮助建立一个AI社区,如果您能成为其中的一员,我们将不胜荣幸。
https://www.codeproject.com/Articles/5380081/Creating-a-LLM-Chat-Module-for-CodeProject-AI-Serv