Modell / Datensatz
trymirai/uzu avatar
trymirai/uzu

uzu: Inferenz direkt in der App, aber nur mit klaren Grenzen

A high-performance inference engine for AI models

1.796 Sterne82 ForksRustMIT

Auf einen Blick

Was ist das?
uzu ist eine in Rust geschriebene Inferenz-Engine von trymirai, die Modelle lokal in iOS-, macOS-, Python-, TypeScript- und Rust-Anwendungen ausführt. Der Ansatz senkt Latenz und vermeidet Inferenzkosten, bringt aber Abhängigkeiten bei Modellformat, Hardware und Download mit sich.
Für wen ist es gedacht?
uzu passt zu Teams, die ein Modell auf Apple-Geräten oder in einer lokalen Python- oder Node-Umgebung betreiben wollen und dafür einen einheitlichen Modellbezeichner sowie einen Engine-Lebenszyklus akzeptieren. Wer CUDA, verteilte Ausführung oder eigene Modellformate ohne Anpassung braucht, ist hier falsch.
Darf ich es kommerziell nutzen?
Ja. MIT ist eine freizügige Lizenz: Sie dürfen darauf aufbauende Software nutzen, verändern und verkaufen, solange Sie die Urheberrechts- und Lizenzhinweise beibehalten.
Wird es noch gepflegt?
Ja. Das Repository hat innerhalb des letzten Tages neue Commits erhalten.
In welcher Sprache ist es geschrieben?
Hauptsächlich Rust, laut der Sprachstatistik von GitHub.

Die Antworten beruhen auf den GitHub-Daten des Projekts (zuletzt abgeglichen am 15. September 2026) und auf unserer Analyse. Sie sind keine Rechtsberatung.

TIEFGEHENDE OPEN-SOURCE-ANALYSE

Welches Problem uzu adressiert und für wen es gedacht ist

Der Ausgangspunkt ist die klassische Server-Inferenz: Ein Modell läuft in einem Rechenzentrum, die App schickt Anfragen dorthin und wartet. Das kostet Netzwerkzeit, erzeugt laufende Kosten pro Anfrage und schickt Nutzerdaten über eine Leitung. uzu setzt an allen drei Punkten an. Die README formuliert das Ziel mit den Worten zero latency, full data privacy und no inference costs. Das sind Marketingformulierungen, aber sie beschreiben eine reale Verschiebung: Die Berechnung wandert auf das Gerät, auf dem die App ohnehin läuft.

Die Zielgruppe lässt sich aus den Bindings ablesen. Es gibt Bindings für Rust, Python, Swift und TypeScript, wobei die Swift-Anbindung laut Package.swift auf iOS und macOS zielt und Swift 5.9 voraussetzt. Wer eine mobile App baut und dort ein Sprach- oder Chatmodell betreiben will, ist der offensichtliche Adressat. Dazu kommen Python- und Node-Projekte, die keinen separaten Inferenzdienst betreiben wollen. Die Themenliste des Repositories nennt metal und tts, was auf Apples Unified Memory und Sprachsynthese als Einsatzfelder hindeutet.

Wer dagegen ein Modell mit mehreren hundert Milliarden Parametern auf einem Cluster betreiben will, gehört nicht zur Zielgruppe. Das Beispielmodell in allen vier Quick-Start-Abschnitten ist alibaba:qwen3.5:0.8b:mirai:mirai-m:4, also ein Modell mit 0,8 Milliarden Parametern. Das ist eine bewusste Kalibrierung: klein genug, um auf ein Endgerät zu passen.

Engine, Modell, Session: der Ablauf im Code

Die Architektur ist in drei Objekte gegliedert, die in allen Bindings in derselben Reihenfolge auftreten. Zuerst wird eine Engine erzeugt, dann wird über die Engine ein Modell aufgelöst, anschließend wird daraus eine Chat-Session gebaut. Die Engine ist der Ressourcenhalter, das Modell beschreibt die Gewichte und die Session hält den Gesprächszustand.

In Rust sieht das so aus: EngineConfig::default() wird an Engine::new übergeben, danach liefert engine.model("alibaba:qwen3.5:0.8b:mirai:mirai-m:4") ein Result, dessen Ok-Zweig ein Option enthält. Der Aufruf engine.download(&model) gibt einen Stream zurück, über den die Schleife while let Some(update) = downloader.next().await läuft und update.progress() einen Wert liefert, der im Beispiel mit 100.0 multipliziert wird. Das ist ein Fortschrittswert zwischen 0 und 1, kein Prozentwert. Wer ihn direkt als Prozentzahl ausgibt, bekommt falsche Zahlen.

Die eigentliche Inferenz läuft über session.reply(messages, ChatReplyConfig::default()). Die Nachrichten werden als Vektor von ChatMessage-Objekten gebaut, mit ChatMessage::system().with_text(...) und ChatMessage::user().with_text(...). Zurück kommt eine Liste von Antworten. Das Beispiel greift mit replies.last() auf die letzte zu und liest zwei getrennte Felder: reply.message.reasoning() und reply.message.text(). Diese Trennung ist bemerkenswert. Reasoning-Inhalte und sichtbarer Text werden nicht in einem String vermischt, sondern getrennt geführt, und reasoning() liefert ein Option, das im Beispiel mit unwrap_or_default() behandelt wird.

Die Python-Variante bildet dieselbe Struktur mit anderen Mitteln ab. Engine.create(engine_config) ist eine asyncio-Koroutine, der Download wird als async for update in (await engine.download(model)).iterator() konsumiert, und die Antwortfelder heißen message.reasoning und message.text ohne Klammern, weil es Attribute statt Methoden sind. In TypeScript ist Engine.create ebenfalls asynchron, der Download ist ein for await über await engine.download(model), und die Felder sind wieder methodenlos: update.progress, message.reasoning, message.text. In Swift dagegen sind es Methodenaufrufe: update.progress(), message.reasoning(), message.text(). Diese Inkonsistenz zwischen den Bindings ist real und kostet beim Wechsel zwischen Sprachen Zeit.

Installation und die Modellbezeichner als eigentliche Schnittstelle

Die Einbindung unterscheidet sich pro Sprache, folgt aber einem Muster. In Rust wird die Abhängigkeit direkt aus dem Repository geholt, nicht von crates.io: uzu = { git = "https://github.com/trymirai/uzu", branch = "main", package = "uzu" }. Das ist ein relevanter Unterschied zu einem veröffentlichten Paket. Der Branch main bedeutet, dass ein Build jederzeit einen neuen Commit ziehen kann, ohne dass sich die Versionsnummer im Manifest ändert. Reproduzierbare Builds brauchen hier eine feste Revision, nicht den Branch-Namen.

In Python lautet der Befehl uv add uzu==0.5.26, in TypeScript pnpm add @trymirai/uzu@0.5.26, in Swift .package(url: "https://github.com/trymirai/uzu.git", from: "0.5.26"). Die drei Paketmanager-Aufrufe pinnen auf eine konkrete Version, der Rust-Aufruf nicht. Diese Asymmetrie ist der auffälligste Punkt in der Installationsanleitung.

Die zweite Schnittstelle ist der Modellbezeichner. Der String alibaba:qwen3.5:0.8b:mirai:mirai-m:4 ist doppelpunktsepariert und trägt offenbar Anbieter, Modellfamilie, Parametergröße und eine Quantisierungs- oder Variantenangabe. Die README verlinkt für die vollständige Liste auf trymirai.com/local-models. Das bedeutet: Die unterstützten Modelle sind ein kuratierter Katalog, kein offenes Format. Wer ein eigenes Modell oder eine eigene Quantisierung einbringen will, findet in der README keinen Hinweis darauf, wie ein Bezeichner registriert oder ein Modell konvertiert wird. Die README nennt als Feature zwar unified model configurations, making it easy to add support for new models, aber der Weg dorthin wird nicht beschrieben.

Immerhin ist der Fehlerfall explizit behandelt. In Rust liefert der Aufruf ein Option, das im Beispiel mit ok_or("Model not found") in einen Fehler verwandelt wird, in TypeScript wird bei fehlendem Modell throw new Error('Model not found') geworfen, in Swift wird mit guard let abgebrochen. Ein falscher Bezeichner führt also zu einem definierten Zustand, nicht zu undefiniertem Verhalten.

Wo uzu an Grenzen stößt

Die wichtigste Einschränkung steht in der README selbst, nur als Feature formuliert: Utilizes unified memory on Apple devices. Unified Memory ist ein Merkmal der Apple-Silicon-Architektur, bei der CPU und GPU denselben Speicher adressieren. Der Vorteil ist, dass Gewichte nicht zwischen Host- und Gerätespeicher kopiert werden müssen. Der Nachteil ist, dass dieses Modell auf anderen Plattformen so nicht existiert. Die README beschreibt nicht, was auf Linux oder Windows passiert, ob dort ein anderer Backend-Pfad greift oder ob die genannten Bindings dort überhaupt bauen. Wer eine Linux-CI betreibt und dort Tests laufen lassen will, hat aus dem vorliegenden Material keine Zusage.

Die zweite Grenze ist der Modellumfang. Das durchgängig verwendete Beispiel hat 0,8 Milliarden Parameter. Die README nennt keinen Speicherbedarf, keine Kontextlänge und keine Durchsatzwerte. Aussagen über die Eignung für größere Modelle lassen sich aus dem Material nicht ableiten.

Die dritte Grenze betrifft den Download-Pfad. Der Quick Start lädt das Modell beim ersten Start über engine.download. Das ist bequem, bedeutet aber, dass die App beim ersten Lauf eine Netzwerkverbindung braucht und dass die Downloadlogik Teil der Anwendung ist. Wer Modelle vorab ausliefern will, etwa im App-Bundle, findet in der README keinen Hinweis auf einen solchen Pfad. Für den Anspruch full data privacy ist das kein Widerspruch, die Inferenz bleibt lokal, aber die Erstbeschaffung der Gewichte läuft über das Netz.

Schließlich fehlt jede Angabe zur Thread- und Speicherkonfiguration. EngineConfig::default() und EngineConfig.create() werden ohne Argumente verwendet. Welche Stellschrauben das Objekt hat, ob Threadanzahl, Speicherlimit oder Backend-Auswahl darin stecken, geht aus der README nicht hervor. Für ein Projekt, das sich über Performance definiert, ist das eine dünne Dokumentationslage an genau der Stelle, an der Performance entsteht.

Was uzu von llama.cpp und ONNX Runtime unterscheidet

Der naheliegende Vergleich ist llama.cpp, das ebenfalls lokale LLM-Inferenz in C++ betreibt und breite Plattformunterstützung sowie eigene Quantisierungsformate mitbringt. Der Unterschied liegt weniger im Rechenkern als in der Schnittstelle. llama.cpp exponiert Modellpfade und Sampler-Parameter direkt, der Nutzer wählt eine GGUF-Datei und stellt die Inferenzparameter selbst ein. uzu geht den umgekehrten Weg: Der Nutzer nennt einen kuratierten Bezeichner, die Engine kümmert sich um Auflösung, Download und Laden. Das senkt die Einstiegshürde und erhöht die Abhängigkeit vom Katalog.

Ein zweiter Unterschied ist die Bindung an eine Sprache. llama.cpp ist C++ mit Bindings, die von der Community gepflegt werden. uzu liefert Rust, Python, Swift und TypeScript aus einem Repository, mit einer gemeinsamen Engine-Semantik. Für ein Team, das eine iOS-App und einen Python-Dienst aus derselben Codebasis bedienen will, ist das ein Argument. Für ein Team, das Java oder Go braucht, ist es ein Ausschlusskriterium, denn diese Bindings existieren im vorliegenden Material nicht.

Der dritte Kandidat ist ONNX Runtime, das kein LLM-spezifisches Projekt ist, sondern ein generischer Graph-Executor für das ONNX-Format. Dort lädt man ein Modell im ONNX-Format und ruft einen Session-Runner mit Tensoren auf. uzu abstrahiert eine Ebene höher: ChatMessage, ChatConfig und ChatReplyConfig setzen voraus, dass das Modell ein Chatmodell ist. Wer ein Klassifikations- oder Embedding-Modell betreiben will, wird mit dieser API nicht glücklich, während ONNX Runtime genau dafür gebaut ist. Die Themenliste nennt tts, was auf weitere Modalitäten hindeutet, aber die README zeigt ausschließlich den Chat-Pfad.

Ein vierter Punkt betrifft die Nachvollziehbarkeit. Die README nennt Traceable computations to ensure correctness against the source-of-truth implementation als Feature. Das ist ein starkes Versprechen, denn es behauptet eine Prüfkette gegen die Referenzimplementierung. Wie diese Traces erzeugt, gespeichert oder ausgewertet werden, steht in der README nicht. Wer Korrektheit belegen muss, sollte das vor der Übernahme klären.

Pflege, Release-Tempo und Lizenz

Die Release-Liste zeigt 0.5.26 vom 6. September 2026, 0.5.25 zwei Tage davor und 0.5.23 am 3. September. Das sind drei Releases innerhalb von vier Tagen, alle in der 0.5-Reihe. Ein solches Tempo bedeutet zweierlei: Das Projekt wird aktiv bearbeitet, und die Patch-Versionen ändern sich schnell. Für Betreiber heißt das, dass ein Pinning auf eine exakte Version sinnvoll ist, und dass die Rust-Einbindung über branch = "main" in diesem Tempo besonders riskant ist.

Der letzte Push liegt auf dem 9. September 2026, drei Tage nach dem jüngsten Release. Der Zeitraum zwischen Release und letztem Push ist kurz, was auf laufende Arbeit hindeutet. Aus diesen Zahlen lässt sich jedoch nichts über Stabilität oder Produktionsreife ableiten. Die Versionsnummer 0.5 sagt lediglich, dass das Projekt sich selbst nicht als 1.0 versteht.

Die Lizenz ist MIT, angegeben im Repository und im Badge der README. MIT ist permissiv: Nutzung, Änderung und Weitergabe sind erlaubt, sofern der Copyright-Hinweis und der Lizenztext beibehalten werden. Für die Einbettung in eine proprietäre App ist das die unkomplizierte Variante, anders als bei copyleft-Lizenzen, die den Quellcode der abgeleiteten Werke offenlegen können. Zwei Punkte bleiben trotzdem offen. Erstens: Die MIT-Lizenz deckt den Code, nicht die Modellgewichte. Die Bezeichner verweisen auf Modelle von Drittanbietern wie alibaba, und deren Lizenzbedingungen sind eine separate Frage, die die README nicht behandelt. Zweitens: Ob das Repository weitere Lizenzen für Teilkomponenten enthält, geht aus dem vorliegenden Material nicht hervor. Das ist keine Rechtsberatung, sondern der Hinweis, dass die Modelllizenz vor einem kommerziellen Einsatz separat geprüft werden muss.

Beim Upgrade-Aufwand ist der Bruch zwischen den Bindings der kritische Punkt. Wenn sich Feldzugriffe zwischen Python und TypeScript einerseits und Swift andererseits unterscheiden, kann eine Umbenennung in einer Version alle vier Bindings gleichzeitig betreffen. Wer mehrere Sprachen einsetzt, sollte die Release Notes jeder Patch-Version lesen, statt automatisch zu aktualisieren.

Redaktionelles Fazit

uzu passt zu Teams, die ein Modell auf Apple-Geräten oder in einer lokalen Python- oder Node-Umgebung betreiben wollen und dafür einen einheitlichen Modellbezeichner sowie einen Engine-Lebenszyklus akzeptieren. Wer CUDA, verteilte Ausführung oder eigene Modellformate ohne Anpassung braucht, ist hier falsch. Vor der Übernahme sollte man den Modellkatalog unter trymirai.com/local-models prüfen, ob der gewünschte Bezeichner wie alibaba:qwen3.5:0.8b:mirai:mirai-m:4 dort auftaucht, und den EngineConfig-Default gegen die eigene Speicher- und Threading-Situation testen.

Offizielle Quellen

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. trymirai/uzu on GitHub
Community-Notizen

Community-Notizen