Skip to Content
Mélodium 0.10.1 is now available!
DocsExemplesInférence LLM distribuée

Inférence LLM distribuée

Source: 15_distributed_llm_inference

Un serveur HTTP qui accepte des prompts en texte brut et renvoie les réponses LLM en flux. L’appel LLM s’exécute sur un runner Mélodium cloud ; le package ml n’a besoin d’être disponible que sur le runner, pas sur la machine locale. Celle-ci ne requiert aucune dépendance ML.

Exécution

melodium run 15_distributed_llm_inference/Compo.toml \ --api_token "my-api-token" \ --openai_key sk-... \ --port 8080
Note

api_token authentifie ici auprès d’une API Mélodium Services, comme Cadence.CI. openai_key est une clé d’API OpenAI, transmise au runner distant.

$ curl -X POST http://127.0.0.1:8080/chat \ -H "Content-Type: text/plain" \ -d "Explain the Mélodium dataflow model in one sentence." Mélodium is a dataflow programming language…

Fonctionnement

server instancie les modèles DistantEngine, DistributionEngine et le HttpServer local. Le modèle Assistant (une enveloppe LLM) est défini ici mais n’est instancié que sur le runner distant :

model Assistant(const openai_key: string) : RemoteLlm { backend = "openai" api_key = |wrap<string>(openai_key) base_url = "" model = "gpt-4o-mini" system = "You are a concise and helpful assistant." max_tokens = |wrap<u64>(1024) temperature = _ top_p = _ timeout = _ } model runner: DistantEngine(api_url=|wrap<string>("https://api.melodium.tech/0.1"), api_token=|wrap<string>(api_token)) model distributor: DistributionEngine( treatment = "distributed_llm_inference/main::inferText", version = "0.1.0" ) model httpServer: HttpServer(host=|from_ipv4(|localhost_ipv4()), port=port)

La machine locale n’a besoin que des packages http, distrib et work ; le package ml (et sa logique d’appel API) réside entièrement sur le runner.

server treatment diagram Voir dans Compositeur Studio

Passer des paramètres const au traitement distant

inferText a besoin de openai_key pour configurer son modèle Assistant, mais les paramètres const ne peuvent pas être passés via des flux. Ils sont envoyés via l’appel start du moteur de distribution, une seule fois, au moment de la connexion :

provisionRunner: distant[distant_engine=runner]( max_duration = 600, memory = 512, // Mo cpu = 1000, // millicœurs storage = 512, // Mo edition = _, arch = _, volumes = [], containers = [], service_containers = [], tags = [] ) startup.trigger -> provisionRunner.trigger,access -> distribStart.access distribStart: start[distributor=distributor](params=|map([|entry<string>("openai_key", openai_key)]))

Côté distant, inferText déclare le même paramètre en const :

treatment inferText(const openai_key: string) model llm: Assistant(openai_key=openai_key) input prompt: Stream<byte> output response: Stream<byte>

Un paramètre var exigerait au contraire des données propres à chaque invocation, ce à quoi servent sendStream / recvStream.

Ce n’est qu’une fois distribStart.ready déclenché que startHttp et le message de log de disponibilité s’exécutent, de sorte qu’aucune requête ne peut arriver avant que le worker distant ne soit connecté :

startHttp[http_server=httpServer]() distribStart.ready -> startHttp.trigger

Distribution de chaque requête

dispatchInfer encapsule distribute, sendStream et recvStream :

treatment dispatchInfer[distributor: DistributionEngine]() input prompt: Stream<byte> output response: Stream<byte> { trig: trigger<byte>() dist: distribute[distributor=distributor]() Self.prompt -> trig.stream,start -> dist.trigger sendPrompt: sendStream<byte>[distributor=distributor](name="prompt") recvResponse: recvStream<byte>[distributor=distributor](name="response") dist.distribution_id -> sendPrompt.distribution_id dist.distribution_id -> recvResponse.distribution_id Self.prompt -> sendPrompt.data recvResponse.data -> Self.response } connection.data -> dispatchInfer.prompt,response -> connection.data

Les noms de flux ("prompt", "response") correspondent aux noms de ports de inferText.

Le traitement distant inferText

treatment inferText(const openai_key: string) model llm: Assistant(openai_key=openai_key) input prompt: Stream<byte> output response: Stream<byte> { decodePrompt: decode() Self.prompt -> decodePrompt.data,text -> doChat.prompt doChat: chat[llm=llm]() chatErrLog: logErrors(label="llm") doChat.error -> chatErrLog.messages encodeResponse: encode() doChat.response -> encodeResponse.text,data -> Self.response }

decode convertit les octets du prompt en chaîne UTF-8, chat appelle le LLM (GPT-4o-mini via OpenAI) et émet les tokens de réponse sous forme de Stream<string>, et encode reconvertit chaque token en octets avant qu’il ne rejoigne Self.response. Comme doChat.response émet token par token, recvResponse.data est transmis directement dans connection.data : le client HTTP voit les tokens apparaître au fur et à mesure de leur génération, sans tampon intermédiaire.

inferText treatment diagram Voir dans Compositeur Studio

Dépendances

[dependencies] std = "0.10.1" # flux de base, journalisation, structures de données http = "0.10.1" # serveur et client HTTP net = "0.10.1" # utilitaires d'adresses IP encoding = "0.10.1" # encodage / décodage UTF-8 work = "0.10.1" # provisionnement de runners cloud distrib = "0.10.1" # distribution de flux entre runners ml = "0.10.1" # inférence LLM, STT, TTS et modèles locaux