我師弟跑路了,留給我一臺特別好的裝置,一臺帶 NVIDIA GeForce RTX 3090 Ti 顯示卡的裝置。我在這臺裝置上可以輕鬆透過 DirectML 跑起來 Phi 模型。既然已經跑起來模型了,我就想著能否用這個模型給更多的應用賦能。其中我想到的第一步就是搭建一個 Http 服務,讓其他裝置可以呼叫到這臺好裝置,從而在這臺好裝置上跑模型,得到的結果透過 http 返回給到其他裝置
這樣一來,其他裝置也就能享受到 Phi 模型帶來的智慧化
在 ASP.NET Core 框架的幫助下,給 Phi 模型呼叫封裝一個服務是非常簡單的事情,我甚至在一個 Program.cs 檔案裡面不到 200 行程式碼就完成了
首先是基於 dotnet 基於 DirectML 控制檯執行 Phi-3 模型 部落格提供的方法用 DirectML 跑起來 Phi-3 模型。如何用 DirectML 跑起來 Phi-3 模型部分本文就一筆帶過,感興趣的夥伴還請閱讀我之前的部落格。放心,全部的程式碼都可以在本文末尾找到下載的方法
準備的程式碼如下
var folder = @"C:\lindexi\Phi3\directml-int4-awq-block-128\";
if (!Directory.Exists(folder))
{
folder = Path.GetFullPath(".");
}
Model model = new Model(folder);
還請大家將資料夾路徑更換為自己的模型資料夾
儘管 NVIDIA GeForce RTX 3090 Ti 顯示卡已經很強大了,但這臺裝置日常還有其他活。為了不被點爆。我這裡新增了一個 SemaphoreSlim 用來控制併發量,程式碼如下
var semaphoreSlim = new SemaphoreSlim(initialCount: 1, maxCount: 1);
每次進入請求的時候,都會等待一下 SemaphoreSlim 從而控制進入到模型計算裡,每次最多隻有一次請求。其他請求就依次進行排隊。為什麼能依次呢?因為這是利用了 SemaphoreSlim 帶來的額外功能,詳細請看 C# dotnet 的鎖 SemaphoreSlim 和佇列
再以下就是通用的初始化 ASP.NET Core 主機的程式碼
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://0.0.0.0:5017");
builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddSimpleConsole());
var app = builder.Build();
我約定了其他裝置的請求採用的是 HTTP 的 POST 請求方式,為了接受和約定請求,我就定義了名為 ChatRequest 的型別。這個型別只有 Prompt 提示詞一個引數,因為這正是模型所需的必備引數
record ChatRequest(string Prompt)
{
}
完成請求類的定義之後,接下來即可宣告 POST 的路徑對映了,程式碼如下
app.MapPost("/Chat", async (ChatRequest request, HttpContext context, [FromServices] ILogger<ChatSessionLogInfo> logger) =>
{
... // 忽略其他程式碼
});
app.Run();
以上程式碼傳入的委託裡面,只有 ChatRequest 是從其他裝置傳送過來的,其他兩個引數都是框架提供的
先設定 http 響應,包括設定狀態碼和設定開始,程式碼如下
app.MapPost("/Chat", async (ChatRequest request, HttpContext context, [FromServices] ILogger<ChatSessionLogInfo> logger) =>
{
var response = context.Response;
response.StatusCode = (int) HttpStatusCode.OK;
await response.StartAsync();
... // 忽略其他程式碼
});
設定狀態碼一定要在 StartAsync 之前,呼叫 StartAsync 時,客戶端將會收到 HTTP 的響應頭了。在 StartAsync 和 CompleteAsync 之間,就可以慢慢傳送 Body 過去
進入 StartAsync 之後,再等待訊號量。如此可以先讓客戶端收到 HTTP 的頭,不會讓客戶端等得太無聊
app.MapPost("/Chat", async (ChatRequest request, HttpContext context, [FromServices] ILogger<ChatSessionLogInfo> logger) =>
{
var response = context.Response;
response.StatusCode = (int) HttpStatusCode.OK;
await response.StartAsync();
await semaphoreSlim.WaitAsync();
try
{
... // 忽略其他程式碼
}
finally
{
semaphoreSlim.Release();
await response.CompleteAsync();
}
});
完成基礎框架邏輯之後,咱就可以開始讓模型處理其他裝置傳送過來的提示詞資訊,核心程式碼如下
var prompt = request.Prompt;
logger.LogInformation($"Session={sessionName};TraceId={traceId}\r\nPrompt={request.Prompt}");
var generatorParams = new GeneratorParams(model);
using var tokenizer = new Tokenizer(model);
var sequences = tokenizer.Encode(prompt);
generatorParams.SetSearchOption("max_length", 1024);
generatorParams.SetInputSequences(sequences);
generatorParams.TryGraphCaptureWithMaxBatchSize(1);
using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
var stringBuilder = new StringBuilder();
while (!generator.IsDone())
{
generator.ComputeLogits();
generator.GenerateNextToken();
// 每次只會新增一個 Token 值
// 需要呼叫 tokenizerStream 的解碼將其轉為人類可讀的文字
// 由於不是每一個 Token 都對應一個詞,因此需要根據 tokenizerStream 壓入進行轉換,而不是直接呼叫 tokenizer.Decode 方法,或者呼叫 tokenizer.Decode 方法,每次都全部轉換
var text = Decode();
// 有些時候這個 decodeText 是一個空文字,有些時候是一個單詞
// 空文字的可能原因是需要多個 token 才能組成一個單詞
// 在 tokenizerStream 底層已經處理了這樣的情況,會在需要多個 Token 才能組成一個單詞的情況下,自動合併,在多個 Token 中間的 Token 都返回空字串,最後一個 Token 才返回組成的單詞
if (!string.IsNullOrEmpty(text))
{
stringBuilder.Append(text);
}
await streamWriter.WriteAsync(text);
await streamWriter.FlushAsync();
string? Decode()
{
// 這裡的 tokenSequences 就是在輸入的 sequences 後面新增 Token 內容
ReadOnlySpan<int> tokenSequences = generator.GetSequence(0);
// 取最後一個進行解碼為文字
var decodeText = tokenizerStream.Decode(tokenSequences[^1]);
//// 當前全部的文字
//var allText = tokenizer.Decode(tokenSequences);
return decodeText;
}
}
對於服務化來說,以上程式碼最核心的就是當模型每吐出一個字的時候,就呼叫一次 await streamWriter.WriteAsync(text)
將其傳送出去
由於我的業務需求都在一個區域網內,我就無視 await streamWriter.WriteAsync(text)
的耗時了。如果大家準備跨網傳送,則可以再使用 Channel 進行最佳化,確保讓模型跑得不間斷,不會由於網路速度影響而讓模型沒有全速跑
以上就是使用 ASP.NET Core 簡單給 Phi 模型封裝一個服務的方法
本文程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快
先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 006b67e95fb0e1aa832c5a63c27b78a3fd33ee0a
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 006b67e95fb0e1aa832c5a63c27b78a3fd33ee0a
獲取程式碼之後,進入 Bp/JearnugurgelrurkawdoBeacecidem 資料夾,即可獲取到原始碼
更多技術部落格,請參閱 部落格導航