11package sk.ainet.apps.kgemma
22
3- import kotlin.math.exp
43import kotlin.random.Random
4+ import sk.ainet.apps.llm.DecoderRuntime
55import sk.ainet.context.ExecutionContext
6- import sk.ainet.io.gguf.gemma.Gemma3nLayerWeights
76import sk.ainet.io.gguf.gemma.Gemma3nRuntimeWeights
87import sk.ainet.lang.nn.layers.Embedding
98import sk.ainet.lang.tensor.Tensor
@@ -12,7 +11,6 @@ import sk.ainet.lang.tensor.plus
1211import sk.ainet.lang.tensor.t
1312import sk.ainet.lang.tensor.times
1413import sk.ainet.lang.nn.normalization.RMSNormalization
15- import sk.ainet.lang.tensor.data.FloatArrayTensorData
1614import sk.ainet.lang.types.DType
1715import kotlin.reflect.KClass
1816
@@ -25,9 +23,7 @@ import kotlin.reflect.KClass
2523 * - Hybrid attention (local sliding-window + global)
2624 * - Per-layer embeddings (optional)
2725 *
28- * The attention strategy (CPU vs GPU) is injected via [AttentionBackend].
29- * All other logic (embedding, norms, projections, FFN, sampling, generate loop)
30- * is shared.
26+ * Extends [DecoderRuntime] for shared forward/generate/sample logic.
3127 *
3228 * @param ctx ExecutionContext for tensor operations
3329 * @param weights Gemma 3n model weights
@@ -44,18 +40,19 @@ public class Gemma3nRuntime<T : DType>(
4440 private val dtype : KClass <T >,
4541 private val config : Gemma3nConfig ,
4642 private val eps : Float = 1e-6f ,
47- private val random : Random = Random .Default
48- ) {
43+ random : Random = Random .Default
44+ ) : DecoderRuntime<T>(random) {
4945
5046 private companion object {
5147 const val BOS_TOKEN : Int = 2 // Gemma uses 2 as BOS
5248 }
5349
54- private val dim = config.hiddenSize
55- private val seqLen = weights.metadata.contextLength
56- private val vocabSize = weights.metadata.vocabSize
57-
58- private var position: Int = 0
50+ // ---- DecoderRuntime abstract properties ----
51+ override val dim: Int = config.hiddenSize
52+ override val seqLen: Int = weights.metadata.contextLength
53+ override val vocabSize: Int = weights.metadata.vocabSize
54+ override val nLayers: Int = weights.layers.size
55+ override val bosToken: Int = BOS_TOKEN
5956
6057 private val embedding = Embedding (
6158 numEmbeddings = vocabSize,
@@ -64,7 +61,7 @@ public class Gemma3nRuntime<T : DType>(
6461 name = " token_embd"
6562 )
6663
67- private val finalNorm = RMSNormalization <T , Float >(
64+ private val finalNormLayer = RMSNormalization <T , Float >(
6865 normalizedShape = intArrayOf(dim),
6966 eps = eps.toDouble(),
7067 name = " final_norm" ,
@@ -92,73 +89,13 @@ public class Gemma3nRuntime<T : DType>(
9289 public val currentPosition: Int
9390 get() = position
9491
95- public fun reset () {
96- attentionBackend.reset()
97- position = 0
98- }
99-
100- /* *
101- * Perform a single forward pass for one token.
102- *
103- * @param tokenId Input token ID
104- * @return Logits tensor [1, vocabSize]
105- */
106- public fun forward (tokenId : Int ): Tensor <T , Float > {
107- require(position < seqLen) { " Context length exceeded: pos=$position seqLen=$seqLen " }
108-
109- var x: Tensor <T , Float > = embedding.forward(intArrayOf(tokenId), ctx)
110-
111- weights.layers.forEachIndexed { layerIdx, layer ->
112- x = runLayer(layerIdx, layer, x)
113- }
114-
115- val norm = finalNorm.forward(x, ctx)
116- val logits = norm.matmul(weights.lmHead.t())
92+ // ---- DecoderRuntime template methods ----
11793
118- position++
119- return logits
120- }
121-
122- /* *
123- * Generate tokens autoregressively.
124- *
125- * @param prompt Initial prompt as token IDs
126- * @param steps Maximum number of tokens to generate
127- * @param temperature Sampling temperature (0 = greedy)
128- * @param onToken Callback for each generated token
129- */
130- public fun generate (prompt : IntArray , steps : Int , temperature : Float , onToken : (Int ) -> Unit ) {
131- require(steps > 0 ) { " steps must be > 0" }
132-
133- val fullPrompt = if (prompt.isNotEmpty() && prompt[0 ] != BOS_TOKEN ) {
134- intArrayOf(BOS_TOKEN ) + prompt
135- } else if (prompt.isEmpty()) {
136- intArrayOf(BOS_TOKEN )
137- } else {
138- prompt
139- }
140-
141- var token = fullPrompt[0 ]
142- var pos = 0
143- var generatedCount = 0
144- while (generatedCount < steps) {
145- val logits = forward(token)
146- val next = if (pos + 1 < fullPrompt.size) {
147- fullPrompt[pos + 1 ]
148- } else {
149- sample(logits, temperature)
150- }
151- if (pos + 1 >= fullPrompt.size) {
152- onToken(next)
153- generatedCount++
154- }
155- token = next
156- pos++
157- }
158- }
94+ override fun embedToken (tokenId : Int ): Tensor <T , Float > =
95+ embedding.forward(intArrayOf(tokenId), ctx)
15996
160- private fun runLayer (layerIdx : Int , layer : Gemma3nLayerWeights < T >, input : Tensor <T , Float >): Tensor <T , Float > {
161- var x = input
97+ override fun runLayer (layerIdx : Int , x : Tensor <T , Float >): Tensor <T , Float > {
98+ val layer = weights.layers[layerIdx]
16299
163100 // Pre-attention normalization
164101 val attnNorm = inputLayernorms[layerIdx].forward(x, ctx)
@@ -185,6 +122,18 @@ public class Gemma3nRuntime<T : DType>(
185122 return afterAttn + ffnOut
186123 }
187124
125+ override fun outputNorm (x : Tensor <T , Float >): Tensor <T , Float > =
126+ finalNormLayer.forward(x, ctx)
127+
128+ override fun outputProject (x : Tensor <T , Float >): Tensor <T , Float > =
129+ x.matmul(weights.lmHead.t())
130+
131+ override fun resetState () {
132+ attentionBackend.reset()
133+ }
134+
135+ // ---- Gemma-specific: GELU activation ----
136+
188137 /* *
189138 * Apply GELU (Gaussian Error Linear Unit) activation.
190139 * Gemma uses GELU instead of SiLU.
@@ -194,7 +143,6 @@ public class Gemma3nRuntime<T : DType>(
194143 val out = FloatArray (buf.size)
195144
196145 // GELU approximation: x * 0.5 * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
197- // Using the exact formulation for better accuracy
198146 val sqrtTwoPi = 0.7978845608028654f // sqrt(2/pi)
199147 val c = 0.044715f
200148
@@ -208,51 +156,4 @@ public class Gemma3nRuntime<T : DType>(
208156
209157 return ctx.fromFloatArray(this .shape, dtype, out )
210158 }
211-
212- private fun sample (logits : Tensor <T , Float >, temperature : Float ): Int {
213- val buf = logits.expectFloatBuffer()
214-
215- if (temperature <= 1e- 6f ) {
216- // Greedy decoding
217- var best = 0
218- var bestVal = buf[0 ]
219- for (i in 1 until buf.size) {
220- if (buf[i] > bestVal) {
221- bestVal = buf[i]
222- best = i
223- }
224- }
225- return best
226- }
227-
228- // Temperature sampling
229- val scaled = FloatArray (buf.size)
230- var maxLogit = Float .NEGATIVE_INFINITY
231- for (i in buf.indices) {
232- val v = buf[i] / temperature
233- scaled[i] = v
234- if (v > maxLogit) maxLogit = v
235- }
236-
237- var sum = 0f
238- for (i in scaled.indices) {
239- val e = exp((scaled[i] - maxLogit).toDouble()).toFloat()
240- scaled[i] = e
241- sum + = e
242- }
243-
244- val r = random.nextFloat() * sum
245- var acc = 0f
246- for (i in scaled.indices) {
247- acc + = scaled[i]
248- if (acc >= r) return i
249- }
250- return scaled.lastIndex
251- }
252-
253- private fun Tensor <T , Float >.expectFloatBuffer (): FloatArray {
254- val data = this .data
255- if (data is FloatArrayTensorData <* >) return data.buffer
256- return data.copyToFloatArray()
257- }
258159}
0 commit comments