Vaikeuksia tekoälyn tai full-stack-kehityksen kanssa? Asiantuntijamme opastavat sinua: räätälöityjä neuvoja, teknistä integrointia ja paljon muuta. Ota yhteyttä osoitteessa [email protected].

Kuinka kehittää Token Streaming UI LLM:ääsi Go:n, FastAPI:n ja JS:n avulla?

Generatiivisten mallien tulosten palauttaminen kestää joskus jonkin aikaa, joten on mielenkiintoista hyödyntää tokenien suoratoistoa, jotta tulokset näkyvät käyttöliittymässä lennossa. Seuraavassa kerrotaan, miten voit toteuttaa tällaisen tekstivirtauksen etusivun LLM:ään Go:n, FastAPI:n ja Javascriptin avulla.

Developer on PC

Mikä on Token Streaming?

Muistutuksena mainittakoon, että merkki on yksilöllinen kokonaisuus, joka voi olla joko pieni sana, sanan osa tai välimerkki. Keskimäärin 1 merkki koostuu 4 merkistä, ja 100 merkkiä vastaa suunnilleen 75 sanaa. Luonnollisen kielen käsittelymallien on muutettava tekstisi merkkeihin, jotta ne voivat käsitellä sitä.

Kun käytetään tekstin generoimista tekoälymallia (joka tunnetaan myös nimellä "generatiivinen" malli), vasteaika voi olla melko suuri laitteistosta ja mallin koosta riippuen. Esimerkiksi kun kyseessä on suuri kielimalli (tunnetaan myös nimellä "LLM"), kuten LLaMA 30B, jota käytetään NVIDIA A100 GPU:lla fp16:ssa, malli tuottaa 100 merkkiä noin 3 sekunnissa. Jos siis odotat generatiivisen mallisi tuottavan suuren tekstikappaleen, jossa on satoja tai tuhansia sanoja, latenssi on suuri, ja sinun on odotettava kenties yli 10 sekuntia saadaksesi täyden vastauksen.

Vastauksen odottaminen niin kauan voi olla ongelma käyttäjäkokemuksen kannalta. Ratkaisu tässä tapauksessa on token streaming!

Token streaming tarkoittaa jokaisen uuden tokenin luomista lennossa sen sijaan, että odottaisit koko vastauksen valmistumista. Tämä on se, mitä näet esimerkiksi ChatGPT-sovelluksessa tai NLP Cloud ChatDolphin -avustajassa. Sanat näkyvät heti, kun malli on ne luonut. Kokeile ChatDolphinin tekoälyavustajaa täällä.

Token streaming ChatDolphinin kanssa NLP Cloudissa Token streaming ChatDolphin assistantilla NLP Cloudissa. Kokeile sitä täällä.

Token streamingiä tukevan päättelymoottorin valitseminen

Ensimmäisenä askeleena sinun on hyödynnettävä päättelymoottoria, joka tukee token streamingia.

Seuraavassa on muutamia vaihtoehtoja, joita kannattaa harkita:

Tässä on esimerkki, jossa käytetään HuggingFace generate()-menetelmää:

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)

Tässä esimerkissä luomme tulosteen GPT-2-mallilla ja tulostamme jokaisen merkin konsoliin heti, kun se saapuu.

Vastauksen suoratoisto FastAPI:n avulla

Nyt kun olet valinnut päättelymoottorin, sinun on palveltava malliasi ja palautettava suoratoistetut merkit.

Malliasi käytetään todennäköisesti Python-ympäristössä, joten tarvitset Python-palvelimen, jotta voit palauttaa tunnukset. ja asettaa ne saataville HTTP-API:n kautta. FastAPI:stä on tullut de facto valinta tällaisiin tilanteisiin.

Tässä käytämme Uvicornia ja FastAPI:n StreamingResponsea palvelemaan jokaista tokenia heti, kun se on luotu. Tässä on esimerkki:

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)

Voit testata suoratoistopalvelimesi seuraavan cURL-komennon avulla:

curl -N localhost:8000

Meillä on nyt toimiva tekoälymalli, joka palauttaa oikein virtaavat merkit.

Voisimme lukea nämä suoratoistetut tunnisteet suoraan selaimen asiakassovelluksesta. Emme kuitenkaan aio tehdä sitä kahdesta syystä.

Ensinnäkin on tärkeää irrottaa toisistaan tekoälymalli muusta pinosta, koska emme halua käynnistää mallia uudelleen joka kerta, kun teemme pienen muutoksen API:han. Pidä mielessä, että nykyaikaiset generatiiviset tekoälymallit ovat hyvin raskaita ja niiden uudelleenkäynnistäminen kestää usein useita minuutteja.

Toinen syy on se, että Python ei välttämättä ole paras valinta. kun halutaan rakentaa korkean läpimenon rinnakkaissovellus, kuten aiomme tehdä. Tämä valinta voidaan tietysti keskustella, ja se voi olla myös makuasia!

Tokenien välittäminen Go Gatewayn kautta

Kuten edellä mainittiin, on tärkeää, että mallisi ja lopullisen asiakkaasi välille lisätään yhdyskäytävä, ja Go on hyvä ohjelmointikieli tällaiseen sovellukseen. Tuotannossa saatat haluta lisätä myös käänteisen välityspalvelimen Go-yhdyskäytävän ja lopullisen asiakkaan välille sekä kuorman tasaajan Go-yhdyskäytävän ja tekoälymallisi välille, jotta voidaan jakaa kuormaa mallisi useille kopioille. Mutta se ei kuulu artikkelimme aihepiiriin!

Go-sovelluksemme vastaa myös lopullisen HTML-sivun renderöinnistä.

Tämä sovellus tekee pyynnön FastAPI-sovellukselle, vastaanottaa FastAPI:lta suoratoistetut tokenit ja välittää jokaisen tokenin eteenpäin. tokenin etusivulle käyttämällä Server Sent Events (SSE) -tapahtumia. SSE on yksinkertaisempi kuin websockets, koska se on yksisuuntainen. Se on hyvä valinta, kun haluat rakentaa sovelluksen, joka lähettää tietoja asiakkaalle kuuntelematta mahdollisia asiakkaan vastausta.

Tässä on Go-koodi (HTML/JS/CSS-malli näytetään seuraavassa osassa):

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"-sivumme renderöi HTML/CSS/JS-sivun (näytetään myöhemmin). Sivu "/start" vastaanottaa POST-pyynnön osoitteesta JS-sovelluksesta, joka käynnistää pyynnön tekoälymalliimme. Ja "/generate"-sivumme palauttaa tuloksen JS-sovellukselle palvelimen lähettämien tapahtumien kautta.

Kun start()-funktio vastaanottaa POST-pyynnön etusivulta, se luo automaattisesti goroutiinin, joka tekee pyynnön. FastAPI-sovelluksellemme.

GeneroiText()-funktio kutsuu FastAPI:tä ja palauttaa jokaisen lennossa vastaanotetun merkin erityisen kanavan (streamedTextCh) kautta. Jos FastAPI:lta vastaanotetaan EOF-merkki, se tarkoittaa, että tekstin tuottaminen on päättynyt.

Generoi()-funktio on ääretön silmukka, joka odottaa streamedTextCh-kanavasta vastaanotettuja uusia merkkejä. Kun uusi merkki on vastaanotettu, se siirretään automaattisesti etusivulle palvelimen lähettämänä tapahtumana. Palvelimen lähettämien tapahtumien on noudatettava tiettyä muotoilua, jossa käytetään "event:" ja "data:". etuliitteitä, mistä johtuu formatServerSentEvent()-funktio.

Jotta SSE olisi täydellinen, tarvitsemme Javascript-asiakkaan, joka pystyy kuuntelemaan palvelimen lähettämiä tapahtumia tilaamalla "generate"-sivun. Seuraavassa luvussa kerrotaan, miten tämä saavutetaan.

Tunnusten vastaanottaminen Javascriptillä selaimessa

Sinun on nyt luotava "templates"-hakemisto ja lisättävä siihen "home.html"-tiedosto.

Tässä on "home.html" -osion sisältö:

<!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>

Kuten näet, SSE:n kuunteleminen selaimessa on melko suoraviivaista.

Ensin sinun on tilattava SSE-päätepisteemme (sivu "/generate"). Sitten sinun on lisättävä tapahtumakuuntelija, joka lukee suoratoistetut tunnisteet heti, kun ne vastaanotetaan.

Nykyaikaiset selaimet yrittävät automaattisesti muodostaa yhteyden uudelleen. tapahtumalähteen, jos yhteysongelmia ilmenee.

Päätelmä

Tiedät nyt, miten luoda moderni generatiivinen tekoälysovellus, joka dynaamisesti lähettää tekstiä selaimessa, à la ChatGPT!

Kuten olette huomanneet, tällainen sovellus ei välttämättä ole yksinkertainen, koska useita kerroksia. Ja tietysti yllä oleva koodi on yksinkertaistettu liikaa, jotta se olisi esimerkin vuoksi.

Token streamingin suurin haaste on verkkovikojen käsittely. Suurin osa näistä vioista tapahtuu Go-backendin ja Javascript-frontendin välillä. Sinulla on on tutkittava joitakin edistyneempiä uudelleenkytkentästrategioita ja varmistettava, että virheet ovat virheet raportoidaan asianmukaisesti käyttöliittymälle.

Toivon, että löysit tämän ohjeen hyödylliseksi!

Vincent
NLP Cloudin kehittäjäasiamies