How to speed up Gemma 4 token generation (decode) with MTP, supported in ailia LLM 1.5.1.
This page describes the models and API needed for MTP as a delta from the ailia LLM NPU (QNN) Guide. NPU setup (deploying the QNN runtime, Android configuration, getting the SoC name) is the same as in the QNN Guide. For measurements, see the ailia LLM MTP Benchmark.
An LLM normally generates text one token per inference. Because each token depends on all the tokens before it, generating 1,000 tokens means running the large main model 1,000 times in sequence. NPUs are good at computing many rows at once, but when processing one token at a time, weight loading dominates and the NPU's compute cannot be used effectively.
MTP (Multi-Token Prediction) uses a small assistant (draft model) to guess the next few tokens ahead of time, and then has the large main model verify those guesses all at once. In ailia LLM, the assistant proposes 3 candidate tokens, and the main model evaluates 4 rows ("1 committed token + 3 candidates") in a single inference (the AR-4 graph). Candidates that match the main model's prediction are accepted as is, and at the first mismatch the token predicted by the main model is used instead.
Even if every candidate is rejected, the one token predicted by the main model is always committed. The more guesses hit, the fewer times the main model runs and the faster generation becomes. Because the output tokens are determined by the main model's predictions, MTP increases speed without degrading output quality (however, due to numerical differences between AR-4 and AR-1, the token sequence is not guaranteed to be identical to normal generation). For sampling with temperature, candidates are accepted based on the probability ratio between the main model and the assistant (speculative sampling), so the output follows the main model's probability distribution.
Switch the main model to the MTP-capable version (-mtp.qnn), deploy the assistant (-mtp-assistant.qnn), and add one line that opens the assistant after opening the main model. No changes are needed to the code that sets prompts and generates.
ailiaLLMOpenModelFileA(llm, "gemma4-e2b-sm7635-int16-mtp.qnn", 0); /* main model (MTP-capable) */
ailiaLLMOpenMtpModelFileA(llm, "gemma4-e2b-sm7635-int16-mtp-assistant.qnn", 3); /* added */
/* ailiaLLMSetPrompt / ailiaLLMGenerate stay unchanged */
libailia_llm.so and libailia_llm_qnn.so (ailia_llm.dll and ailia_llm_qnn.dll on Windows) from the same version.ailiaLLMSetPrompt / ailiaLLMSetMultimodalPrompt) and calling ailiaLLMGenerate work the same as before. Even with MTP, each ailiaLLMGenerate call returns one token, delivering committed tokens in order from an internal queue.-mmproj.qnn) for the same SoC, model and precision as the main model.Always use the main model and assistant as a pair for the same SoC, model and precision. All files target ailia LLM 1.5.1 (QAIRT 2.47.0.260601) with a context length of 8192. Both the main model and the assistant use 4-bit weights (W4); FP16 / Int16 refers to the activation precision.
| SoC | Example device | Model | File | Contents | Size |
|---|---|---|---|---|---|
| sm8475 (Hexagon v69 / FP16) | Snapdragon 8+ Gen 1 | E2B | gemma4-e2b-sm8475-fp16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 3.61 GB |
| gemma4-e2b-sm8475-fp16-mtp-assistant.qnn | Assistant (draft model) | 44.6 MB | |||
| gemma4-e2b-sm8475-fp16-mmproj.qnn | Image / audio encoder (mmproj) | 1.10 GB | |||
| sm7635 (Hexagon v73 / Int16) | Snapdragon 7s Gen 3 | E2B | gemma4-e2b-sm7635-int16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 3.61 GB |
| gemma4-e2b-sm7635-int16-mtp-assistant.qnn | Assistant (draft model) | 46.8 MB | |||
| gemma4-e2b-sm7635-int16-mmproj.qnn | Image / audio encoder (mmproj) | 0.63 GB | |||
| E4B | gemma4-e4b-sm7635-int16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 5.56 GB | ||
| gemma4-e4b-sm7635-int16-mtp-assistant.qnn | Assistant (draft model) | 47.5 MB | |||
| gemma4-e4b-sm7635-int16-mmproj.qnn | Image / audio encoder (mmproj) | 0.63 GB | |||
| sc8380xp (Hexagon v73 / FP16) | Surface (Snapdragon X Plus) | E2B | gemma4-e2b-sc8380xp-fp16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 3.63 GB |
| gemma4-e2b-sc8380xp-fp16-mtp-assistant.qnn | Assistant (draft model) | 46.6 MB | |||
| gemma4-e2b-sc8380xp-fp16-mmproj.qnn | Image / audio encoder (mmproj) | 1.09 GB | |||
| E4B | gemma4-e4b-sc8380xp-fp16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 5.61 GB | ||
| gemma4-e4b-sc8380xp-fp16-mtp-assistant.qnn | Assistant (draft model) | 47.0 MB | |||
| gemma4-e4b-sc8380xp-fp16-mmproj.qnn | Image / audio encoder (mmproj) | 1.10 GB | |||
| qcs6490 (Hexagon v68 / Int16) | QCS6490 | E2B | gemma4-e2b-qcs6490-int16-mtp.qnn | Main model (AR-256 / AR-4 / AR-1) | 3.60 GB |
| gemma4-e2b-qcs6490-int16-mtp-assistant.qnn | Assistant (draft model) | 44.7 MB | |||
| gemma4-e2b-qcs6490-int16-mmproj.qnn | Image / audio encoder (mmproj) | 0.63 GB |
For image and audio input, use the -mmproj.qnn for the same SoC, model and precision as the main model.
If the assistant is not opened, the MTP-capable main model generates one token at a time with AR-1 as before. A single MTP-capable main model file is enough even if you switch MTP on and off.
On the CPU, use a regular Gemma 4 GGUF for the main model and the MTP GGUF for the assistant.
| Model | File | Contents | Size |
|---|---|---|---|
| E2B | gemma-4-E2B-it-qat-Q4_0.gguf | Main model (example) | 3.35 GB |
| mtp-gemma-4-E2B-it-Q4_0.gguf | Assistant (draft model) | 59.2 MB | |
| E4B | gemma-4-E4B-it-qat-Q4_0.gguf | Main model (example) | 5.15 GB |
| mtp-gemma-4-E4B-it-Q4_0.gguf | Assistant (draft model) | 59.7 MB |
The assistant must match the main model's vocabulary and hidden size. Pair an E2B main model with the E2B assistant, and an E4B main model with the E4B assistant.
The MTP-enabled evaluation APK for Android is ailia-models-kotlin-qnn.apk.
The MTP-enabled evaluation app for Windows on Arm is ailia-models-flutter-qnn-v261017.zip. It runs on Snapdragon X Plus and QCS6490.
Open the assistant with ailiaLLMOpenMtpModelFileA / W. As with ailiaLLMOpenModelFileA / W for the main model, choose the variant according to the path string type.
int ailiaLLMOpenMtpModelFileA(struct AILIALLM *llm, const char *path, unsigned int n_draft);
int ailiaLLMOpenMtpModelFileW(struct AILIALLM *llm, const wchar_t *path, unsigned int n_draft);
| Argument | Description |
|---|---|
llm | The AILIALLM instance in which the main model has been opened |
path | Path to the assistant. The A variant takes UTF-8 and the W variant a wide string (UTF-16 on Windows). Specify -mtp-assistant.qnn for an NPU main model, or mtp-gemma-4-*.gguf for a CPU main model. |
n_draft | Number of candidate (draft) tokens the assistant generates per round. The main model verifies these candidates together with one token of its own. Specify 1 or more; normally specify 3. On the NPU (QNN), the range is 1 to 3. Specifying 0 returns an error. If you do not use MTP, do not call ailiaLLMOpenMtpModelFileA / W. |
Call the APIs in the following order.
ailiaLLMCreateailiaLLMOpenModelFileA / W (-mtp.qnn on the NPU)ailiaLLMOpenMtpModelFileA / W (only once, before setting the first prompt)ailiaLLMOpenMultimodalProjectorFileA / WailiaLLMSetPrompt / ailiaLLMSetMultimodalPrompt and ailiaLLMGenerate.qnn without AR-4) returns an error.ailiaLLMDestroy) and create a new one. A second call returns INVALID_STATE.CONTEXT_FULL.These examples get the SoC name on the NPU and open the MTP-capable main model and assistant. The model files are assumed to be in the app's private storage. The precision (fp16 / int16) depends on the SoC; choose it according to the table above.
#include "ailia_llm.h"
#include <stdio.h>
#include <string.h>
/* model_dir: the app's private storage. On success, the caller must Destroy. */
int open_qnn_mtp_model(const char *model_dir, struct AILIALLM **out) {
if (model_dir == NULL || out == NULL) return AILIA_LLM_STATUS_INVALID_ARGUMENT;
*out = NULL;
const char *soc = NULL; /* owned by the library; do not free */
int status = ailiaLLMGetQNNModelName(&soc);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
/* sm8475 / sc8380xp use FP16, sm7635 / qcs6490 use Int16 */
const char *precision =
(strcmp(soc, "sm8475") == 0 || strcmp(soc, "sc8380xp") == 0) ? "fp16" : "int16";
char target[512], assistant[512];
int n1 = snprintf(target, sizeof(target), "%s/gemma4-e2b-%s-%s-mtp.qnn", model_dir, soc, precision);
int n2 = snprintf(assistant, sizeof(assistant), "%s/gemma4-e2b-%s-%s-mtp-assistant.qnn", model_dir, soc, precision);
if (n1 < 0 || (size_t)n1 >= sizeof(target) || n2 < 0 || (size_t)n2 >= sizeof(assistant))
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
struct AILIALLM *llm = NULL;
status = ailiaLLMCreate(&llm);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
status = ailiaLLMOpenModelFileA(llm, target, 0); /* main model */
if (status == AILIA_LLM_STATUS_SUCCESS)
status = ailiaLLMOpenMtpModelFileA(llm, assistant, 3); /* draft model */
if (status != AILIA_LLM_STATUS_SUCCESS) { ailiaLLMDestroy(llm); return status; }
*out = llm;
return AILIA_LLM_STATUS_SUCCESS;
}
On the CPU, specify a GGUF for the main model and the MTP GGUF for the assistant.
ailiaLLMOpenModelFileA(llm, "gemma-4-E2B-it-qat-Q4_0.gguf", 4096);
ailiaLLMOpenMtpModelFileA(llm, "mtp-gemma-4-E2B-it-Q4_0.gguf", 3);
from pathlib import Path
import ailia_llm
soc = ailia_llm.AiliaLLM.get_qnn_model_name()
precision = "fp16" if soc in ("sm8475", "sc8380xp") else "int16"
model_dir = Path("/data/user/0/your.package/files")
llm = ailia_llm.AiliaLLM()
llm.open(str(model_dir / f"gemma4-e2b-{soc}-{precision}-mtp.qnn"), n_ctx=0)
llm.open_mtp_model(str(model_dir / f"gemma4-e2b-{soc}-{precision}-mtp-assistant.qnn"), n_draft=3)
for delta in llm.generate([{"role": "user", "content": "Introduce yourself."}]):
print(delta, end="", flush=True)
On the CPU, call llm.open_mtp_model("mtp-gemma-4-E2B-it-Q4_0.gguf", n_draft=3) after llm.open("gemma-4-E2B-it-qat-Q4_0.gguf"). On failure, AiliaLLMError is raised.
import 'package:ailia_llm/ailia_llm_model.dart';
final soc = AiliaLLMModel.getQNNModelName();
final precision = (soc == 'sm8475' || soc == 'sc8380xp') ? 'fp16' : 'int16';
const dir = '/data/user/0/your.package/files';
final model = AiliaLLMModel();
model.open('$dir/gemma4-e2b-$soc-$precision-mtp.qnn', 0);
model.openMtpModel('$dir/gemma4-e2b-$soc-$precision-mtp-assistant.qnn', 3);
// Call model.close() when done
On the CPU, call model.openMtpModel('mtp-gemma-4-E2B-it-Q4_0.gguf', 3) after model.open('gemma-4-E2B-it-qat-Q4_0.gguf', 4096). On failure, an Exception is thrown; the main model stays open and MTP is not enabled.
context is the Android Activity or Application Context.
import axip.ailia_llm.AiliaLLM
val soc = AiliaLLM.getQNNModelName()
val precision = if (soc == "sm8475" || soc == "sc8380xp") "fp16" else "int16"
val dir = context.filesDir
val llm = AiliaLLM()
llm.openModelFile("$dir/gemma4-e2b-$soc-$precision-mtp.qnn", 0)
llm.openMtpModel("$dir/gemma4-e2b-$soc-$precision-mtp-assistant.qnn", 3)
// Call llm.close() when done
On the CPU, call llm.openMtpModel("mtp-gemma-4-E2B-it-Q4_0.gguf", 3) after llm.openModelFile("gemma-4-E2B-it-qat-Q4_0.gguf", 4096). On failure, a RuntimeException is thrown.
using System;
using System.IO;
using UnityEngine;
using ailiaLLM;
string soc = AiliaLLMModel.GetQNNModelName();
string precision = (soc == "sm8475" || soc == "sc8380xp") ? "fp16" : "int16";
string dir = Application.persistentDataPath;
var llm = new AiliaLLMModel();
if (!llm.Create()) throw new Exception("ailiaLLMCreate failed");
if (!llm.Open(Path.Combine(dir, $"gemma4-e2b-{soc}-{precision}-mtp.qnn"), 0))
throw new Exception(llm.GetErrorDetail());
if (!llm.OpenMtpModel(Path.Combine(dir, $"gemma4-e2b-{soc}-{precision}-mtp-assistant.qnn"), 3))
throw new Exception(llm.GetErrorDetail());
// Call llm.Dispose() when done
Open the mmproj for the same SoC, model and precision after opening the assistant. Prompts are set in the same way as in the Multimodal Guide.
ailiaLLMOpenModelFileA(llm, "gemma4-e2b-sm7635-int16-mtp.qnn", 0);
ailiaLLMOpenMtpModelFileA(llm, "gemma4-e2b-sm7635-int16-mtp-assistant.qnn", 3);
ailiaLLMOpenMultimodalProjectorFileA(llm, "gemma4-e2b-sm7635-int16-mmproj.qnn");
/* then ailiaLLMSetMultimodalPrompt / ailiaLLMGenerate */