iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
AI & Data

Rust 加 MLOps,你說有沒有搞頭?系列 第 17

[Day 17] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (8/10)|Rust 中載入 GGML 模型

  • 分享至 

  • xImage
  •  

今日份 Ferris

度過了昨天最難的部分,今天來繼續完善我們的專案吧!
https://ithelp.ithome.com.tw/upload/images/20231002/20141304EZYp4jdb5D.jpg
*Ferris 3D by Ray March

載入模型

🏮 今天完整的程式碼可以拉到最底下 Put it together 區塊或是在 GitHub 找到。

在昨天的 API 程式碼中,我們假設模型在伺服器 (Actix 後端) 開啟時就會被載入,並被放在 actix_web::web::Data wrapper 中,昨天的程式碼如下:

let model =
        extract(|data: Data<Llama>, _connection: ConnectionInfo| async move { data.into_inner() })
            .await
            .unwrap();

函式簽名

所以我們今天就要來讓這個假設成立,在 [Day 12] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (3/9)|Leptos 小教室 時提到後端的程式碼放在 src/main.rs 中,所以我們要先在這個檔案裡寫一個載入模型的函式。

這個函式請放在 async fn main() -> std::io::Result<()> 區塊之下,因為待會我們會在此函式區塊中使用它。

首先使用 cfg_if 巨集指定這個函式只在伺服器端編譯,並引入需要的函式庫與物件:

cfg_if! {
    if #[cfg(feature = "ssr")] {
        use std::env;

        use actix_web::*;
        use dotenv::dotenv;
        use llm::models::Llama;

        fn get_language_model() -> Llama {}
    }
}

設定模型位址

由於我們要用 .env 檔來設定模型的位址,所以要加入 dotenv crate,其方法一樣是在 Cargo.toml 的 [dependencies] 區塊最後面加上:

dotenv = { version = "0.15.0", optional = true}

並在 [features] 區塊 ssr 的 list 中也加上:

"dep:dotenv"

然後在專案的資料夾下建立一個 .env 檔,其中用 MODEL_PATH 來指定模型的位置,例如 (請換成實際的下載位置,要下載哪個模型請參考 [Day 15] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (6/9)|GGML 量化 LLaMa):

MODEL_PATH="/home/ubuntu/iron-llama/Taiwan-LLaMa-13b-1.0.ggmlv3.q5_K_M.bin"

使用 Rustformers 載入 GGML 模型

我們將函式命名為 get_language_model,它會回傳 llm::models::Llama 結構體,也就是我們的模型,模型的載入參考 llm 官方文件,每一行的說明直接註解在上面:

		  fn get_language_model() -> Llama {
            use std::path::PathBuf;
            dotenv().ok();
            let model_path = env::var("MODEL_PATH").expect("MODEL_PATH cannot be empty");

            // load a GGML model from disk
            llm::load::<Llama>(
                // path to GGML file
                &PathBuf::from(&model_path),
                /// Read the vocabulary from the model if available, and use a simplistic tokenizer.
                /// This is easy to use, but may not be the best choice for your use case, and is not
                /// guaranteed to be available for all models.
                llm::TokenizerSource::Embedded,
                // llm::ModelParameters
                Default::default(),
                // load progress callback
                llm::load_progress_callback_stdout
            ).unwrap_or_else(|err| panic!("Failed to load model: {err}"));
        }

將模型載入後端

有了載入模型的函式後,我們可以在建立 HttpServer 前將模型包進前面提到的 Data wrapper 中, 所以請在 async fn main() -> std::io::Result<()> 區塊中 let routes = generate_route_list(|| view! { <App/> }); 之下加上:

let model = web::Data::new(get_language_model());

然後就能將模型加到 Actix 後端建立的程序之中,所以請在 HttpServer 區塊中 App::new() 之下加上 .app_data(model.clone())

	  HttpServer::new(move || {
        let leptos_options = &conf.leptos_options;
        let site_root = &leptos_options.site_root;

        App::new()
            **.app_data(model.clone())**
            .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
						...

在底層這個 Data wrapper 會把資料包進 Rust 的原子性參考計數 Arc<T> 中,其中字母 A 指的就是 Atomic,Arc 型別能讓數個 worker 能擁有接收端,它讓我們可以安全地在多重執行緒共享所有權。

會這麼做的原因是 Actix 後端的 HttpServer 預設上會開啟與系統實體 CPU 數量相同的 HTTP workers,而搭配 Rust 提供的 Arc 型別就能輕鬆地進行多執行緒處理,詳細請參考 Multi-Threading

如果 cargo leptos watch 還在執行的話 (沒有請執行這行指令),可以看到終端機印出成功載入模型的資訊:
https://ithelp.ithome.com.tw/upload/images/20231002/20141304CoksE73t2A.png

完成 send 函式

成功載入模型之後,converse 函式的假設就成立了,我們來填坑收尾吧!

首先把 src/app.rs 內的 todo! 巨集換成呼叫 converse(conversation()).await (因為 converse 是異步函式,所以要 等 (await) 一下)。
其輸入值是 conversation ReadSignal 呼叫 get() 後 (這裡用了函式呼叫式的簡寫) 取得的內容,也就是代表歷史對話的 Conversation 結構體。

最值得注意的是 converse 是實作在 src/api.rs 中的函式,其中包含的都是 伺服器端 的邏輯,但我們卻得以在 客戶端 呼叫它。

這一切都歸功於昨天提到的 #[server()] 巨集,會在客戶端自動生成指向 API 的 HTTP request,一切都被 Leptos 抽象化了!

Put it together

今天總共修改了 3 個檔案,另外還建立了 1 個 .env 檔。

  1. 首先是 src/main.rs,經過今天的內容,async fn favicon 之上的程式碼整理如下:
    use cfg_if::cfg_if;
    
    #[cfg(feature = "ssr")]
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        use actix_files::Files;
        use actix_web::*;
        use iron_llama::app::*;
        use leptos::*;
        use leptos_actix::{generate_route_list, LeptosRoutes};
    
        let conf = get_configuration(None).await.unwrap();
        let addr = conf.leptos_options.site_addr;
        // Generate the list of routes in your Leptos App
        let routes = generate_route_list(|| view! { <App/> });
    
        let model = web::Data::new(get_language_model());
    
        HttpServer::new(move || {
            let leptos_options = &conf.leptos_options;
            let site_root = &leptos_options.site_root;
    
            App::new()
                .app_data(model.clone())
                .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
                // serve JS/WASM/CSS from `pkg`
                .service(Files::new("/pkg", format!("{site_root}/pkg")))
                // serve other assets from the `assets` directory
                .service(Files::new("/assets", site_root))
                // serve the favicon from /favicon.ico
                .service(favicon)
                .leptos_routes(
                    leptos_options.to_owned(),
                    routes.to_owned(),
                    || view! { <App/> },
                )
                .app_data(web::Data::new(leptos_options.to_owned()))
            //.wrap(middleware::Compress::default())
        })
        .bind(&addr)?
        .run()
        .await
    }
    
    cfg_if! {
        if #[cfg(feature = "ssr")] {
            use std::env;
    
            use actix_web::*;
            use dotenv::dotenv;
            use llm::models::Llama;
    
            fn get_language_model() -> Llama {
                use std::path::PathBuf;
                dotenv().ok();
                let model_path = env::var("MODEL_PATH").expect("MODEL_PATH cannot be empty");
    
                // load a GGML model from disk
                llm::load::<Llama>(
                    // path to GGML file
                    &PathBuf::from(&model_path),
                    /// Read the vocabulary from the model if available, and use a simplistic tokenizer.
                    /// This is easy to use, but may not be the best choice for your use case, and is not
                    /// guaranteed to be available for all models.
                    llm::TokenizerSource::Embedded,
                    // llm::ModelParameters
                    Default::default(),
                    // load progress callback
                    llm::load_progress_callback_stdout
                ).unwrap_or_else(|err| panic!("Failed to load model: {err}"))
            }
        }
    }
    
  2. Cargo.toml 引入了 dotenv crate。
  3. 實作完成 converse 函式的先決條件,因此把 src.app.rs 的 async move { todo!(converse) } 改成了 async move { converse(conversation()).await }

好啦,今天就這樣囉,明天再來把前端的兩個主要區塊實作好就完成囉!
/images/emoticon/emoticon31.gif


上一篇
[Day 16] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (7/10)|後端 LLM API
下一篇
[Day 18] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (9/10)|前端美化與最終成果
系列文
Rust 加 MLOps,你說有沒有搞頭?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言