diff --git a/src/main/java/org/beehive/gpullama3/inference/InferenceCore.java b/src/main/java/org/beehive/gpullama3/inference/InferenceCore.java index 25e46972..b68e951e 100644 --- a/src/main/java/org/beehive/gpullama3/inference/InferenceCore.java +++ b/src/main/java/org/beehive/gpullama3/inference/InferenceCore.java @@ -1,13 +1,13 @@ package org.beehive.gpullama3.inference; import org.beehive.gpullama3.auxiliary.Parallel; +import org.beehive.gpullama3.inference.state.Qwen2MoEState; +import org.beehive.gpullama3.inference.weights.standard.*; +import org.beehive.gpullama3.model.qwen2.Qwen2MoE; +import org.beehive.gpullama3.model.qwen2.Qwen2MoEConfiguration; import org.beehive.gpullama3.tensor.standard.FloatTensor; import org.beehive.gpullama3.inference.state.Phi3State; import org.beehive.gpullama3.inference.state.State; -import org.beehive.gpullama3.inference.weights.standard.Phi3StandardWeights; -import org.beehive.gpullama3.inference.weights.standard.Qwen2StandardWeights; -import org.beehive.gpullama3.inference.weights.standard.Qwen3StandardWeights; -import org.beehive.gpullama3.inference.weights.standard.StandardWeights; import org.beehive.gpullama3.inference.weights.tornado.TornadoWeights; import org.beehive.gpullama3.model.Configuration; import org.beehive.gpullama3.model.Model; @@ -260,6 +260,177 @@ public static FloatTensor forwardJavaDevstral(Model model, State state, int toke return state.logits; } + public static FloatTensor forwardJavaQwen2MoE(Model model, State state, int token, int position) { + final Qwen2MoEConfiguration config = (Qwen2MoEConfiguration) model.configuration(); + final Qwen2MoEStandardWeights weights = (Qwen2MoEStandardWeights) model.weights(); + final Qwen2MoEState moeState = (Qwen2MoEState) state; + int dim = config.dim(); + int headSize = config.headSize(); + int kvDim = (config.dim() * config.numberOfKeyValueHeads()) / config.numberOfHeads(); + int kvMul = config.numberOfHeads() / config.numberOfKeyValueHeads(); // integer multiplier of the kv sharing in multiquery + float sqrtHeadSize = (float) Math.sqrt(headSize); + + weights.token_embedding_table.copyTo(token * dim, state.x, 0, dim); + + // forward all the layers + for (int l = 0; l < config.numberOfLayers(); l++) { + // attention rmsnorm + final int curLayer = l; + rmsnorm(state.xb, state.x, weights.rms_att_weight[curLayer], 0, dim, config.rmsNormEps()); + + // qkv matmuls for this position + weights.wq[l].matmul(state.xb, state.q, dim, dim); + weights.wk[l].matmul(state.xb, state.k, kvDim, dim); + weights.wv[l].matmul(state.xb, state.v, kvDim, dim); + + // qkv additions with qkv bias + state.q.addInPlace(weights.q_bias[curLayer]); + state.k.addInPlace(weights.k_bias[curLayer]); + state.v.addInPlace(weights.v_bias[curLayer]); + + // RoPE relative positional encoding: complex-valued rotate q and k in each head + // GPT-NeoX style RoPE, real/imaginary components are stored with a headSize/2 offset per head, instead of consecutive. + for (int h = 0; h < config.numberOfHeads(); ++h) { + int rotn = h < config.numberOfKeyValueHeads() ? 2 : 1; // how many vectors? 2 = q & k, 1 = q only + int poffset = h * headSize; + for (int i0 = 0; i0 < headSize; i0 += 2) { + int ic = i0 / 2; + float fcr = weights.freq_cis_real.getFloat((position) * (headSize / 2) + ic); + float fci = weights.freq_cis_imag.getFloat((position) * (headSize / 2) + ic); + for (int vi = 0; vi < rotn; vi++) { + FloatTensor vec = (vi == 0) ? state.q : state.k; // the vector to rotate (query or key) + float v0 = vec.getFloat(poffset + ic); + float v1 = vec.getFloat(poffset + ic + headSize / 2); + vec.setFloat(poffset + ic, v0 * fcr - v1 * fci); + vec.setFloat(poffset + ic + headSize / 2, v0 * fci + v1 * fcr); + } + } + } + + // save key,value at this time step (position) to our kv cache + //int loff = l * config.seq_len * kvDim; // kv cache layer offset for convenience + state.k.copyTo(0, state.keyCache[curLayer], position * kvDim, kvDim); + state.v.copyTo(0, state.valueCache[curLayer], position * kvDim, kvDim); + + // multihead attention. iterate over all heads + Parallel.parallelFor(0, config.numberOfHeads(), h -> { + // get the query vector for this head + // float* q = s.q + h * headSize; + int qOffset = h * headSize; + + // attention scores for this head + // float* att = s.att + h * config.seq_len; + int attOffset = h * config.contextLength(); + + // iterate over all timesteps, including the current one + for (int t = 0; t <= position; t++) { + // get the key vector for this head and at this timestep + // float* k = s.key_cache + loff + t * dim + h * headSize; + int keyCacheOffset = /* loff + */ t * kvDim + (h / kvMul) * headSize; + // calculate the attention score as the dot product of q and k + float score = state.q.dot(qOffset, state.keyCache[curLayer], keyCacheOffset, headSize); + score /= sqrtHeadSize; + // save the score to the attention buffer + state.att.setFloat(attOffset + t, score); + } + + // softmax the scores to get attention weights, from 0..position inclusively + state.att.softmaxInPlace(attOffset, position + 1); + + // weighted sum of the values, store back into xb + // float* xb = s.xb + h * headSize; + int xbOffset = h * headSize; + // memset(xb, 0, headSize * sizeof(float)); + state.xb.fillInPlace(xbOffset, headSize, 0f); + + for (int t = 0; t <= position; t++) { + // get the value vector for this head and at this timestep + // float* v = s.value_cache + loff + t * dim + h * headSize;C + int vOffset = /* loff + */ t * kvDim + (h / kvMul) * headSize; + // get the attention weight for this timestep + float a = state.att.getFloat(attOffset + t); + // accumulate the weighted value into xb + state.xb.saxpyInPlace(xbOffset, state.valueCache[curLayer], vOffset, headSize, a); + } + }); + + // final matmul to get the output of the attention + weights.wo[l].matmul(state.xb, state.xb2, dim, dim); + + // residual connection back into x + state.x.addInPlace(state.xb2); + + // MoE FFN pre-normalization + rmsnorm(state.xb, state.x, weights.rms_ffn_weight[curLayer], 0, dim, config.rmsNormEps()); + + int numberOfExperts = config.numberOfExperts(); + int topK = config.numberOfExpertsUsed(); + int expertHiddenDim = config.moeHiddenDim(); + + // Compute routing probabilities over all experts, then select top-k. + // Qwen1.5-MoE uses norm_topk_prob=false: each selected expert's routing weight is + // its probability over all experts without rescaling the top-k weights to sum to one. + weights.routerGate[l].matmul(state.xb, moeState.routerLogits, numberOfExperts, dim); + moeState.routerLogits.softmaxInPlace(0, numberOfExperts); + + int[] selectedExperts = new int[topK]; + float[] routingWeights = new float[topK]; + for (int i = 0; i < topK; i++) { + float best = Float.NEGATIVE_INFINITY; + int index = -1; + for (int j = 0; j < numberOfExperts; j++) { + if (moeState.routerLogits.getFloat(j) > best) { + best = moeState.routerLogits.getFloat(j); + index = j; + } + } + selectedExperts[i] = index; + routingWeights[i] = best; + moeState.routerLogits.setFloat(index, Float.NEGATIVE_INFINITY); + } + // Compute each selected expert and accumulate its weighted output. + for (int j = 0; j < topK; j++) { + int expert = selectedExperts[j]; + int gateUpOffset = expert * expertHiddenDim * dim; + int downOffset = expert * dim * expertHiddenDim; + matmulExpert(weights.gateExps[l], gateUpOffset, state.xb, moeState.hbE, expertHiddenDim, dim); + matmulExpert(weights.upExps[l], gateUpOffset, state.xb, moeState.hbE2, expertHiddenDim, dim); + moeState.hbE.mapInPlace(v -> v / (float) (1.0 + Math.exp(-v))); + moeState.hbE.multiplyInPlace(moeState.hbE2); + matmulExpert(weights.downExps[l], downOffset, moeState.hbE, moeState.yTmp, dim, expertHiddenDim); + state.x.saxpyInPlace(0, moeState.yTmp, 0, dim, routingWeights[j]); + } + + // Compute the always-on shared expert. + int sharedExpertHiddenDim = config.sharedExpertHiddenDim(); + weights.sharedGate[l].matmul(state.xb, moeState.hbS, sharedExpertHiddenDim, dim); + weights.sharedUp[l].matmul(state.xb, moeState.hbS2, sharedExpertHiddenDim, dim); + moeState.hbS.mapInPlace(v -> v / (float) (1.0 + Math.exp(-v))); + moeState.hbS.multiplyInPlace(moeState.hbS2); + weights.sharedDown[l].matmul(moeState.hbS, moeState.yTmp, dim, sharedExpertHiddenDim); + + // Gate the shared expert output. + float gateScore = weights.sharedGateInp[l].dot(0, state.xb, 0, dim); + float sharedExpertWeight = 1f / (1f + (float) Math.exp(-gateScore)); + state.x.saxpyInPlace(0, moeState.yTmp, 0, dim, sharedExpertWeight); + } + + // final rmsnorm + classifier (same as dense Qwen2) + rmsnorm(state.x, state.x, weights.rms_final_weight, 0, dim, config.rmsNormEps()); + weights.wcls.matmul(state.x, state.logits, config.vocabularySize(), dim); + + return state.logits; + } + + /** + * Like {@link FloatTensor#matmul}, but reads the weight matrix starting at element + * offset {@code base} instead of 0 — so it can target ONE expert inside a stacked + * {@code [nExpert × d0 × d1]} tensor. out[d0] = (d0×d1 sub-matrix at base) · in[d1]. + */ + private static void matmulExpert(FloatTensor w, int base, FloatTensor in, FloatTensor out, int d0, int d1) { + Parallel.parallelFor(0, d0, i -> out.setFloat(i, w.dot(base + i * d1, in, 0, d1))); + } + public static FloatTensor forwardJavaQwen2(Model model, State state, int token, int position) { final Qwen2Configuration config = (Qwen2Configuration) model.configuration(); final Qwen2StandardWeights weights = (Qwen2StandardWeights) model.weights(); diff --git a/src/main/java/org/beehive/gpullama3/inference/state/Qwen2MoEState.java b/src/main/java/org/beehive/gpullama3/inference/state/Qwen2MoEState.java new file mode 100644 index 00000000..d0872883 --- /dev/null +++ b/src/main/java/org/beehive/gpullama3/inference/state/Qwen2MoEState.java @@ -0,0 +1,119 @@ +package org.beehive.gpullama3.inference.state; + +import org.beehive.gpullama3.model.Configuration; +import org.beehive.gpullama3.model.qwen2.Qwen2MoEConfiguration; +import org.beehive.gpullama3.tensor.standard.ArrayFloatTensor; +import org.beehive.gpullama3.tensor.standard.FloatTensor; +import uk.ac.manchester.tornado.api.types.arrays.FloatArray; +import uk.ac.manchester.tornado.api.types.arrays.HalfFloatArray; +import uk.ac.manchester.tornado.api.types.arrays.IntArray; + +import java.util.stream.Stream; + +public class Qwen2MoEState extends Qwen2State { + + // Router output scores, one per expert (length = numberOfExperts). + // Used to pick the top-k experts for the current token. + public final FloatTensor routerLogits; + + // Scratch buffers for a single routed expert's internal FFN (length = moeHiddenDim). + // hbE holds gate_proj(xb), hbE2 holds up_proj(xb); combined via silu(hbE) * hbE2. + public final FloatTensor hbE; + public final FloatTensor hbE2; + + // Scratch buffers for the shared expert's internal FFN (length = sharedExpertHiddenDim). + // Same role as hbE/hbE2, but for the always-on shared expert instead of a routed one. + public final FloatTensor hbS; + public final FloatTensor hbS2; + + // Temporary holder for a single expert's down-projected output (length = dim), + // before it is weighted and accumulated into the residual stream (state.x). + public final FloatTensor yTmp; + + // TornadoVM buffers for the single-token GPU MoE path. These are deliberately + // separate from the CPU FloatTensor fields above: TaskGraph kernels operate on + // TornadoVM arrays that can remain resident on the device between tasks. + public final FloatArray wrapRouterLogits; + public final IntArray wrapSelectedExperts; + public final FloatArray wrapRoutingWeights; + public final FloatArray wrapExpertGate; + public final FloatArray wrapSharedGate; + public final FloatArray wrapSharedOutput; + + public Qwen2MoEState(Configuration config, int batchsize) { + super(config, batchsize); + Qwen2MoEConfiguration c = (Qwen2MoEConfiguration) config; + this.routerLogits = ArrayFloatTensor.allocate(c.numberOfExperts()); + this.hbE = ArrayFloatTensor.allocate(c.moeHiddenDim()); + this.hbE2 = ArrayFloatTensor.allocate(c.moeHiddenDim()); + this.hbS = ArrayFloatTensor.allocate(c.sharedExpertHiddenDim()); + this.hbS2 = ArrayFloatTensor.allocate(c.sharedExpertHiddenDim()); + this.yTmp = ArrayFloatTensor.allocate(c.dim()); + + this.wrapRouterLogits = new FloatArray(c.numberOfExperts()); + this.wrapSelectedExperts = new IntArray(c.numberOfExpertsUsed()); + this.wrapRoutingWeights = new FloatArray(c.numberOfExpertsUsed()); + this.wrapExpertGate = new FloatArray(c.moeHiddenDim()); + this.wrapSharedGate = new FloatArray(c.sharedExpertHiddenDim()); + this.wrapSharedOutput = new FloatArray(c.dim()); + } + + @Override + protected StateFields createStateFields(Configuration configuration) { + StateFields fields = new StateFields(); + + Qwen2MoEConfiguration config = (Qwen2MoEConfiguration) configuration; + + int nEmbdGqa = config.kvDim(); + + fields.x = ArrayFloatTensor.allocate(config.dim()); + fields.xb = ArrayFloatTensor.allocate(config.dim()); + fields.xb2 = ArrayFloatTensor.allocate(config.dim()); + fields.hb = ArrayFloatTensor.allocate(config.hiddenDim()); + fields.hb2 = ArrayFloatTensor.allocate(config.hiddenDim()); + fields.q = ArrayFloatTensor.allocate(config.dim()); + fields.k = ArrayFloatTensor.allocate(config.kvDim()); + fields.v = ArrayFloatTensor.allocate(config.kvDim()); + fields.att = ArrayFloatTensor.allocate(config.numberOfHeads(), config.contextLength()); + fields.logits = ArrayFloatTensor.allocate(config.vocabularySize()); + + fields.keyCache = Stream.generate(() -> ArrayFloatTensor.allocate(config.contextLength(), nEmbdGqa)) + .limit(config.numberOfLayers()) + .toArray(FloatTensor[]::new); + fields.valueCache = Stream.generate(() -> ArrayFloatTensor.allocate(config.contextLength(), nEmbdGqa)) + .limit(config.numberOfLayers()) + .toArray(FloatTensor[]::new); + + switch (config.quantization()) { + case "FP16" -> fields.createActivationFP16(config.dim()); + case "Q8_0" -> fields.createActivationQ8_0(config.dim()); + default -> throw new UnsupportedOperationException("Unsupported quantization format: " + config.quantization()); + } + fields.wrapX = new FloatArray(config.dim()); + fields.wrapXb = new FloatArray(config.dim()); + fields.wrapXbFP16 = new HalfFloatArray(config.dim()); + fields.wrapXb2 = new FloatArray(config.dim()); + fields.wrapHb = new FloatArray(config.hiddenDim()); + fields.wrapHb2 = new FloatArray(config.hiddenDim()); + + fields.wrapLogits = new FloatArray(config.vocabularySize()); + fields.wrapQ = new FloatArray(config.dim()); + fields.wrapK = new FloatArray(config.kvDim()); + fields.wrapV = new FloatArray(config.kvDim()); + + fields.wrapKeyCache = new FloatArray(config.contextLength() * nEmbdGqa * config.numberOfLayers()); + fields.wrapValueCache = new FloatArray(config.contextLength() * nEmbdGqa * config.numberOfLayers()); + fields.wrapValueCache.init(0.f); + fields.wrapKeyCache.init(0.f); + fields.wrapAtt = new FloatArray(config.numberOfHeads() * config.contextLength()); + fields.positionHolder = new IntArray(1); + + // State invokes this override before the Qwen2State constructor body runs, + // so use the Qwen2 work-group size directly instead of State.localSize. + fields.temp = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); + fields.tempFFN = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); + fields.tempLogits = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); + + return fields; + } +} diff --git a/src/main/java/org/beehive/gpullama3/inference/state/Qwen2State.java b/src/main/java/org/beehive/gpullama3/inference/state/Qwen2State.java index 266730ac..5f8213eb 100644 --- a/src/main/java/org/beehive/gpullama3/inference/state/Qwen2State.java +++ b/src/main/java/org/beehive/gpullama3/inference/state/Qwen2State.java @@ -12,9 +12,11 @@ public class Qwen2State extends State { + protected static final int QWEN2_LOCAL_SIZE = 32; + public Qwen2State(Configuration config, int batchsize) { super(config, batchsize); - this.localSize = 32; + this.localSize = QWEN2_LOCAL_SIZE; } @Override protected StateFields createStateFields(Configuration configuration) { @@ -66,9 +68,11 @@ protected StateFields createStateFields(Configuration configuration) { fields.positionHolder = new IntArray(1); // Temporary arrays - fields.temp = new FloatArray(1 + ((config.dim() + localSize - 1) / localSize)); - fields.tempFFN = new FloatArray(1 + ((config.dim() + localSize - 1) / localSize)); - fields.tempLogits = new FloatArray(1 + ((config.dim() + localSize - 1) / localSize)); + // State invokes this override before the Qwen2State constructor body runs, + // so use the Qwen2 work-group size directly instead of State.localSize. + fields.temp = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); + fields.tempFFN = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); + fields.tempLogits = new FloatArray(1 + ((config.dim() + QWEN2_LOCAL_SIZE - 1) / QWEN2_LOCAL_SIZE)); return fields; diff --git a/src/main/java/org/beehive/gpullama3/inference/weights/standard/Qwen2MoEStandardWeights.java b/src/main/java/org/beehive/gpullama3/inference/weights/standard/Qwen2MoEStandardWeights.java new file mode 100644 index 00000000..3620281c --- /dev/null +++ b/src/main/java/org/beehive/gpullama3/inference/weights/standard/Qwen2MoEStandardWeights.java @@ -0,0 +1,78 @@ +package org.beehive.gpullama3.inference.weights.standard; + +import org.beehive.gpullama3.tensor.GGMLType; +import org.beehive.gpullama3.tensor.standard.ArrayFloatTensor; +import org.beehive.gpullama3.tensor.standard.FloatTensor; + +public class Qwen2MoEStandardWeights extends StandardWeights { + // Qwen2-MoE-specific weights + public final FloatTensor[] q_bias, k_bias, v_bias; + public final FloatTensor[] routerGate; + public final FloatTensor[] gateExps; + public final FloatTensor[] upExps; + public final FloatTensor[] downExps; + public final FloatTensor[] sharedGate; + public final FloatTensor[] sharedUp; + public final FloatTensor[] sharedDown; + public final FloatTensor[] sharedGateInp; + + public Qwen2MoEStandardWeights( + FloatTensor token_embedding_table, + FloatTensor[] rms_att_weight, + FloatTensor[] wq, + FloatTensor[] wk, + FloatTensor[] wv, + FloatTensor[] q_bias, + FloatTensor[] k_bias, + FloatTensor[] v_bias, + FloatTensor[] wo, + FloatTensor[] rms_ffn_weight, + FloatTensor[] w1, + FloatTensor[] w2, + FloatTensor[] w3, + FloatTensor[] routerGate, + FloatTensor[] gateExps, + FloatTensor[] upExps, + FloatTensor[] downExps, + FloatTensor[] sharedGate, + FloatTensor[] sharedUp, + FloatTensor[] sharedDown, + FloatTensor[] sharedGateInp, + FloatTensor rms_final_weight, + ArrayFloatTensor freq_cis_real, + ArrayFloatTensor freq_cis_imag, + FloatTensor wcls, + GGMLType weightType) { + super(token_embedding_table, + rms_att_weight, + wq, + wk, + wv, + wo, + rms_ffn_weight, + w1, + w2, + w3, + rms_final_weight, + freq_cis_real, + freq_cis_imag, + wcls, + weightType); + this.q_bias = q_bias; + this.k_bias = k_bias; + this.v_bias = v_bias; + this.routerGate = routerGate; + this.gateExps = gateExps; + this.upExps = upExps; + this.downExps = downExps; + this.sharedGate = sharedGate; + this.sharedUp = sharedUp; + this.sharedDown = sharedDown; + this.sharedGateInp = sharedGateInp; + } + + @Override + public GGMLType getWeightType() { + return weightType; + } +} diff --git a/src/main/java/org/beehive/gpullama3/inference/weights/tornado/Qwen2MoETornadoWeights.java b/src/main/java/org/beehive/gpullama3/inference/weights/tornado/Qwen2MoETornadoWeights.java new file mode 100644 index 00000000..68f7b5b9 --- /dev/null +++ b/src/main/java/org/beehive/gpullama3/inference/weights/tornado/Qwen2MoETornadoWeights.java @@ -0,0 +1,78 @@ +package org.beehive.gpullama3.inference.weights.tornado; + +import org.beehive.gpullama3.tensor.GGMLType; +import org.beehive.gpullama3.tensor.tornado.TornadoTensor; + +/** + * TornadoVM weight container for Qwen2-MoE / Qwen1.5-MoE models. + * + *
The inherited fields provide the embedding, attention, RMSNorm and final + * classifier weights. MoE-specific tensors remain in their GGUF Q8_0 layout; + * GPU kernels will access their {@code ByteArray} representation through + * {@link TornadoTensor#asByteArray()}.
+ */ +public final class Qwen2MoETornadoWeights extends Qwen2TornadoWeights { + + public final TornadoTensor[] routerGateLayered; + public final TornadoTensor[] gateExpertsLayered; + public final TornadoTensor[] upExpertsLayered; + public final TornadoTensor[] downExpertsLayered; + public final TornadoTensor[] sharedGateLayered; + public final TornadoTensor[] sharedUpLayered; + public final TornadoTensor[] sharedDownLayered; + public final TornadoTensor[] sharedGateInputLayered; + + // @formatter:off + public Qwen2MoETornadoWeights( + TornadoTensor tokenEmbeddingTable, + TornadoTensor[] rmsAttWeightLayered, + TornadoTensor[] wqLayered, + TornadoTensor[] wkLayered, + TornadoTensor[] wvLayered, + TornadoTensor[] qBiasLayered, + TornadoTensor[] kBiasLayered, + TornadoTensor[] vBiasLayered, + TornadoTensor[] woLayered, + TornadoTensor[] rmsFfnWeightLayered, + TornadoTensor[] routerGateLayered, + TornadoTensor[] gateExpertsLayered, + TornadoTensor[] upExpertsLayered, + TornadoTensor[] downExpertsLayered, + TornadoTensor[] sharedGateLayered, + TornadoTensor[] sharedUpLayered, + TornadoTensor[] sharedDownLayered, + TornadoTensor[] sharedGateInputLayered, + TornadoTensor rmsFinalWeight, + TornadoTensor freqCisReal, + TornadoTensor freqCisImag, + TornadoTensor wCls, + GGMLType weightType) { + super(tokenEmbeddingTable, + rmsAttWeightLayered, + wqLayered, + wkLayered, + wvLayered, + qBiasLayered, + kBiasLayered, + vBiasLayered, + woLayered, + rmsFfnWeightLayered, + null, + null, + null, + rmsFinalWeight, + freqCisReal, + freqCisImag, + wCls, + weightType); + this.routerGateLayered = routerGateLayered; + this.gateExpertsLayered = gateExpertsLayered; + this.upExpertsLayered = upExpertsLayered; + this.downExpertsLayered = downExpertsLayered; + this.sharedGateLayered = sharedGateLayered; + this.sharedUpLayered = sharedUpLayered; + this.sharedDownLayered = sharedDownLayered; + this.sharedGateInputLayered = sharedGateInputLayered; + } + // @formatter:on +} diff --git a/src/main/java/org/beehive/gpullama3/model/ModelType.java b/src/main/java/org/beehive/gpullama3/model/ModelType.java index 0659da7d..d3d7fa63 100644 --- a/src/main/java/org/beehive/gpullama3/model/ModelType.java +++ b/src/main/java/org/beehive/gpullama3/model/ModelType.java @@ -7,6 +7,7 @@ import org.beehive.gpullama3.model.loader.MistralModelLoader; import org.beehive.gpullama3.model.loader.Phi3ModelLoader; import org.beehive.gpullama3.model.loader.Qwen2ModelLoader; +import org.beehive.gpullama3.model.loader.Qwen2MoEModelLoader; import org.beehive.gpullama3.model.loader.Qwen3ModelLoader; import java.nio.channels.FileChannel; @@ -59,6 +60,13 @@ public Model loadModel(FileChannel fileChannel, GGUF gguf, int contextLength, bo } }, + QWEN_2_MOE { + @Override + public Model loadModel(FileChannel fileChannel, GGUF gguf, int contextLength, boolean useTornadovm) { + return new Qwen2MoEModelLoader(fileChannel, gguf, contextLength, useTornadovm).loadModel(); + } + }, + DEEPSEEK_R1_DISTILL_QWEN { @Override public Model loadModel(FileChannel fileChannel, GGUF gguf, int contextLength, boolean useTornadovm) { diff --git a/src/main/java/org/beehive/gpullama3/model/format/Qwen3ChatFormat.java b/src/main/java/org/beehive/gpullama3/model/format/Qwen3ChatFormat.java index 5d121c31..cd87a3d1 100644 --- a/src/main/java/org/beehive/gpullama3/model/format/Qwen3ChatFormat.java +++ b/src/main/java/org/beehive/gpullama3/model/format/Qwen3ChatFormat.java @@ -93,6 +93,8 @@ public ListInputs and outputs are GPU-resident TornadoVM arrays:
+ *The first implementation deliberately uses one GPU thread for this + * small operation (60 experts in the target model). A later optimization + * can parallelize the reductions within one workgroup.
+ */ + public static void softmaxAndTopK( + KernelContext context, + FloatArray routerLogits, + IntArray selectedExperts, + FloatArray routingWeights, + int numberOfExperts, + int topK) { + + // The first correctness-oriented implementation is serial. Without + // this guard, every thread would race to overwrite the same buffers. + if (context.groupIdx != 0 || context.localIdx != 0) { + return; + } + + // Find the maximum first to keep the softmax numerically stable. + float maxLogit = Float.NEGATIVE_INFINITY; + int maxIndex = -1; + for (int expert = 0; expert < numberOfExperts; expert++) { + float logit = routerLogits.get(expert); + if (logit > maxLogit) { + maxLogit = logit; + maxIndex = expert; + } + } + + // Convert every router score to a probability over all experts. + float sumExp = 0.0f; + for (int expert = 0; expert < numberOfExperts; expert++) { + sumExp += TornadoMath.exp(routerLogits.get(expert) - maxLogit); + } + for (int expert = 0; expert < numberOfExperts; expert++) { + float probability = TornadoMath.exp(routerLogits.get(expert) - maxLogit) / sumExp; + routerLogits.set(expert, probability); + } + + // Select the top-K probabilities without renormalizing their sum. + selectedExperts.set(0, maxIndex); + routingWeights.set(0, routerLogits.get(maxIndex)); + routerLogits.set(maxIndex, Float.NEGATIVE_INFINITY); + + for (int slot = 1; slot < topK; slot++) { + maxLogit = Float.NEGATIVE_INFINITY; + maxIndex = -1; + for (int expert = 0; expert < numberOfExperts; expert++) { + if (routerLogits.get(expert) > maxLogit) { + maxLogit = routerLogits.get(expert); + maxIndex = expert; + } + } + selectedExperts.set(slot, maxIndex); + routingWeights.set(slot, routerLogits.get(maxIndex)); + routerLogits.set(maxIndex, Float.NEGATIVE_INFINITY); + } + } + + /** + * Computes the routed expert's gated activation for one top-K slot: + * {@code SiLU(W_gate[expert] * x) * (W_up[expert] * x)}. + * + *The expert matrices are stacked in one Q8_0 tensor per layer. The + * selected expert id determines which matrix slice this kernel reads.
+ */ + public static void fusedRoutedExpertGateUpSwiGLUQ8_0( + KernelContext context, + FloatArray input, + IntArray selectedExperts, + int slot, + ByteArray gateExperts, + ByteArray upExperts, + FloatArray expertHidden, + int dim, + int moeHiddenDim, + int numberOfExperts, + int localWorkGroupSize) { + + int rowId = context.groupIdx; + int localId = context.localIdx; + + int expert = selectedExperts.get(slot); + if (rowId >= moeHiddenDim || expert < 0 || expert >= numberOfExperts) { + return; + } + + // Locate this output row within the selected expert's stacked matrix. + int blocksPerRow = (dim + Q8_0_BLOCK_SIZE - 1) / Q8_0_BLOCK_SIZE; + int rowBlockOffset = + (expert * moeHiddenDim + rowId) * blocksPerRow; + + // One workgroup cooperates on the Gate and Up dot products for this row. + float gatePartialSum = 0.0f; + float upPartialSum = 0.0f; + + for (int column = localId; + column < dim; + column += localWorkGroupSize) { + + // Byte offset of the first byte of the Q8_0 block that contains this column. + // Each block occupies 34 bytes: a 2-byte FP16 scale plus 32 int8 quants. + int blockByteOffset = + (rowBlockOffset + column / Q8_0_BLOCK_SIZE) * Q8_0_BLOCK_BYTES; + + // Skip the 2-byte scale at the block start and locate this column's int8 quant. + int quantOffset = + blockByteOffset + 2 + column % Q8_0_BLOCK_SIZE; + + float inputValue = input.get(column); + + // getHalfFloat reads the FP16 scale from the first two block bytes, then converts it to FP32. + float gateScale = + gateExperts.getHalfFloat(blockByteOffset).getFloat32(); + float upScale = + upExperts.getHalfFloat(blockByteOffset).getFloat32(); + + byte gateQuant = gateExperts.get(quantOffset); + byte upQuant = upExperts.get(quantOffset); + + float gateWeight = gateQuant * gateScale; + float upWeight = upQuant * upScale; + + gatePartialSum += gateWeight * inputValue; + upPartialSum += upWeight * inputValue; + + } + + // Sum the partial gate values from all threads in this workgroup. + float[] localSums = context.allocateFloatLocalArray(localWorkGroupSize); + localSums[localId] = gatePartialSum; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + float gate = localSums[0]; + + // Reuse local memory to sum the partial up values. + localSums[localId] = upPartialSum; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + + // One thread writes this output row after both reductions are complete. + if (localId == 0) { + float up = localSums[0]; + float siluGate = gate / (1.0f + TornadoMath.exp(-gate)); + expertHidden.set(rowId, siluGate * up); + } + } + + /** + * Down-projects one selected expert and accumulates its routed contribution: + * {@code residual += routingWeight[slot] * W_down[expert] * expertHidden}. + */ + public static void routedExpertDownProjectAndAccumulateQ8_0( + KernelContext context, + FloatArray expertHidden, + FloatArray residual, + IntArray selectedExperts, + FloatArray routingWeights, + int slot, + ByteArray downExperts, + int dim, + int moeHiddenDim, + int numberOfExperts, + int localWorkGroupSize) { + + // One workgroup produces one element of the down-projected vector. + int rowId = context.groupIdx; + int localId = context.localIdx; + if (rowId >= dim) { + return; + } + + int expert = selectedExperts.get(slot); + if (expert < 0 || expert >= numberOfExperts) { + return; + } + float routingWeight = routingWeights.get(slot); + + // downExperts has the logical shape [experts, dim, moeHiddenDim]. + int blocksPerRow = (moeHiddenDim + Q8_0_BLOCK_SIZE - 1) / Q8_0_BLOCK_SIZE; + int rowBlockOffset = (expert * dim + rowId) * blocksPerRow; + + // Every thread accumulates a different subset of this row's dot product. + float partialSum = 0.0f; + for (int column = localId; + column < moeHiddenDim; + column += localWorkGroupSize) { + // The start byte of the Q8_0 block holding this down-projection weight. + // Block layout: a 2-byte FP16 scale followed by 32 int8 quants. + int blockByteOffset = + (rowBlockOffset + column / Q8_0_BLOCK_SIZE) * Q8_0_BLOCK_BYTES; + + // Quants begin immediately after the scale; column % 32 is the index within this block. + int quantOffset = blockByteOffset + 2 + column % Q8_0_BLOCK_SIZE; + + float weight = downExperts.get(quantOffset) + * downExperts.getHalfFloat(blockByteOffset).getFloat32(); + partialSum += weight * expertHidden.get(column); + } + + // Combine all thread-local partial sums into the completed output row. + float[] localSums = context.allocateFloatLocalArray(localWorkGroupSize); + localSums[localId] = partialSum; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + + if (localId == 0) { + float outputValue = localSums[0]; + residual.set(rowId, residual.get(rowId) + routingWeight * outputValue); + } + } + + /** Computes {@code SiLU(sharedGate * input) * (sharedUp * input)}. */ + public static void sharedExpertGateUpSwiGLUQ8_0( + KernelContext context, + FloatArray input, + ByteArray sharedGate, + ByteArray sharedUp, + FloatArray sharedHidden, + int dim, + int sharedExpertHiddenDim, + int localWorkGroupSize) { + + // One workgroup computes one shared-expert hidden output row. + int rowId = context.groupIdx; + int localId = context.localIdx; + + if (rowId >= sharedExpertHiddenDim) { + return; + } + + // Number of Q8_0 blocks needed for one row of dim weights. + int blocksPerRow = (dim + Q8_0_BLOCK_SIZE - 1) / Q8_0_BLOCK_SIZE; + + // Index of this row's first Q8_0 block in the shared weight array. + int rowBlockOffset = rowId * blocksPerRow; + + float gatePartialSum = 0.0f; + float upPartialSum = 0.0f; + + for (int column = localId; + column < dim; + column += localWorkGroupSize) { + + // Byte offset of the first byte of the Q8_0 block that contains this column. + // Each block occupies 34 bytes: a 2-byte FP16 scale plus 32 int8 quants. + int blockByteOffset = + (rowBlockOffset + column / Q8_0_BLOCK_SIZE) * Q8_0_BLOCK_BYTES; + + // Skip the 2-byte scale at the block start and locate this column's int8 quant. + int quantOffset = + blockByteOffset + 2 + column % Q8_0_BLOCK_SIZE; + + float inputValue = input.get(column); + + // getHalfFloat reads the FP16 scale from the first two block bytes, then converts it to FP32. + float gateScale = + sharedGate.getHalfFloat(blockByteOffset).getFloat32(); + float upScale = + sharedUp.getHalfFloat(blockByteOffset).getFloat32(); + + byte gateQuant = sharedGate.get(quantOffset); + byte upQuant = sharedUp.get(quantOffset); + + float gateWeight = gateQuant * gateScale; + float upWeight = upQuant * upScale; + + gatePartialSum += gateWeight * inputValue; + upPartialSum += upWeight * inputValue; + + + } + + float[] localSums = context.allocateFloatLocalArray(localWorkGroupSize); + + localSums[localId] = gatePartialSum; + context.localBarrier(); + + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + + float gate = localSums[0]; + + // Reuse local memory to sum the partial up values. + localSums[localId] = upPartialSum; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + + // One thread writes this output row after both reductions are complete. + if (localId == 0) { + float up = localSums[0]; + float siluGate = gate / (1.0f + TornadoMath.exp(-gate)); + sharedHidden.set(rowId, siluGate * up); + } + } + + /** Down-projects the shared expert hidden vector into the model dimension. */ + public static void sharedExpertDownProjectQ8_0( + KernelContext context, + FloatArray sharedHidden, + ByteArray sharedDown, + FloatArray sharedOutput, + int dim, + int sharedExpertHiddenDim, + int localWorkGroupSize) { + + + int rowId = context.groupIdx; + int localId = context.localIdx; + if (rowId >= dim) { + return; + } + + // Number of Q8_0 blocks needed for one row of dim weights. + int blocksPerRow = (sharedExpertHiddenDim + Q8_0_BLOCK_SIZE - 1) / Q8_0_BLOCK_SIZE; + + // Index of this row's first Q8_0 block in the shared weight array. + int rowBlockOffset = rowId * blocksPerRow; + + float partialSum = 0.0f; + for (int column = localId; + column < sharedExpertHiddenDim; + column += localWorkGroupSize) { + // The start byte of the Q8_0 block holding this down-projection weight. + // Block layout: a 2-byte FP16 scale followed by 32 int8 quants. + int blockByteOffset = + (rowBlockOffset + column / Q8_0_BLOCK_SIZE) * Q8_0_BLOCK_BYTES; + + // Quants begin immediately after the scale; column % 32 is the index within this block. + int quantOffset = blockByteOffset + 2 + column % Q8_0_BLOCK_SIZE; + + float weight = sharedDown.get(quantOffset) + * sharedDown.getHalfFloat(blockByteOffset).getFloat32(); + partialSum += weight * sharedHidden.get(column); + } + + // Combine all thread-local partial sums into the completed output row. + float[] localSums = context.allocateFloatLocalArray(localWorkGroupSize); + localSums[localId] = partialSum; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + + if (localId == 0) { + float outputValue = localSums[0]; + sharedOutput.set(rowId, outputValue); + } + } + + /** Computes the shared gate sigmoid and adds the weighted shared output to the residual. */ + public static void sharedExpertGateAndAccumulate( + KernelContext context, + FloatArray input, + FloatArray sharedGateInput, + FloatArray sharedOutput, + FloatArray residual, + int dim, + int localWorkGroupSize) { + int localId = context.localIdx; + + float partialScore = 0.0f; + + for (int column = localId; + column < dim; + column += localWorkGroupSize) { + partialScore += sharedGateInput.get(column) * input.get(column); + } + + float[] localSums = context.allocateFloatLocalArray(localWorkGroupSize); + localSums[localId] = partialScore; + context.localBarrier(); + for (int stride = localWorkGroupSize / 2; stride > 0; stride >>= 1) { + if (localId < stride) { + localSums[localId] += localSums[localId + stride]; + } + context.localBarrier(); + } + float gateScore = localSums[0]; + float sharedWeight = + 1.0f / (1.0f + TornadoMath.exp(-gateScore)); + + for (int index = localId; + index < dim; + index += localWorkGroupSize) { + residual.set(index, + residual.get(index) + sharedWeight * sharedOutput.get(index)); + } + } +} diff --git a/src/main/java/org/beehive/gpullama3/tornadovm/layers/type/q8_0/Qwen2MoEQ8_0FFNLayers.java b/src/main/java/org/beehive/gpullama3/tornadovm/layers/type/q8_0/Qwen2MoEQ8_0FFNLayers.java new file mode 100644 index 00000000..0a439d80 --- /dev/null +++ b/src/main/java/org/beehive/gpullama3/tornadovm/layers/type/q8_0/Qwen2MoEQ8_0FFNLayers.java @@ -0,0 +1,261 @@ +package org.beehive.gpullama3.tornadovm.layers.type.q8_0; + +import org.beehive.gpullama3.inference.state.Qwen2MoEState; +import org.beehive.gpullama3.inference.weights.tornado.Qwen2MoETornadoWeights; +import org.beehive.gpullama3.model.qwen2.Qwen2MoEConfiguration; +import org.beehive.gpullama3.tornadovm.kernels.Qwen2Kernels; +import org.beehive.gpullama3.tornadovm.kernels.Qwen2MoEKernels; +import org.beehive.gpullama3.tornadovm.kernels.Qwen3Kernels; +import org.beehive.gpullama3.tornadovm.kernels.TransformerComputeKernelsLayered; +import org.beehive.gpullama3.tornadovm.layers.AbstractTransformerLayerTaskGraphs; +import org.beehive.gpullama3.tornadovm.scheduling.SchedulerType; +import org.beehive.gpullama3.tornadovm.scheduling.WorkerGridFactory; +import uk.ac.manchester.tornado.api.GridScheduler; +import uk.ac.manchester.tornado.api.TaskGraph; +import uk.ac.manchester.tornado.api.WorkerGrid; +import uk.ac.manchester.tornado.api.WorkerGrid1D; +import uk.ac.manchester.tornado.api.WorkerGrid2D; +import uk.ac.manchester.tornado.api.enums.DataTransferMode; + +/** + * Single-token Q8_0 TaskGraphs for Qwen2-MoE / Qwen1.5-MoE. + * + *The attention block follows Qwen2. Its dense FFN is replaced by the + * routed-expert pipeline: normalize, route, choose top-K experts, execute each + * selected expert, and accumulate its weighted output into {@code wrapX}.
+ */ +public final class Qwen2MoEQ8_0FFNLayers + extends AbstractTransformerLayerTaskGraphs