Yapay zeka veya tam yığın geliştirme ile mücadele mi ediyorsunuz? Uzmanlarımız size rehberlik etmek için burada: özel tavsiyeler, teknik entegrasyon ve daha fazlası. Bize ulaşın [email protected].

Go, FastAPI ve JS ile LLM'niz için Token Streaming UI Nasıl Geliştirilir

Üretken modellerin bir sonuç döndürmesi bazen biraz zaman alabilir, bu nedenle sonucun kullanıcı arayüzünde anında görünmesini sağlamak için token akışından yararlanmak ilginçtir. İşte Go, FastAPI ve Javascript ile LLM'niz için böyle bir metin akışı ön ucunu nasıl elde edebileceğiniz.

PC'de Geliştirici

Token Akışı Nedir?

Hatırlatmak gerekirse, bir token küçük bir kelime, bir kelimenin parçası veya noktalama işareti olabilen benzersiz bir varlıktır. Ortalama olarak 1 token 4 karakterden oluşur, ve 100 belirteç kabaca 75 kelimeye eşdeğerdir. Doğal Dil İşleme modellerinin işlemek için metninizi belirteçlere dönüştürmesi gerekir.

Bir metin oluşturma yapay zeka modeli ("üretken" model olarak da bilinir) kullanırken, donanımınıza ve modelinizin boyutuna bağlı olarak yanıt süresi oldukça yüksek olabilir. Örneğin, fp16'da bir NVIDIA A100 GPU'ya yerleştirilen LLaMA 30B gibi büyük bir dil modeli (diğer adıyla "LLM") söz konusu olduğunda, model 100 jetonu yaklaşık 3 saniyede üretir. Dolayısıyla, üretici modelinizin yüzlerce veya binlerce kelimeden oluşan büyük bir metin parçası oluşturmasını bekliyorsanız, gecikme yüksek olacaktır ve belki de beklemeniz gerekecektir. tam yanıt almak için 10 saniyeden daha uzun bir süre gerekir.

Yanıt almak için bu kadar uzun süre beklemek kullanıcı deneyimi açısından bir sorun olabilir. Bu durumda çözüm token akışıdır!

Token akışı, tüm yanıtın hazır olmasını beklemek yerine her yeni tokenin anında üretilmesiyle ilgilidir. Bu sizin ChatGPT uygulamasında veya örneğin NLP Cloud ChatDolphin asistanında görebilirsiniz. Kelimeler model tarafından üretildikleri anda görünürler. ChatDolphin AI asistanını buradan deneyin.

NLP Cloud üzerinde ChatDolphin ile token akışı NLP Cloud üzerinde ChatDolphin asistanı ile token akışı. Burada dene.

Token Akışını Destekleyen Bir Çıkarım Motoru Seçme

İlk adım, token akışını destekleyen bir çıkarım motorundan yararlanmanız olacaktır.

İşte göz önünde bulundurmak isteyebileceğiniz bazı seçenekler:

İşte HuggingFace generate() yöntemini kullanan bir örnek:

from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
from threading import Thread

tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
inputs = tokenizer(["An increasing sequence: one,"], return_tensors="pt")
streamer = TextIteratorStreamer(tokenizer)

# Run the generation in a separate thread, so that we can fetch the generated text in a non-blocking way.
generation_kwargs = dict(inputs, streamer=streamer, max_new_tokens=20)
thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()
for new_text in streamer:
    print(new_text)

Bu örnekte, GPT-2 modeliyle bir çıktı oluşturuyoruz ve her jetonu gelir gelmez konsola yazdırıyoruz.

FastAPI ile Yanıt Akışı

Artık bir çıkarım motoru seçtiğinize göre, modelinizi sunmanız ve akışa alınan belirteçleri döndürmeniz gerekecektir.

Modeliniz büyük olasılıkla bir Python ortamında çalışacaktır, bu nedenle belirteçleri döndürmek için bir Python sunucusuna ihtiyacınız olacaktır ve bunları bir HTTP API aracılığıyla kullanılabilir hale getirin. FastAPI bu tür durumlar için fiili bir seçim haline gelmiştir.

Burada Uvicorn ve FastAPI'nin StreamingResponse'sini kullanarak her bir token'ı üretildiği anda servis ediyoruz. İşte bir örnek:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
from threading import Thread

model = AutoModelForCausalLM.from_pretrained("gpt2")
tokenizer = AutoTokenizer.from_pretrained("gpt2")

app = FastAPI()

async def generate():
    inputs = tokenizer(["An increasing sequence: one,"], return_tensors="pt")
    streamer = TextIteratorStreamer(tokenizer)
    generation_kwargs = dict(inputs, streamer=streamer, max_new_tokens=20)
    thread = Thread(target=model.generate, kwargs=generation_kwargs)
    thread.start()
    for new_text in streamer:
        yield new_text

@app.get("/")
async def main():
    return StreamingResponse(generate())

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Aşağıdaki cURL komutu sayesinde akış sunucunuzu test edebilirsiniz:

curl -N localhost:8000

Artık düzgün bir şekilde akış belirteçleri döndüren çalışan bir AI modelimiz var.

Bu akış belirteçlerini tarayıcıdaki bir istemci uygulamasından doğrudan okuyabiliriz. Ancak 2 nedenden dolayı bunu yapmayacağız.

İlk olarak, aşağıdakileri birbirinden ayırmak önemlidir AI modelini yığının geri kalanından ayırırız çünkü her seferinde modeli yeniden başlatmak istemeyiz API'de küçük bir değişiklik yapacağız. Modern jeneratif yapay zeka modellerinin çok ağır olduğunu unutmayın ve yeniden başlatılması genellikle birkaç dakika sürer.

İkinci bir neden ise Python'un her zaman en iyi seçenek olmamasıdır Bizim yapacağımız gibi yüksek verimli eşzamanlı bir uygulama oluşturmak söz konusu olduğunda. Bu seçim elbette tartışılabilir ve bu bir zevk meselesi de olabilir!

Belirteçleri Bir Go Ağ Geçidi Üzerinden İletme

Yukarıda da belirtildiği gibi, modeliniz ile nihai müşteriniz arasına bir geçit eklemek önemlidir, ve Go böyle bir uygulama için iyi bir programlama dilidir. Üretimde, bir tersine çevirici de eklemek isteyebilirsiniz Go ağ geçidi ile son istemci arasında proxy ve Go ağ geçidiniz ile AI modeliniz arasında bir yük dengeleyici yükü modelinizin birkaç kopyasına yaymak için. Ancak bu bizim makalemizin kapsamı dışında!

Go uygulamamız aynı zamanda nihai HTML sayfasının oluşturulmasından da sorumlu olacaktır.

Bu uygulama FastAPI uygulamasına bir istekte bulunur, FastAPI'den akışlı belirteçleri alır ve her bir Sunucu Gönderilen Olayları (SSE) kullanarak ön uca token gönderir. SSE, tek yönlü olduğu için websocket'lerden daha basittir. Bu 'yi dinlemeden bir istemciye bilgi gönderen bir uygulama oluşturmak istediğinizde iyi bir seçimdir. müşteri yanıtı.

İşte Go kodu (HTML/JS/CSS şablonu bir sonraki bölümde gösterilecektir):

package main

import (
    "bufio"
    "fmt"
    "html/template"
    "io"
    "log"
    "net/http"
    "strings"
)

var (
    templates      *template.Template
    streamedTextCh chan string
)

func init() {
    // Parse all templates in the templates folder.
    templates = template.Must(template.ParseGlob("templates/*.html"))

    streamedTextCh = make(chan string)
}

// generateText calls FastAPI and returns every token received on the fly through
// a dedicated channel (streamedTextCh).
// If the EOF character is received from FastAPI, it means that text generation is over.
func generateText(streamedTextCh chan<- string) {
    var buf io.Reader = nil

    req, err := http.NewRequest("GET", "http://127.0.0.1:8000", buf)
    if err != nil {
        log.Fatal(err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    reader := bufio.NewReader(resp.Body)

outerloop:
    for {
        chunk, err := reader.ReadBytes('\x00')
        if err != nil {
            if err == io.EOF {
                break outerloop
            }
            log.Println(err)
            break outerloop
        }

        output := string(chunk)

        streamedTextCh <- output
    }
}

// formatServerSentEvent creates a proper SSE compatible body.
// Server sent events need to follow a specific formatting that
// uses "event:" and "data:" prefixes.
func formatServerSentEvent(event, data string) (string, error) {
    sb := strings.Builder{}

    _, err := sb.WriteString(fmt.Sprintf("event: %s\n", event))
    if err != nil {
        return "", err
    }
    _, err = sb.WriteString(fmt.Sprintf("data: %v\n\n", data))
    if err != nil {
        return "", err
    }

    return sb.String(), nil
}

// generate is an infinite loop that waits for new tokens received 
// from the streamedTextCh. Once a new token is received,
// it is automatically pushed to the frontend as a server sent event. 
func generate(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "SSE not supported", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/event-stream")

    for text := range streamedTextCh {
        event, err := formatServerSentEvent("streamed-text", text)
        if err != nil {
            http.Error(w, "Cannot format SSE message", http.StatusInternalServerError)
            return
        }

        _, err = fmt.Fprint(w, event)
        if err != nil {
            http.Error(w, "Cannot format SSE message", http.StatusInternalServerError)
            return
        }

        flusher.Flush()
    }
}

// start starts an asynchronous request to the AI engine.
func start(w http.ResponseWriter, r *http.Request) {
    go generateText(streamedTextCh)
}

func home(w http.ResponseWriter, r *http.Request) {
    if err := templates.ExecuteTemplate(w, "home.html", nil); err != nil {
        log.Println(err.Error())
        http.Error(w, "", http.StatusInternalServerError)
        return
    }
}

func main() {
    http.HandleFunc("/generate", generate)
    http.HandleFunc("/start", start).Methods("POST")
    http.HandleFunc("/", home).Methods("GET")

    log.Fatal(http.ListenAndServe(":8000", r))
}                

"/home" sayfamız HTML/CSS/JS sayfasını oluşturur (daha sonra gösterilecektir). "/start" sayfası şu sayfadan bir POST isteği alır Yapay zeka modelimize bir isteği tetikleyen JS uygulaması. Ve "/generate" sayfamız, sunucu tarafından gönderilen olaylar aracılığıyla sonucu JS uygulamasına döndürür.

start() işlevi ön uçtan bir POST isteği aldığında, otomatik olarak istekte bulunacak bir goroutine oluşturur FastAPI uygulamamıza.

generateText() işlevi FastAPI'yi çağırır ve özel bir kanal (streamedTextCh) aracılığıyla anında alınan her belirteci döndürür. FastAPI'den EOF karakteri alınırsa, bu metin üretiminin bittiği anlamına gelir.

generate() işlevi, streamedTextCh kanalından alınan yeni belirteçleri bekleyen sonsuz bir döngüdür. Yeni bir belirteç alındığında, otomatik olarak sunucu tarafından gönderilen bir olay olarak ön uca gönderilir. Sunucu tarafından gönderilen olayların "event:" ve "data:" öneklerini kullanan belirli bir biçimlendirmeyi takip etmesi gerekir. öneklerini, dolayısıyla formatServerSentEvent() işlevini kullanır.

SSE'nin tamamlanabilmesi için, "generate" sayfasına abone olarak sunucu tarafından gönderilen olayları dinleyebilen bir Javascript istemcisine ihtiyacımız var. Bunu nasıl başaracağımızı anlamak için bir sonraki bölüme bakın.

Tarayıcıda Javascript ile Belirteçleri Alma

Şimdi bir "templates" dizini oluşturmanız ve içine bir "home.html" dosyası eklemeniz gerekiyor.

İşte "home.html" içeriği:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Our Streamed Tokens App</title>
</head>
<body>
    <div id="response-section"></div>    
    <form method="POST">
        <button onclick="start()">Start</button>
    </form>
</body>
<script>
    // Disable the default behavior of the HTML form.
    document.querySelector('form').addEventListener('submit', function(e) {
        e.preventDefault()
    })

    // Make a request to the /start to trigger the request to the AI model.
    async function start() {
        try {
            const response = await fetch("/start", {
            method: "POST",
            })
        } catch (error) {
            console.error("Error when starting process:", error)
        }
    }

    // Listen to SSE by subscribing to the /generate page, and
    // put the result in the #response-section div.
    const evtSource = new EventSource("generate")
    evtSource.addEventListener("streamed-text", (event) => {
        document.getElementById('response-section').innerHTML = event.data
    })
</script>
</html>

Gördüğünüz gibi, tarayıcıda SSE dinlemek oldukça basittir.

Öncelikle SSE uç noktamıza ("/generate" sayfası) abone olmanız gerekir. daha sonra akışa alınan belirteçleri en kısa sürede okuyacak bir olay dinleyicisi eklemeniz gerekir. alınırlar.

Modern tarayıcılar otomatik olarak yeniden bağlanmayı dener Bağlantı sorunları olması durumunda olay kaynağı.

Sonuç

Artık dinamik olarak çalışan modern bir üretken yapay zeka uygulamasının nasıl oluşturulacağını biliyorsunuz. Tarayıcıda metin akışı, à la ChatGPT!

Sizin de fark ettiğiniz gibi, böyle bir uygulamanın birkaç uygulama kadar basit olması gerekmez. katmanlar dahil edilmiştir. Ve tabii ki yukarıdaki kod, kodun anlaşılması için aşırı basitleştirilmiştir. Örnek.

Token akışıyla ilgili temel zorluk, ağ arızalarının ele alınmasıyla ilgilidir. Çoğu bu hatalar Go arka ucu ile Javascript ön ucu arasında gerçekleşecektir. Yapacaksın daha gelişmiş yeniden bağlantı stratejilerini keşfetmeli ve hataların UI'ye uygun şekilde bildirilmiştir.

Umarım bu öğreticiyi faydalı bulmuşsunuzdur!

Vincent
NLP Cloud'da Geliştirici Danışmanı