Generatiivsed mudelid võtavad mõnikord aega, et tulemus tagasi anda, seega on huvitav kasutada sümbolite voogedastust, et tulemus ilmuks kohe kasutajaliideses. Siin on kirjeldatud, kuidas saate oma LLM-i jaoks Go, FastAPI ja Javascripti abil saavutada sellise tekstivoogude voogedastuse frontendiga.
Meeldetuletus on unikaalne üksus, mis võib olla kas väike sõna, sõna osa või kirjavahemärk. Keskmiselt koosneb 1 sümbol 4 tähemärgist, ja 100 märki vastab ligikaudu 75 sõnale. Loomuliku keele töötlemise mudelid peavad teksti töötlemiseks muutma teie teksti märgideks.
Teksti genereeriva tehisintellekti mudeli (tuntud ka kui "genereeriv" mudel) kasutamisel võib reageerimisaeg olla üsna suur, sõltuvalt teie riistvarast ja mudeli suurusest. Näiteks NVIDIA A100 GPU-l fp16-s kasutatava suure keelemudeli (tuntud ka kui "LLM") nagu LLaMA 30B puhul genereerib mudel 100 märki umbes 3 sekundiga. Seega, kui te ootate, et teie generatiivne mudel genereeriks suure, sadadest või tuhandetest sõnadest koosneva teksti, on latentsus suur ja te peate ootama võib-olla rohkem kui 10 sekundit, et saada täielik vastus.
Vastuse saamine nii kaua võib olla kasutajakogemuse seisukohalt probleemiks. Sellisel juhul on lahenduseks sümboolne voogedastus!
Token streaming tähendab iga uue tokeni genereerimist jooksvalt selle asemel, et oodata kogu vastuse valmimist. See on see, mida te näete näiteks ChatGPT rakenduses või NLP Cloud ChatDolphini assistendil. Sõnad ilmuvad kohe, kui mudel neid genereerib. Proovige Delfiini tehisintellekti assistenti siin.
Token streaming koos ChatDolphini assistendiga NLP Cloudis. Proovige seda siin.
Esimene samm on kasutada järeldusmootorit, mis toetab sümbolite voogedastust.
Siin on mõned võimalused, mida võiksite kaaluda:
Siin on näide, milles kasutatakse meetodit HuggingFace generate():
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)
Selles näites genereerime väljundi GPT-2 mudeliga ja trükime iga märgi konsooli kohe, kui see saabub.
Nüüd, kui olete valinud järeldusmootori, peate oma mudelit teenindama ja tagastama voogedastatud märgid.
Teie mudel töötab tõenäoliselt Python-keskkonnas, nii et teil on vaja Python-serverit, et tagastada märgid. ja teha need HTTP API kaudu kättesaadavaks. FastAPI on saanud de facto valikuks sellistes olukordades.
Siinkohal kasutame Uvicorn ja FastAPI StreamingResponse'i, et teenida iga token kohe pärast selle genereerimist. Siin on näide:
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)
Saate oma voogedastusserverit testida tänu järgmisele cURL-käsule:
curl -N localhost:8000
Meil on nüüd toimiv tehisintellekti mudel, mis tagastab korralikult voogedastatud märgid.
Me võiksime neid voogedastatud märgiseid lugeda otse kliendirakendusest brauseris. Kuid me ei kavatse seda teha 2 põhjusel.
Esiteks on oluline lahti siduda AI-mudelit ülejäänud virnast, sest me ei taha mudelit iga kord uuesti käivitada, kui me teeme APIs väikese muudatuse. Pidage meeles, et kaasaegsed genereerivad AI-mudelid on väga rasked ja nende taaskäivitamine võtab sageli mitu minutit.
Teine põhjus on see, et Python ei ole tingimata parim valik. kui tegemist on suure läbilaskevõimega samaaegse rakenduse loomisega, nagu me kavatseme teha. See valik võib muidugi arutada ja see võib olla ka maitse küsimus!
Nagu eespool mainitud, on oluline lisada värav teie mudeli ja lõppkliendi vahele, ja Go on hea programmeerimiskeel sellise rakenduse jaoks. Tootmises võiksite lisada ka vastupidise Go värava ja lõppkliendi vahele ning Go värava ja tehisintellekti mudeli vahele koormuse tasakaalustaja, et tagada jagada koormust mitmele teie mudeli koopiale. Kuid see ei kuulu meie artikli reguleerimisalasse!
Meie Go rakendus vastutab ka lõpliku HTML-lehe renderdamise eest.
See rakendus teeb taotluse FastAPI rakendusele, saab FastAPI-st voogedastatud märgid ja edastab iga sümboli edasi, kasutades Server Sent Events (SSE). SSE on lihtsam kui websockets, sest see on ühesuunaline. See on hea valik, kui soovite luua rakenduse, mis edastab teavet kliendile, ilma et kuulaksite võimalikku kliendi vastust.
Siin on Go-kood (HTML/JS/CSS malli näidatakse järgmises osas):
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))
}
Meie "/home" leht renderdab HTML/CSS/JS lehe (näidatud hiljem). Lehekülg "/start" saab POST päringu aadressilt JS-rakendusest, mis käivitab päringu meie AI-mudelile. Ja meie "/generate" leht tagastab tulemuse JS rakendusele serveri saadetud sündmuste kaudu.
Kui funktsioon start() saab frontendilt POST päringu, loob see automaatselt goroutine'i, mis teeb päringu meie FastAPI rakendusele.
Funktsioon generateText() kutsub FastAPI-d ja tagastab iga lennult saadud sümboli spetsiaalse kanali (streamedTextCh) kaudu. Kui FastAPI-st saadakse EOF-märk, tähendab see, et teksti genereerimine on lõppenud.
Funktsioon generate() on lõputu tsükkel, mis ootab uusi märgiseid, mis on saadud streamedTextCh kanalilt. Kui uus märk on saadud, lükatakse see automaatselt frontaalsüsteemi serveri saadetud sündmusena. Serveri saadetud sündmused peavad järgima konkreetset vormingut, mis kasutab "event:" ja "data:". eesliiteid, sellest ka funktsioon formatServerSentEvent().
Selleks, et SSE oleks täielik, vajame Javascript-klienti, mis suudab kuulata serveri saadetud sündmusi, tellides "generate" lehe. Kuidas seda saavutada, vaata järgmises jaotises.
Nüüd peate looma kataloogi "templates" ja lisama sinna faili "home.html".
Siin on "home.html" sisu:
<!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>
Nagu näete, on SSE kuulamine brauseris üsna lihtne.
Kõigepealt peate tellima meie SSE lõpp-punkti (lehekülg "/generate"). Seejärel tuleb lisada sündmuse kuulaja, mis loeb voogedastuse märgid kohe, kui need saadakse.
Kaasaegsed brauserid püüavad automaatselt uuesti ühendust luua sündmuse allikas ühendusprobleemide korral.
Te teate nüüd, kuidas luua kaasaegset genereerivat tehisintellekti rakendust, mis dünaamiliselt voogedastab teksti brauseris, à la ChatGPT!
Nagu te olete märganud, ei ole selline taotlus tingimata lihtne, sest mitu kihid on kaasatud. Ja muidugi on ülaltoodud kood liialt lihtsustatud, et teha näide.
Peamine väljakutse sümboolse voogedastuse puhul seisneb võrguhäirete käsitlemises. Enamik neist tõrgetest juhtub Go backend'i ja Javascript'i frontend'i vahel. Te peate peate uurima mõningaid edasijõudnuid taasühendamisstrateegiaid ja veenduma, et vead on kasutajaliidesele korralikult teatatakse.
Loodan, et leidsid selle õpetuse kasulikuks!
Vincent
NLP Cloudi arendajate eestkõneleja