Large Language Models
Overview
The smile.llm package provides building blocks and complete inference stacks for decoder-only LLMs on the same FFM-backed LibTorch bridge as smile.deep. It ships a LLaMA-3 implementation, a Qwen3.5 hybrid stack (Gated DeltaNet + gated full attention), a tiktoken BPE tokenizer, a continuous-batching inference engine, KV-cache / radix prefix reuse, weight quantization, and tensor parallelism.
For the shared tensor / layer API see Deep Learning. Production HTTP serving (OpenAI-compatible chat, tool calling, web UI) is covered on SMILE Serve.
Prerequisites
smile.llm uses the same native stack as smile.deep: the smile_torch shared library and LibTorch must be discoverable by the platform loader, and FFM access must be enabled (--enable-native-access=ALL-UNNAMED). See Deep Learning — Prerequisites for per-platform details.
GPU inference needs CUDA-enabled LibTorch. On Ampere or newer hardware models load in BFloat16; older GPUs use Float16. CPU inference runs in Float32.
Package Map
| Package / type | Role |
|---|---|
Role, Message, ContentPart, … | Chat dialog types (text + multimodal parts, tool fields) |
ChatCompletion, ChatOptions, FinishReason | Inference results and per-request tool-calling options |
LanguageModel | Common façade: encodeChat, generate, chat |
tokenizer/ | Tokenizer interface + Tiktoken (BPE) / HuggingFace vocab |
transformer/ | Shared primitives: Attention, FeedForward, RoPE |
model/llama/ | LLaMA-3: Llama, GQA, blocks, args |
model/qwen/ | Qwen3.5 hybrid text stack + vision tower |
engine/ | Continuous-batching runtime, speculative decoding |
cache/ | Paged KV pool + radix prefix reuse |
attention/, quant/, parallel/ | Backends, weight quantization, tensor parallelism |
template/, tool/ | Chat templates and tool-call parsers |
Core Types
| Type | Kind | Purpose |
|---|---|---|
Role | enum | system, user, assistant, tool |
Message | record | Dialog turn — role + ordered ContentParts, optional tool fields |
ContentPart | sealed interface | TextPart, ImageUrlPart, VideoUrlPart, AudioUrlPart |
FinishReason | enum | stop, length, tool_calls |
ChatCompletion | record | Result — text, tokens, log-probs, finish reason, tool calls |
ChatOptions | record | Per-request tools / toolChoice / parallel tool calls |
LanguageModel | interface | Single-prompt façade for chat-capable decoder LLMs |
import smile.llm.Message;
import smile.llm.ChatCompletion;
Message[] dialog = {
Message.system("You are a helpful assistant."),
Message.user("What is the capital of France?")
};
ChatCompletion reply = llama.chat(dialog, 256, 0.6, 0.9, false, 0L, null);
System.out.println(reply.content()); // "The capital of France is Paris."
System.out.println(reply.reason()); // FinishReason.stop
Message also carries multimodal parts and tool fields. Concurrent multi-request scheduling belongs in InferenceEngine, not on LanguageModel.
import smile.llm.Message;
import smile.llm.Role;
import smile.llm.TextPart;
import smile.llm.ImageUrlPart;
import java.util.List;
// Multimodal turn: text interleaved with an image
Message turn = new Message(Role.user, List.of(
new TextPart("What is in this picture?"),
new ImageUrlPart("https://example.com/cat.jpg")));
// Assistant tool call + tool result
Message call = new Message(Role.assistant, List.of(),
List.of(toolCall), null, null);
Message result = new Message(Role.tool, "{\"temp\": 21}",
null, toolCall.id(), null);
Passing a non-null cancelRequested BooleanSupplier enables cooperative cancel: the implementation checks it between decode steps and throws CancellationException when it returns true (KV is still unbound in finally).
Tokenizer
Tokenizer is the encode/decode interface. Tiktoken is the BPE implementation compatible with OpenAI’s tiktoken (used by LLaMA-3). HuggingFaceBpeVocab loads a HuggingFace tokenizer.json into the same representation.
import smile.llm.tokenizer.Tiktoken;
Tiktoken tok = new Tiktoken(
pattern, // regex splitting pattern
specialTokens, // map of special-token string → rank
ranks, // merged BPE vocabulary (bytes → rank)
bosId, eosId // BOS / EOS token IDs
);
int[] ids = tok.encode("Hello, world!", true, true); // BOS+EOS
String text = tok.decode(ids);
int vocab = tok.size();
- BPE merge-table look-ups for regular tokens
- Special-token injection with a separate regex guard
- UTF-8-safe decoding (strict
tryDecodethrowsCharacterCodingExceptionon invalid bytes)
Positional Encodings
| Class | Algorithm | Used by |
|---|---|---|
PositionalEncoding | Sinusoidal (sin/cos, fixed) | Original Transformer |
RotaryPositionalEncoding | RoPE (complex-number rotation) | LLaMA |
Qwen3.5 uses partial / interleaved multi-axis RoPE (InterleavedMRope, PartialRotaryEncoding) for multimodal positions.
import smile.deep.tensor.Tensor;
import smile.llm.transformer.PositionalEncoding;
import smile.llm.transformer.RotaryPositionalEncoding;
PositionalEncoding pe = new PositionalEncoding(512, 2048);
Tensor out = pe.forward(embeddingTensor); // adds positional signal
RotaryPositionalEncoding rope =
new RotaryPositionalEncoding(headDim, maxSeqLen);
Transformer Primitives
smile.llm.transformer holds shared blocks reused by both model families:
| Class | Role |
|---|---|
Attention | Scalar-dot-product attention over the selected backend |
FeedForward | SwiGLU-style MLP block |
PositionalEncoding | Sinusoidal encoding |
RotaryPositionalEncoding | RoPE |
LLaMA
A full LLaMA-3 inference implementation in smile.llm.model.llama. High-level entry point: Llama with build(), generate(), and chat().
| Class | Role |
|---|---|
LlamaModelArgs | Hyperparameters from params.json / HF config.json |
LlamaModel | Embedding + N × LlamaBlock + output projection |
LlamaBlock | Decoder block: GQA + FeedForward + RMS norms |
GroupedQueryAttention | Grouped-query attention with KV-cache and RoPE |
Llama | High-level entry — load, generate, chat |
Loading a Checkpoint
Llama.build supports two on-disk layouts: Meta (params.json plus consolidated.*.pt shards) and HuggingFace (config.json plus *.safetensors, optionally indexed by model.safetensors.index.json). Overloads accept memFractionStatic (SGLang-style static-region fraction for weights + KV) and a KV-cache dtype override.
import smile.llm.model.llama.Llama;
// Loads params.json / config.json + checkpoint weights
Llama llama = Llama.build(
"model/Meta-Llama-3-8B-Instruct",
"model/Meta-Llama-3-8B-Instruct/tokenizer.model",
/*maxBatchSize=*/ 4,
/*maxSeqLen=*/ 2048,
/*deviceId=*/ (byte) 0 // CUDA:0; -1 for CPU
);
Generation and Chat
import smile.llm.ChatCompletion;
import smile.llm.Message;
// Raw token IDs
int[] prompt = llama.tokenizer.encode("Once upon a time", true, false);
ChatCompletion result = llama.generate(
prompt, /*maxGenLen=*/ 200, /*temperature=*/ 0.6,
/*topp=*/ 0.9, /*logprobs=*/ false, /*seed=*/ 42L,
/*listener=*/ null);
System.out.println(result.content());
// Dialog format
ChatCompletion reply = llama.chat(
new Message[]{
Message.system("Be concise."),
Message.user("Explain RoPE in one sentence.")
},
/*maxGenLen=*/ 128, /*temperature=*/ 0.7, /*topp=*/ 0.9,
/*logprobs=*/ false, /*seed=*/ 0L, /*listener=*/ null);
System.out.println(reply.content());
Streaming
Pass a GenerationListener to receive text chunks as they are produced. Serve wires this through GenerationListeners.toPublisher.
import smile.llm.GenerationListener;
GenerationListener listener = new GenerationListener() {
@Override public void onText(String chunk) {
System.out.print(chunk);
}
};
int[] prompt = llama.tokenizer.encode("Tell me a joke", true, false);
llama.generate(prompt, 200, 0.8, 0.95, false, 0L, listener);
Qwen3.5
Qwen implements the Qwen3.5 hybrid text stack: efficient linear attention (GatedDeltaNet) for most layers and full gated attention for the rest, with optional sparse MoE, a unified thinking / non-thinking mode, and a native vision tower for multimodal input. Checkpoints use HuggingFace tokenizer.json (resolved automatically from the checkpoint directory).
| Class | Role |
|---|---|
QwenModelArgs | Hyperparameters from config.json |
QwenModel / QwenBlock | Top-level decoder and per-layer block |
GatedDeltaNet / GatedAttention | Linear vs full gated attention cores |
QwenMtp | Multi-token-prediction head (speculative decoding) |
QwenVisionTower / QwenVlProcessor | Vision encoder and image/video preprocessing |
Qwen | High-level entry — build(), generate(), chat(), complete() |
import smile.llm.model.qwen.Qwen;
import smile.llm.ChatCompletion;
import smile.llm.Message;
Qwen qwen = Qwen.build(
"model/Qwen3.5-8B",
/*maxBatchSize=*/ 4,
/*maxSeqLen=*/ 4096, // <= 0 uses the config value
/*deviceId=*/ (byte) 0
);
ChatCompletion out = qwen.complete(
"Write a haiku about the sea.", 64, 0.7, 0.9, false, 0L, null);
ChatCompletion reply = qwen.chat(
new Message[]{
Message.system("You are a helpful assistant."),
Message.user("What are the key differences between Java and Kotlin?")
},
/*maxGenLen=*/ 512, /*temperature=*/ 0.7, /*topp=*/ 0.9,
/*logprobs=*/ false, /*seed=*/ 0L, /*listener=*/ null);
System.out.println(reply.content());
Overloads add memFractionStatic, a KV-cache dtype override, KV page size, ParallelConfig (tensor parallelism), and safetensors loader thread count. Speculative decoding (MTP) is enabled by default when MTP weights are present; prefix replay for hybrid models must be opted into explicitly:
qwen.setSpeculativeEnabled(true); // native MTP draft/verify
qwen.setPrefixReplayEnabled(true); // radix prefix + DeltaNet restore
Inference Engine
InferenceEngine is the continuous-batching runtime used by serve. It admits work up to maxInFlight while KV pages are free (Fluid Injection), prefills (optionally chunked under a token budget), then runs a batched decodeStep over all decoding requests. GenerationHandle.abort() performs Instant Eviction of queued and in-flight KV.
| Type | Role |
|---|---|
InferenceEngine | Scheduler + worker; submit(...) → GenerationHandle |
ModelExecutor | Low-level bind / prefill / decodeStep (implemented by Llama / Qwen) |
GenerationRequest | One job: tokens or dialog, sampling, optional multimodal / tools |
GenerationHandle | In-flight handle: future(), abort() |
SpeculativeDecoding | MTP draft / verify helpers |
Sampling | Temperature / top-p sampling |
import smile.llm.engine.InferenceEngine;
import smile.llm.engine.GenerationRequest;
import smile.llm.engine.GenerationHandle;
import smile.llm.ChatCompletion;
try (var engine = new InferenceEngine(llama, /*maxInFlight=*/ 8)) {
GenerationHandle handle = engine.submit(
GenerationRequest.ofTokens(
prompt, 256, 0.6, 0.9, false, 0L, listener));
ChatCompletion reply = handle.future().join();
}
Runtime introspection: queueSize(), inFlight(), kvFreeSlots(), kvFreePages(), activeDecodeCount(), and cumulative timing counters (queueWaitMsTotal(), prefillMsTotal(), decodeMsTotal()). Serve uses one prompt per GenerationRequest with smile.chat.max-batch-size as the in-flight cap; streaming disconnect calls GenerationHandle.abort().
KV Cache & Radix Prefix Reuse
KvCachePool is a paged KV-cache allocator. Pages are the unit of allocation (DEFAULT_PAGE_SIZE = 16 tokens). RadixCache organizes cached prefixes in a compressed radix tree so shared system prompts and conversation histories are computed once and reused across requests.
| Type | Role |
|---|---|
KvCachePool | Paged KV storage; bind / put / get / finishRequest |
RadixCache | Longest-prefix match / insert over token sequences |
KvCacheLayout | Layer / head / dim layout descriptor |
FlashInferKvMetadata | CSR metadata for the FlashInfer attention path |
KvCacheExhaustedException | Thrown when no free pages remain |
import smile.llm.cache.KvCachePool;
import smile.torch.ScalarType;
KvCachePool.StaticKvBudget budget =
KvCachePool.computeStaticKvBudget(...);
KvCachePool pool = KvCachePool.allocate(
layout, device, ScalarType.BFloat16, budget);
pool.setPrefixReuseEnabled(true); // radix match/insert
Prefix reuse is enabled per model via Llama.setPrefixReuseEnabled(...) / Qwen.setPrefixReplayEnabled(...).
Attention Backends
AttentionBackends.install(...) selects the process-wide attention kernel at chat-service startup:
| Backend | Notes |
|---|---|
TORCH_NATIVE | Default; pure LibTorch attention |
FLASHINFER | Requires libsmile_torch built with USE_FLASHINFER; falls back to TORCH_NATIVE with a warning when unavailable |
import smile.llm.attention.AttentionBackends;
import smile.llm.attention.AttentionBackend;
AttentionBackends.install(AttentionBackend.FLASHINFER);
Quantization
Weight-quantization backends for memory- and bandwidth-bound inference. The checkpoint format is detected automatically and resolved to a GEMM backend:
| QuantFormat | Meaning |
|---|---|
DENSE | Dense BF16 / FP16 / FP32 weights |
FP8 | Native FP8 (e4m3 / e5m2) weights + scales |
NVFP4 | Native NVFP4 weights (Blackwell) |
GPTQ | HuggingFace GPTQ INT4 |
AWQ | HuggingFace AWQ INT4 |
import smile.llm.quant.QuantPolicy;
QuantPolicy.Resolved resolved =
QuantPolicy.resolve(checkpointDir, device, backendOverride);
// resolved.format() → QuantFormat
// resolved.backend() → WeightGemmBackend
// (FP8 / NVFP4 primary, Marlin Ampere failover)
QuantLinearFactory builds the appropriate linear layer (Fp8Linear, Fp8BlockLinear, Nvfp4Linear, MarlinLinear) for the resolved format. Native FP8 checkpoints (e.g. Qwen/Qwen3.8-27B-FP8) need Hopper+ GPUs (sm_90+).
Tensor Parallelism
In-process tensor parallelism for multi-GPU inference. Pipeline axes are reserved for future multi-node PP.
| Type | Role |
|---|---|
ParallelConfig | (tpSize, ppSize, dpSize, devices); single(...), tensorParallel(...) |
TensorParallelGroup | Rank group + collective ops |
ColumnParallelLinear / RowParallelLinear | Sharded linear layers |
WeightSharding / TensorShardSpec | Weight partitioning |
import smile.llm.parallel.ParallelConfig;
import smile.llm.model.qwen.Qwen;
ParallelConfig tp = ParallelConfig.tensorParallel((byte) 0, (byte) 1);
Qwen qwen = Qwen.build(dir, /*maxBatchSize=*/ 8, /*maxSeqLen=*/ 8192,
(byte) 0, /*memFractionStatic=*/ 0.85, "bfloat16", tp);
Tool Calling & Chat Templates
ChatOptions carries the tools and selection policy for a request; the model injects them into the chat template and parses tool calls out of the output.
import smile.llm.ChatOptions;
import smile.llm.ToolDefinition;
import smile.llm.FunctionDefinition;
import smile.llm.ToolChoice;
ChatOptions options = new ChatOptions(
new ToolDefinition[]{ new ToolDefinition(new FunctionDefinition(
"get_weather", "Get the current weather", parametersSchema)) },
ToolChoice.AUTO,
/*parallelToolCalls=*/ true);
int[] prompt = model.encodeChat(dialog, options);
| Type | Role |
|---|---|
ChatTemplate | Interface for model-specific prompt formatting |
Qwen3ChatTemplate | Qwen3 template (thinking / non-thinking, tools) |
ToolCallParser | Interface for parsing tool calls from generated text |
JsonToolCallParser / Qwen3XmlToolCallParser | Concrete parsers |
AssistantTextSanitizer | Strips tool-call markup from assistant text |
Serving
SMILE Serve is a production Quarkus inference server for classic ML, ONNX, and OpenAI-compatible LLM chat — with a bundled React web UI. See the SMILE Serve page for Docker quick start, configuration, OpenAI-compatible chat completions (including Qwen3+ tool calling), the supported models, and the web UI.
The smile.llm package ships native implementations of Llama-3.1 and Qwen3.5/3.8. All other model families — Phi, Mistral, Gemma, Granite, gpt-oss, DeepSeek, and more — are served through ONNX Runtime GenAI from a genai_config.json package; the full list of supported architectures and GenAI-ready checkpoints is on the SMILE Serve and ONNX GenAI pages.