Model Loading and Inference
Documentation · Training · 中文 | 日本語 | Deutsch | Русский
ruLLM provides model loading, tokenization, cached autoregressive generation, and request scheduling. Its Cargo package is ruLLM; its Rust import name is rullm.
Prepare a local model
Keep the model files in one directory:
| File | Purpose |
|---|---|
config.json |
Model architecture and parameters |
model.safetensors |
Single-file weights |
model.safetensors.index.json and every listed shard |
Sharded weights, instead of a single weight file |
tokenizer.json |
Text tokenization and decoding |
tokenizer_config.json |
ChatML template required by Qwen2/Qwen2.5 pipelines |
generation_config.json |
Optional Qwen2/Qwen2.5 generation configuration supplying EOS settings |
preprocessor_config.json |
Qwen3.5 image preprocessing configuration, required for image input |
Loaders accept local directories and do not download models. Select the loader for your architecture:
| Model/input | Loader |
|---|---|
| Unquantized Llama text | load_huggingface_llama_pipeline::<B> |
| Unquantized Qwen2/Qwen2.5 text | load_huggingface_qwen2_pipeline::<B> |
| AWQ GEMM INT4 Qwen2/Qwen2.5 | load_huggingface_awq_qwen2_pipeline::<R> |
| Unquantized Qwen3.5 text | load_huggingface_qwen35_text::<B> |
| Qwen3.5 vision encoder | load_huggingface_qwen35_vision::<B> |
| Qwen3.5 images and text | load_huggingface_qwen35_multimodal::<B> |
B is a tensor Backend; R is a device Runtime. The Qwen3.5 text loader uses a qwen3_5_text configuration with per-layer layer_types and rejects quantization configurations. Do not pass AWQ files to a floating-point loader.
Run the text generation examples
After setting up the environment, run these commands from the source root. The relative paths point to two local model directories:
$env:RUDA_CUDA_COMPILER = 'nvrtc'
cargo run --locked -p ruda-llm --features nvidia --example qwen2_generate -- ./models/qwen2 "Hello" 32 1
The Qwen3.5 example defaults to Ruda IR → PTX when RUDA_CUDA_COMPILER is unset. Use --release for optimized execution. Unsupported direct PTX operations return errors rather than falling back to NVRTC. The PTX version must match the target GPU and driver; see the PTX backend reference.
Remove-Item Env:RUDA_CUDA_COMPILER -ErrorAction SilentlyContinue
$env:RUDA_PTX_VERSION = '8.0'
cargo run --release --locked -p ruda-llm --features nvidia-ptx --example qwen35_generate -- ./models/qwen35 "Hello" 8 1
The CUDA C++ / NVRTC path remains available with an explicit selection:
$env:RUDA_CUDA_COMPILER = 'nvrtc'
cargo run --release --locked -p ruda-llm --features nvidia-ptx --example qwen35_generate -- ./models/qwen35 "Hello" 8 1
Arguments are the model directory, raw text prompt, maximum new tokens, and run count. The last two default to 32 and 1 for Qwen2, or 8 and 1 for Qwen3.5. Run count must be positive. Examples print JSON lines for loading and generation. Generation lines include text, generated_token_ids, and stopped_on_eos.
These examples encode the prompt directly without adding a chat template. For conversational models, prepare the prompt with render_chat as below.
Generate a chat reply in an application
For an application directory alongside the RUDA source directory, use these dependencies.
[dependencies]
rullm = { package = "ruLLM", path = "../RUDA/ruLLM", features = ["nvidia"] }
ruda-tensor-device = { path = "../RUDA/ruda-tensor-device", default-features = false, features = ["std", "cuda"] }
ruda-driver-cuda = { path = "../RUDA/ruda-driver-cuda", default-features = false, features = ["std"] }
half = "=2.7.1"
This complete src/main.rs accepts a model directory and user message as command-line arguments. Later function examples reuse its B alias, imports, and dependencies:
use half::bf16;
use ruda_tensor_device::cuda::{Cuda, CudaDevice};
use rullm::{
GreedyGenerationConfig, Qwen2ChatMessage, Qwen2ChatRole,
load_huggingface_qwen2_pipeline,
};
use std::{error::Error, io};
type B = Cuda<bf16, i32>;
fn main() -> Result<(), Box<dyn Error>> {
let mut args = std::env::args().skip(1);
let directory = args.next().ok_or_else(|| io::Error::other("model directory required"))?;
let prompt = args.next().ok_or_else(|| io::Error::other("prompt required"))?;
let device = CudaDevice::default();
let pipeline = load_huggingface_qwen2_pipeline::<B>(&directory, &device)?;
let messages = [Qwen2ChatMessage::new(Qwen2ChatRole::User, prompt)];
let prompt = pipeline.render_chat(&messages, true)?;
let output = pipeline.generate_text(
&prompt,
GreedyGenerationConfig {
max_new_tokens: 32,
eos_token_ids: pipeline.loaded.default_eos_token_ids.clone(),
},
false,
true,
&device,
)?;
println!("{}", output.generated_text);
Ok(())
}
render_chat handles plain-text Qwen2/Qwen2.5 ChatML messages. Passing true adds the assistant generation prefix. Roles also include System and Assistant; supply messages in conversation order. This API does not accept tool-call messages.
The two boolean arguments to generate_text are add_special_tokens and skip_special_tokens. Do not add special tokens again after rendering ChatML; decoding can skip them. generated_text contains only the new reply, while text includes both prompt and reply.
Temperature, top-k, and top-p sampling
Pass the rendered prompt to generate_text_sampled:
use rullm::{
GeneratedText, HuggingFaceLoadError, HuggingFaceQwen2Pipeline,
SamplingConfig, SamplingGenerationConfig,
};
fn sample_text(
pipeline: &HuggingFaceQwen2Pipeline<B>,
prompt: &str,
device: &CudaDevice,
) -> Result<GeneratedText, HuggingFaceLoadError> {
pipeline.generate_text_sampled(
prompt,
SamplingGenerationConfig {
max_new_tokens: 32,
eos_token_ids: pipeline.loaded.default_eos_token_ids.clone(),
sampling: SamplingConfig {
temperature: 0.8,
top_k: 40,
top_p: 0.9,
seed: Some(42),
},
},
false,
true,
device,
)
}
| Parameter | Values and effect |
|---|---|
temperature |
Finite and positive; scales logits first |
top_k |
Keeps high-scoring tokens next; 0 disables filtering, ties at the boundary are retained |
top_p |
Finally filters by cumulative probability; range (0, 1], with 1 disabling filtering |
seed |
Request-local sampling seed; None uses system entropy |
max_new_tokens |
Limits new tokens, excluding the prompt |
eos_token_ids |
Stops after generating any listed token |
Use generate_text for maximum-logit decoding rather than setting temperature to zero. A fixed seed fixes the sampling random sequence, not numerical consistency across models or backends.
Load AWQ weights
The AWQ loader takes CudaRuntime, not Cuda<bf16, i32>, as its generic parameter:
use ruda_driver_cuda::CudaRuntime;
use rullm::{HuggingFaceAwqQwen2Pipeline, load_huggingface_awq_qwen2_pipeline};
fn load_awq(
directory: &str,
device: &CudaDevice,
) -> Result<HuggingFaceAwqQwen2Pipeline<CudaRuntime>, HuggingFaceLoadError> {
load_huggingface_awq_qwen2_pipeline::<CudaRuntime>(directory, device)
}
The returned pipeline also provides render_chat, encode, decode, generate_text, and generate_text_sampled. AWQ configuration requires quant_method = "awq", bits = 4, version = "gemm", and zero_point = true. group_size must be positive or -1. Linear projections use F16 activations. See ruBLAS INT4 for packed weight layouts.
Qwen3.5 image input
generate_image_files_greedy takes token IDs for one prompt, JPEG/PNG paths in prompt order, an image processor, and generation options:
use rullm::{
Qwen35ImageProcessor, TokenGenerationOutput,
load_huggingface_qwen35_multimodal,
};
fn generate_from_images(
directory: &str,
prompt_ids: &[i32],
paths: &[&str],
device: &CudaDevice,
) -> Result<TokenGenerationOutput, Box<dyn Error>> {
let model = load_huggingface_qwen35_multimodal::<B>(directory, device)?;
let processor = Qwen35ImageProcessor::from_huggingface(directory)?;
let output = model.generate_image_files_greedy(
prompt_ids,
paths,
&processor,
&GreedyGenerationConfig {
max_new_tokens: 32,
eos_token_ids: vec![model.text.config.eos_token_id],
},
)?;
Ok(output)
}
Encode prompt_ids with the model's tokenizer.json. Follow the model's prompt template, keeping one placeholder token per image using image_token_id from config.json. Image paths must match placeholder count and order. This entry point expands placeholders internally; do not expand them beforehand. It handles still images and rejects video tokens.
For existing RGB8 data, use Qwen35RgbImage { pixels, height, width } with generate_rgb_greedy. Pixels are interleaved RGB bytes without row padding. For separate preprocessing, preprocess_files and preprocess_rgb return FP32 patches, a two-dimensional shape, and per-image grids.
When serving multiple requests, load the model and processor outside the request loop. Decode the returned generated_token_ids with the same tokenizer.
Caches and continuous batching
Single-request generation functions create and maintain a cache for that generation. To drive Qwen3.5 manually, call model.new_cache(), pass the prompt tensor to forward_cached_last(tokens, &mut cache), then pass only new tokens. Do not resubmit the entire cached prefix.
To integrate your own batch executor, use ContinuousBatchScheduler:
- Create it with
ContinuousBatchScheduler::new(config, kv_config).ContinuousBatchConfigsetsmax_active_sequencesandmax_batch_tokens.PagedKvCacheConfigsetsblock_size,num_pages, andmax_sequence_length. All must be positive. - Submit requests with
submit(prompt_token_ids, generation)and retain each returnedRequestId. - Call
schedule()for aScheduledBatch. Itskinddistinguishes Prefill/Decode;sequencessupplies each row's tokens, start position, context length, and block table. - Run the model in row order, write the corresponding KV pages, and select each row's next token. On success, call
complete_batch(batch.id, &generated_token_ids)with one result token per row. On failure, callfail_batch(batch.id)to cancel that batch's page reservations. - Retrieve completed requests and generated tokens with
pop_finished(), then continue scheduling.
The scheduler manages requests and page-allocation metadata. The caller's executor owns device KV storage and model execution. Use token_matrix(), context_lengths(), and flattened_block_table() to construct batch inputs. Complete or cancel an outstanding batch before calling schedule() again.
API reference: ruLLM exports, Generation options, Scheduler.
Qwen3.5 fused paged attention
Qwen35BatchCache selects its attention path when new_batch_cache creates it. Set RUDA_PAGED_ATTENTION=fused before cache creation to use ruDNN's fused paged attention in Qwen3.5 continuous batching. Unset or legacy retains the existing path; other values are rejected. Changing the variable does not switch an existing cache, and it is not a global switch for every generation entry point.
The fused path keeps a physical KV arena per full-attention layer and uses the scheduler's page tables for prefill/decode without gathering historical KV into padded tensors. Arena dimensions use num_pages, block_size, KV heads and head dimension; reserve GPU memory for that capacity. Inputs follow ruDNN's contiguous F32/F16/BF16 contract. Shared-prefix writes require scheduler-level copy-on-write. After a failed device append, rebuild the cache rather than continuing from its previous position.
Reusable attention and MoE components
rullm::gpu_inference exposes TensorOps, gqa_scores, gqa_value_product, mla_absorb_query, mla_scores and mla_value_product. These compose caller-supplied device tensor operations without repeating cached GQA heads or expanding historical MLA latent values. The caller supplies masking, softmax, positional encoding and model-specific scaling.
rullm::device_inference re-exports ruDNN's HostPlan, DevicePlan, PagedAttentionError, SwiGluExperts, RoutingOptions, GroupRoutingOptions, route_sigmoid_grouped and GroupedStrategy. These are building blocks, not a complete MLA/MoE model loader; adapters still provide projections, routing configuration and cache ownership.