Vorschau-Umgebung - hier siehst du auch noch nicht freigegebenen Content.
Bauteil 02 / 05Block III · Inferenz

Was in einem Modell-Download steckt

„Lokal betreiben" heißt: ein Ordner mit ein paar Gigabyte darin. Welche Datei welche Aufgabe hat - und warum ausgerechnet die kleinste davon, das Chat-Template, darüber entscheidet, ob dein Modell antwortet oder stottert.

Basics

Für alle frei: das Konzept, die Analogie, das Warum.

Du hast dich für ein Open-Weights-Modell entschieden, den Download gestartet - und bekommst einen Ordner. Ein paar große Dateien, ein Dutzend kleine, zusammen 5, 30 oder 140 Gigabyte. Das war's. Kein Programm, kein Installer, nichts, was von allein etwas tut.

Was in diesem Ordner liegt, entscheidet später darüber, ob du verstehst, warum dasselbe Modell bei dir anders antwortet als beim Anbieter. Also gehen wir ihn durch, Datei für Datei.

Der Ordner, den man bekommt

So sieht ein echtes Repo aus - hier Qwen/Qwen3-8B, aber die Form ist bei fast jedem Hugging-Face-Modell dieselbe:

Qwen3-8B/
├── config.json                              1 KB
├── generation_config.json                   239 B
├── model-00001-of-00005.safetensors         3,9 GB
├── model-00002-of-00005.safetensors         3,9 GB
├── model-00003-of-00005.safetensors         3,9 GB
├── model-00004-of-00005.safetensors         3,7 GB
├── model-00005-of-00005.safetensors         1,1 GB
├── model.safetensors.index.json             36 KB
├── tokenizer.json                           11 MB
├── tokenizer_config.json                    9 KB
├── vocab.json                               2,7 MB
├── merges.txt                               1,6 MB
├── LICENSE                                  11 KB
└── README.md                                18 KB

Zwei Beobachtungen vorweg. Erstens: Fast das gesamte Volumen steckt in fünf Dateien, alles andere ist Text. Zweitens: Der ganze Rest - die paar hundert Kilobyte - ist genau das, was aus einem Sack Zahlen ein benutzbares Modell macht.

config.json - was das Modell ist

Der Bauplan. Hier steht die Architektur, nicht das Gelernte:

{
  "architectures": ["Qwen3ForCausalLM"],
  "hidden_size": 4096,
  "num_hidden_layers": 36,
  "num_attention_heads": 32,
  "num_key_value_heads": 8,
  "vocab_size": 151936,
  "max_position_embeddings": 40960,
  "rope_theta": 1000000.0,
  "torch_dtype": "bfloat16"
}

Die Engine liest diese Datei zuerst. architectures sagt ihr, welchen Modelltyp sie überhaupt bauen soll - steht dort ein Name, den sie nicht kennt, bricht das Laden ab, obwohl die Gewichte tadellos sind. Genau daher kommt die Fehlermeldung „unsupported architecture" bei brandneuen Modellen: Die Engine ist zu alt für den Bauplan, nicht für die Zahlen.

Der Rest sind die Maße: Layer (wie tief), Hidden Size (wie breit), Attention Heads und - der wichtigste Nebensatz - num_key_value_heads. Weniger KV-Heads als Attention-Heads bedeutet einen kleineren KV-Cache und damit mehr gleichzeitige Nutzer auf derselben Karte.

vocab_size ist die Größe des Vokabulars, max_position_embeddings das Kontextfenster, für das trainiert wurde, und rope_theta gehört zu RoPE (Rotary Position Embedding), dem Verfahren, mit dem das Modell überhaupt weiß, an welcher Stelle ein Token steht. An dieser Zahl drehen die Verlängerungstricks, mit denen aus 40 960 Tokens im Datenblatt später 128 000 werden - das ist die zweite, größere Kontextzahl aus der Model Card.

model-*.safetensors - die Gewichte

Das eigentliche Modell: die gelernten Zahlen, aufgeteilt in Schnipsel von jeweils rund 4 GB. Geteilt wird nicht aus Not, sondern aus Vernunft - ein abgebrochener Download kostet dann einen Schnipsel und nicht 16 GB.

Weil die Zahlen über mehrere Dateien verstreut sind, braucht es ein Inhaltsverzeichnis: model.safetensors.index.json listet für jeden Gewichtstensor, in welchem Schnipsel er liegt. Fehlt diese Datei oder ist ein Schnipsel nicht mitgekommen, lädt gar nichts - „alle Teile oder keins".

Der Dateiname trägt außerdem eine kleine Sicherheitsgeschichte mit sich. Früher hießen diese Dateien pytorch_model.bin und waren Python-Pickle: ein Format, das beim Laden nicht nur Daten liest, sondern beliebigen Code ausführen darf. Ein „Modell" herunterzuladen hieß damit, ein Programm auszuführen. safetensors kann das nicht: ein JSON-Kopf mit Namen, Typ und Position jedes Tensors, dahinter rohe Zahlen. Nichts daran ist ausführbar - und nebenbei lädt es schneller, weil die Datei direkt in den Speicher eingeblendet werden kann, statt entpackt zu werden. Wenn dir heute noch ein .bin begegnet, ist das ein Altbestand, kein Feature.

tokenizer.json & Co. - das gelernte Vokabular

Das Modell liest keine Buchstaben, sondern Tokens - dazu gibt es im LLM-Call das Nötige. Welche Textstücke es kennt, ist nichts, was die Serversoftware wüsste: Es ist gelernt und liegt deshalb mit im Download.

  • tokenizer.json - das komplette Vokabular plus die Zerlegungsregeln, in einer einzigen Datei. Rund 11 MB, weil dort 151 936 Einträge stehen.
  • vocab.json und merges.txt - dieselbe Information im älteren, zweiteiligen Format (Vokabular und die gelernten Verschmelzungsregeln des Byte-Pair-Encodings). Viele Repos legen beides bei, damit auch ältere Werkzeuge zurechtkommen.
  • tokenizer_config.json - die Bedienungsanleitung dazu: welche Tokenizer-Klasse, wie lang ein Modell-Input maximal sein darf, welche Sonder-Tokens es gibt. Und - dazu gleich mehr - oft auch das Chat-Template.

Wichtig ist die Konsequenz: Vokabular und Gewichte gehören zusammen. Token Nummer 8 623 bedeutet für dieses Modell ein bestimmtes Textstück, weil es das im Training so gelernt hat. Mischst du den Tokenizer des einen mit den Gewichten des anderen Modells, kommt kein Fehler - es kommt Kauderwelsch.

special_tokens_map.json - woher <|im_start|> kommt

Neben den gelernten Textstücken gibt es eine Handvoll Sonder-Tokens, die keine Sprache sind, sondern Steuerzeichen: Gesprächsanfang, Gesprächsende, Auffüllzeichen. Bei unserem Qwen sind das <|im_start|> und <|im_end|> - dieselben Marker, die im Deep Dive zum LLM-Call im Rohstring auftauchen.

special_tokens_map.json ist die Datei, die festlegt, welches Token welche Rolle spielt: eos_token, bos_token, pad_token. Sie steht nicht in jedem Repo - Qwen etwa schreibt diese Zuordnung direkt in tokenizer_config.json, andere Familien legen die eigene Datei daneben. Beide Wege sind normal; entscheidend ist, dass die Angabe im Download steht und nicht im Server.

Warum das zählt: Das eos_token ist das Signal „ich bin fertig". Kennt die Engine es nicht oder das falsche, hört das Modell nicht auf zu reden und schreibt fröhlich die nächste Nutzerfrage gleich mit.

generation_config.json - die Werkseinstellung

Eine kleine Datei mit den Sampling-Vorgaben, die der Hersteller für sein Modell empfiehlt:

{
  "temperature": 0.6,
  "top_p": 0.95,
  "top_k": 20,
  "eos_token_id": [151645, 151643]
}

Das ist keine Vorschrift, sondern ein Vorschlag - aber ein gut begründeter, denn er stammt von denen, die das Modell nachtrainiert haben. Wer sich wundert, warum dasselbe Modell in zwei Werkzeugen unterschiedlich kreativ wirkt, findet hier oft die Antwort: Manche Engines übernehmen diese Defaults, andere setzen ihre eigenen darüber.

LICENSE und README.md

Die Lizenz sagt, was du mit dem Modell tun darfst - und das ist bei Open Weights keine Formalie, sondern reicht von „Apache-2.0, mach was du willst" bis zu eigenen Lizenzen mit Nutzungsgrenzen. Die README.md ist die Model Card: Beschreibung, Benchmarks, Beispielcode. Wie man sie liest, ohne auf die Werbung hereinzufallen, steht im Bauteil Modelle.

Das Chat-Template liegt mit im Ordner

Jetzt der Punkt, um den es in diesem Kapitel eigentlich geht.

Das Chat-Template ist die Schablone, die aus deinem messages-Array den einen Textstrom macht, den das Modell tatsächlich liest. Es ist der Grund, warum aus {"role": "user", "content": "Hallo"} am Ende so etwas wird:

<|im_start|>user
Hallo<|im_end|>
<|im_start|>assistant

Die naheliegende Annahme wäre, dass diese Umwandlung zur Serversoftware gehört - vLLM oder Ollama müssten doch wissen, wie man einen Chat formatiert. Sie gehört nicht dorthin. Das Template kommt mit dem Modell, und zwar in einer dieser beiden Formen:

  • als eigene Datei chat_template.jinja im Ordner, oder
  • als Feld "chat_template" in der tokenizer_config.json.

Beide Schreibweisen sind im Umlauf; die eigene Datei ist die neuere und setzt sich gerade durch, weil ein mehrzeiliges Template in einer JSON-Zeile schwer zu lesen ist. Inhaltlich ist es dasselbe: ein Jinja-Template, also reine Textverarbeitung. messages-Array rein, ein String raus. Keine Gewichte, keine Rechnung, kein Modell - eine Schablone.

Daraus folgen drei Dinge, die man im Betrieb wiedererkennt:

  • Ein falsches Template macht aus einem guten Modell einen Stotterer. Die API-Schicht sieht dabei völlig unverändert aus: derselbe POST /v1/chat/completions, dieselben Felder, gültiges JSON zurück. Nur der Inhalt der Antwort ist plötzlich abgehackt, wiederholt sich oder hört nicht auf. Wer an dieser Stelle am Prompt schraubt, sucht am falschen Ende.
  • Wer das Template austauscht, ändert das Verhalten des Modells - ohne ein einziges Gewicht anzufassen. Ein zusätzlicher Satz in der Schablone landet bei jeder Anfrage im Kontext. Das ist ein legitimes Werkzeug und zugleich eine Falle, wenn man es vergisst.
  • Bei GGUF steckt das Template in den Metadaten der Datei, nicht daneben - eine .gguf-Datei ist ein Container, der Gewichte, Tokenizer und Template zusammenpackt. Ollama kann es zusätzlich per Modelfile überschreiben. Genau das ist eine der häufigsten Ursachen dafür, dass „dasselbe" Modell in zwei Werkzeugen unterschiedlich antwortet.

Warum es dasselbe Modell in fünf Größen gibt

Wer nach einem Modell sucht, findet selten ein Repo, sondern eine Liste: das Original und ein halbes Dutzend Ableger mit Kürzeln im Namen. Dahinter steckt Quantisierung - die Gewichte werden gröber gespeichert, statt mit 16 Bit pro Zahl nur noch mit 8, 4 oder weniger. Das Modell schrumpft, die Antworten bleiben erstaunlich lange gleich.

Wichtig ist hier nur die Sortierung. Es gibt nicht ein Quantisierungsformat, sondern mehrere, und sie zielen auf verschiedene Hardware:

FormatWofür gedachtWoran du es erkennst
GGUFllama.cpp und alles darauf (Ollama, LM Studio) - Laptop, CPU, Apple Silicon-GGUF im Repo-Namen, Dateien wie …-Q4_K_M.gguf
AWQ / GPTQ4-Bit auf der GPU, für Server-Engines wie vLLM-AWQ, -GPTQ-Int4 im Repo-Namen
FP8 / NVFP4native 4- und 8-Bit-Formate neuerer NVIDIA-Karten-FP8, -NVFP4 im Repo-Namen

Die Formate sind nicht austauschbar: Ein GGUF läuft nicht in vLLM wie ein AWQ, und ein AWQ-Repo bekommst du nicht in Ollama gestartet. Wer vor einer Repo-Liste steht, sortiert deshalb zuerst nach Format (passt es zu meiner Engine?) und erst danach nach Größe. Was die Kürzel Q4_K_M, IQ3 und Q8_0 im Einzelnen bedeuten und wie viel Qualität sie kosten, steht im Bauteil Modelle.

Wer liest was

Damit lässt sich die Ausgangsfrage beantworten. Die drei üblichen Engines lesen unterschiedliche Teile dieses Ordners - und genau daher kommen die Verhaltensunterschiede:

vLLMllama.cppOllama
Gewichte*.safetensors + Indexdie eine .gguf-Dateidie eine .gguf-Datei
Architekturconfig.jsonMetadaten in der GGUFMetadaten in der GGUF
Tokenizertokenizer.jsonin der GGUF eingebackenin der GGUF eingebacken
Chat-Templateaus tokenizer_config.json bzw. chat_template.jinjaaus den GGUF-MetadatenGGUF-Metadaten, überschreibbar per Modelfile
Sampling-Defaultsgeneration_config.json als Vorgabeeigene Defaultseigene Defaults + PARAMETER im Modelfile

Zwei Zeilen dieser Tabelle sind die Erklärung für fast jede „bei mir läuft es anders"-Meldung. Beim Template hängt es daran, ob überhaupt das Template des Modells verwendet wird oder eine Ersatzschablone des Werkzeugs. Und bei den Sampling-Defaults daran, dass die Empfehlung des Herstellers nur von manchen gelesen wird.

Der Ordner ist also nicht nur ein Download. Er ist ein kleines Bündel aus Bauplan, Zahlen, Wörterbuch und Bedienungsanleitung - und jede Engine nimmt sich davon, was sie versteht.

Ein Modell ist kein Programm, sondern ein Ordner: config.json sagt, was es ist, die *.safetensors sind das Gelernte, die Tokenizer-Dateien sind sein Wörterbuch - und das Chat-Template ist Teil des Downloads, nicht Teil des Servers.

Es liegt als chat_template.jinja oder als Feld in tokenizer_config.json (bei GGUF: in den Metadaten der Datei). Wer es tauscht, ändert das Verhalten des Modells, ohne ein Gewicht anzufassen. Und weil vLLM, llama.cpp und Ollama unterschiedliche Teile dieses Ordners lesen, antwortet „dasselbe" Modell in zwei Werkzeugen eben doch verschieden.

Vertiefung

Mit kostenlosem Konto: Experimente, Quizze und die Vertiefung.

Anmelden, um diesen Inhalt zu sehen

Dieser Bereich ist Mitgliedern vorbehalten. Logge dich ein, um weiterzulesen.

Jetzt anmelden

Deep-Dive

Für Pro-Mitglieder: die Tiefe für alle, die es wirklich bauen wollen.

Anmelden, um diesen Inhalt zu sehen

Dieser Bereich ist Mitgliedern vorbehalten. Logge dich ein, um weiterzulesen.

Jetzt anmelden

Diskussion· noch keine Beiträge

Unser Kommentar-Agent liest jeden neuen Beitrag, bedankt sich oder empfiehlt passende Inhalte.

Sei die erste Stimme - was denkst du dazu?

Melde dich an, um mitzudiskutieren.

Anmelden