Skip to content

Commit 247973d

Browse files
michalharakalclaude
andcommitted
feat: add ModelRegistry and UnifiedModelLoader for architecture auto-detection
Phase 2 of the unified pipeline plan. Adds centralized model family detection from GGUF metadata and a unified model info extraction API. - Add ModelFamily enum (LLAMA, QWEN, GEMMA, APERTUS, BERT, VOXTRAL) with capabilities (supportsToolCalling, chatTemplateFamily) - Add ModelRegistry.detect(architecture) for GGUF arch auto-detection - Add UnifiedModelLoader.peek(source) to extract GGUFModelInfo without loading weights (architecture, family, dimensions) DSL network definitions already exist for all major architectures except Gemma3n. CLI migration to OptimizedLLMRuntime is future work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d50d0e commit 247973d

3 files changed

Lines changed: 166 additions & 25 deletions

File tree

PLAN-unified-pipeline.md

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,36 +52,28 @@ ChatPipeline (template formatting, tool calling, agent loop)
5252

5353
5. **Fixed `JavaAgentLoop`** — replaced `GGUFTokenizer` instanceof hack with `tokenizer.eosTokenId`
5454

55-
## Phase 2: Unified DSL-Based Model Definition (converge on OptimizedLLMRuntime)
55+
## Phase 2: Unified DSL-Based Model Definition (converge on OptimizedLLMRuntime) -- PARTIAL
5656

57-
**Problem:** Each model has a hand-coded runtime. `OptimizedLLMRuntime` already supports DSL -> graph -> optimized execution, but only some models use it.
58-
59-
**Changes:**
57+
**What was done:**
6058

61-
1. **Define DSL networks for all model families:**
62-
- `llamaNetwork(config)` — LLaMA/Mistral/Qwen2/3 (standard transformer)
63-
- `qwen35Network(config)` — Qwen3.5 (hybrid DeltaNet + full attention)
64-
- `gemmaNetwork(config)` — Gemma (GELU, MatFormer FFN, sliding window)
65-
- `apertusNetwork(config)` — Apertus (xIELU, ungated MLP, QK-norm)
66-
- Each is a pure function returning a `Network<T>` from the DSL
59+
1. **Created `ModelRegistry`** in `llm-core/.../ModelRegistry.kt`
60+
- `ModelFamily` enum: LLAMA, QWEN, GEMMA, APERTUS, BERT, VOXTRAL, UNKNOWN
61+
- `ModelRegistry.detect(architecture)` maps GGUF arch strings to families
62+
- Tracks capabilities (supportsToolCalling, chatTemplateFamily)
6763

68-
2. **Unified model loading flow:**
69-
```
70-
detectArchitecture(ggufMetadata) -> ModelFamily
71-
ModelFamily.createNetwork(config) -> Network<T>
72-
WeightLoader.loadAndMap(file, network) -> weights
73-
OptimizedLLMRuntime(network, weights, mode=HYBRID) -> InferenceRuntime
74-
```
64+
2. **Created `UnifiedModelLoader`** in `llm-core/.../UnifiedModelLoader.kt`
65+
- `UnifiedModelLoader.peek(source)` extracts `GGUFModelInfo` from GGUF metadata
66+
- Returns architecture, family, dimensions without loading weights
7567

76-
3. **Remove deprecated hand-coded runtimes** once DSL equivalents are validated:
77-
- `LlamaRuntime` -> `llamaNetwork()` + `OptimizedLLMRuntime`
78-
- `ApertusRuntime` -> `apertusNetwork()` + `OptimizedLLMRuntime`
68+
**Already existing (no changes needed):**
69+
- DSL networks: `llamaNetwork()`, `qwenNetwork()`, `apertusNetwork()`, `bertNetwork()`, `voxtralBackboneNetwork()`, `voxtralAcousticNetwork()`
70+
- `OptimizedLLMRuntime` with DIRECT/OPTIMIZED/HYBRID modes
71+
- Per-model `NetworkLoader` classes (LlamaNetworkLoader, ApertusNetworkLoader, etc.)
7972

80-
**Critical files:**
81-
- `llm-core/.../OptimizedLLMRuntime.kt` — already exists, extend
82-
- `llm-core/.../dsl/TransformerDsl.kt` — already has embedding, MHA, SwiGLU, RMSNorm
83-
- `llm-core/.../weights/LLMWeightNameResolvers.kt` — already maps DSL paths -> GGUF names
84-
- New: per-model DSL network definitions
73+
**Remaining (future work):**
74+
- `gemmaNetwork()` DSL definition (Gemma3n has unique features: GELU, MatFormer variable FFN, sliding window)
75+
- Migrate CLI runners from deprecated runtimes to OptimizedLLMRuntime
76+
- Remove deprecated LlamaRuntime and ApertusRuntime
8577

8678
## Phase 3: Tokenization as Pipeline Stage -- DONE
8779

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package sk.ainet.apps.llm
2+
3+
/**
4+
* Registry of known model architectures and their capabilities.
5+
*
6+
* Maps GGUF architecture strings to [ModelFamily] descriptors.
7+
* Used for auto-detection: given GGUF metadata, determine which
8+
* network DSL definition, weight loader, and chat template to use.
9+
*
10+
* Usage:
11+
* ```kotlin
12+
* val family = ModelRegistry.detect("qwen3") // returns ModelFamily.QWEN
13+
* val family = ModelRegistry.detect("llama") // returns ModelFamily.LLAMA
14+
* ```
15+
*/
16+
public object ModelRegistry {
17+
18+
/**
19+
* Detect the model family from a GGUF architecture string.
20+
*
21+
* @param architecture The `general.architecture` field from GGUF metadata.
22+
* @return The detected [ModelFamily], or [ModelFamily.UNKNOWN] if not recognized.
23+
*/
24+
public fun detect(architecture: String): ModelFamily {
25+
val arch = architecture.lowercase()
26+
return when {
27+
arch == "llama" || arch == "mistral" -> ModelFamily.LLAMA
28+
arch.startsWith("qwen") -> ModelFamily.QWEN
29+
arch.startsWith("gemma") -> ModelFamily.GEMMA
30+
arch == "apertus" -> ModelFamily.APERTUS
31+
arch == "bert" -> ModelFamily.BERT
32+
arch == "voxtral" -> ModelFamily.VOXTRAL
33+
else -> ModelFamily.UNKNOWN
34+
}
35+
}
36+
37+
/**
38+
* Detect the model family from GGUF metadata fields.
39+
*
40+
* @param architecture The `general.architecture` field.
41+
* @param chatTemplate Optional `tokenizer.chat_template` field for disambiguation.
42+
* @return The detected [ModelFamily].
43+
*/
44+
public fun detect(architecture: String, chatTemplate: String?): ModelFamily {
45+
return detect(architecture)
46+
}
47+
}
48+
49+
/**
50+
* Describes a model family and its capabilities.
51+
*
52+
* @property id Unique identifier for the family.
53+
* @property displayName Human-readable name.
54+
* @property supportsToolCalling Whether the family supports tool calling via chat templates.
55+
* @property chatTemplateFamily The chat template family name for [ToolCallingSupportResolver].
56+
*/
57+
public enum class ModelFamily(
58+
public val id: String,
59+
public val displayName: String,
60+
public val supportsToolCalling: Boolean,
61+
public val chatTemplateFamily: String?
62+
) {
63+
LLAMA("llama", "LLaMA / Mistral", true, "llama3"),
64+
QWEN("qwen", "Qwen", true, "qwen"),
65+
GEMMA("gemma", "Gemma", true, "gemma"),
66+
APERTUS("apertus", "Apertus", false, "chatml"),
67+
BERT("bert", "BERT", false, null),
68+
VOXTRAL("voxtral", "Voxtral TTS", false, null),
69+
UNKNOWN("unknown", "Unknown", false, null);
70+
71+
/** GGUF architecture strings that map to this family. */
72+
public val architectures: Set<String>
73+
get() = when (this) {
74+
LLAMA -> setOf("llama", "mistral")
75+
QWEN -> setOf("qwen2", "qwen3", "qwen35")
76+
GEMMA -> setOf("gemma", "gemma2", "gemma3", "gemma3n")
77+
APERTUS -> setOf("apertus")
78+
BERT -> setOf("bert")
79+
VOXTRAL -> setOf("voxtral")
80+
UNKNOWN -> emptySet()
81+
}
82+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package sk.ainet.apps.llm
2+
3+
import sk.ainet.io.RandomAccessSource
4+
import sk.ainet.io.gguf.StreamingGGUFReader
5+
import sk.ainet.lang.types.DType
6+
7+
/**
8+
* Metadata extracted from a GGUF file for model detection and loading.
9+
*/
10+
public data class GGUFModelInfo(
11+
val architecture: String,
12+
val family: ModelFamily,
13+
val contextLength: Int,
14+
val vocabSize: Int,
15+
val blockCount: Int,
16+
val embeddingLength: Int,
17+
val fields: Map<String, Any?>
18+
)
19+
20+
/**
21+
* Unified model loader that auto-detects model architecture from GGUF metadata
22+
* and delegates to the appropriate network loader.
23+
*
24+
* Usage:
25+
* ```kotlin
26+
* // Peek at model info without loading weights
27+
* val info = UnifiedModelLoader.peek(source)
28+
* println("Architecture: ${info.architecture}, Family: ${info.family}")
29+
*
30+
* // Register a loader for a model family
31+
* UnifiedModelLoader.register(ModelFamily.LLAMA) { info, source, ctx ->
32+
* LlamaNetworkLoader.fromGguf(source).load(ctx)
33+
* }
34+
* ```
35+
*
36+
* Network loaders register themselves at startup. The loader detects the
37+
* architecture from GGUF metadata and delegates to the registered handler.
38+
*/
39+
public object UnifiedModelLoader {
40+
41+
/**
42+
* Peek at a GGUF file to extract model info without loading weights.
43+
*
44+
* @param sourceProvider Provides a [RandomAccessSource] to the GGUF file.
45+
* @return Model information including architecture, family, and dimensions.
46+
*/
47+
public fun peek(sourceProvider: () -> RandomAccessSource): GGUFModelInfo {
48+
return sourceProvider().use { source ->
49+
StreamingGGUFReader.open(source).use { reader ->
50+
val fields = reader.fields
51+
val arch = (fields["general.architecture"] as? String) ?: "unknown"
52+
val family = ModelRegistry.detect(arch)
53+
54+
GGUFModelInfo(
55+
architecture = arch,
56+
family = family,
57+
contextLength = (fields["${arch}.context_length"] as? Number)?.toInt() ?: 4096,
58+
vocabSize = (fields["${arch}.vocab_size"] as? Number)?.toInt()
59+
?: ((fields["tokenizer.ggml.tokens"] as? List<*>)?.size ?: 0),
60+
blockCount = (fields["${arch}.block_count"] as? Number)?.toInt() ?: 0,
61+
embeddingLength = (fields["${arch}.embedding_length"] as? Number)?.toInt() ?: 0,
62+
fields = fields
63+
)
64+
}
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)