Skip to content

Commit b8a6b50

Browse files
committed
feat: GGUF quantized FLUX support — auto-detect .gguf transformer, use Q4_1 for 24GB VRAM
1 parent fb0226b commit b8a6b50

1 file changed

Lines changed: 89 additions & 16 deletions

File tree

crates/mold-inference/src/engine.rs

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::{bail, Result};
22
use candle_core::{DType, Device, IndexOp, Module, Tensor};
33
use candle_nn::VarBuilder;
44
use candle_transformers::models::{clip, flux, t5};
5+
use candle_transformers::quantized_var_builder;
56
use mold_core::{GenerateRequest, GenerateResponse, ImageData, ModelPaths, OutputFormat};
67
use std::path::PathBuf;
78
use std::time::Instant;
@@ -14,10 +15,40 @@ pub trait InferenceEngine: Send + Sync {
1415
fn is_loaded(&self) -> bool;
1516
}
1617

18+
/// BF16 or quantized (GGUF) FLUX transformer.
19+
enum FluxTransformer {
20+
BF16(flux::model::Flux),
21+
Quantized(flux::quantized_model::Flux),
22+
}
23+
24+
impl FluxTransformer {
25+
fn denoise(
26+
&self,
27+
img: &Tensor,
28+
img_ids: &Tensor,
29+
txt: &Tensor,
30+
txt_ids: &Tensor,
31+
vec_: &Tensor,
32+
timesteps: &[f64],
33+
guidance: f64,
34+
) -> Result<Tensor> {
35+
match self {
36+
Self::BF16(m) => {
37+
flux::sampling::denoise(m, img, img_ids, txt, txt_ids, vec_, timesteps, guidance)
38+
.map_err(anyhow::Error::from)
39+
}
40+
Self::Quantized(m) => {
41+
flux::sampling::denoise(m, img, img_ids, txt, txt_ids, vec_, timesteps, guidance)
42+
.map_err(anyhow::Error::from)
43+
}
44+
}
45+
}
46+
}
47+
1748
/// Loaded FLUX model components, ready for inference.
1849
/// T5 and CLIP run on CPU to save VRAM; FLUX transformer and VAE run on GPU.
1950
struct LoadedFlux {
20-
flux_model: flux::model::Flux,
51+
flux_model: FluxTransformer,
2152
t5_model: t5::T5EncoderModel,
2253
t5_tokenizer: Tokenizer,
2354
clip_model: clip::text_model::ClipTextTransformer,
@@ -29,6 +60,8 @@ struct LoadedFlux {
2960
device: Device,
3061
dtype: DType,
3162
is_schnell: bool,
63+
/// True if using quantized GGUF model (state tensors must be F32)
64+
is_quantized: bool,
3265
}
3366

3467
/// FLUX inference engine backed by candle.
@@ -144,21 +177,41 @@ impl FluxEngine {
144177
let clip_tokenizer = Tokenizer::from_file(&self.clip_tokenizer_path)
145178
.map_err(|e| anyhow::anyhow!("failed to load CLIP tokenizer: {e}"))?;
146179

147-
// Load FLUX transformer on GPU (23GB BF16 — fits in 24GB VRAM)
148-
tracing::info!(path = %self.paths.transformer.display(), "loading FLUX transformer on GPU...");
180+
// Load FLUX transformer on GPU — GGUF quantized or BF16 safetensors
181+
let is_quantized = self
182+
.paths
183+
.transformer
184+
.extension()
185+
.and_then(|e| e.to_str())
186+
.map(|e| e.eq_ignore_ascii_case("gguf"))
187+
.unwrap_or(false);
188+
149189
let flux_cfg = if is_schnell {
150190
flux::model::Config::schnell()
151191
} else {
152192
flux::model::Config::dev()
153193
};
154-
let flux_vb = unsafe {
155-
VarBuilder::from_mmaped_safetensors(
156-
std::slice::from_ref(&self.paths.transformer),
157-
gpu_dtype,
158-
&device,
159-
)?
194+
195+
tracing::info!(
196+
path = %self.paths.transformer.display(),
197+
quantized = is_quantized,
198+
"loading FLUX transformer on GPU..."
199+
);
200+
201+
let flux_model = if is_quantized {
202+
let vb =
203+
quantized_var_builder::VarBuilder::from_gguf(&self.paths.transformer, &device)?;
204+
FluxTransformer::Quantized(flux::quantized_model::Flux::new(&flux_cfg, vb)?)
205+
} else {
206+
let flux_vb = unsafe {
207+
VarBuilder::from_mmaped_safetensors(
208+
std::slice::from_ref(&self.paths.transformer),
209+
gpu_dtype,
210+
&device,
211+
)?
212+
};
213+
FluxTransformer::BF16(flux::model::Flux::new(&flux_cfg, flux_vb)?)
160214
};
161-
let flux_model = flux::model::Flux::new(&flux_cfg, flux_vb)?;
162215
tracing::info!("FLUX transformer loaded on GPU");
163216

164217
// Load VAE on GPU (small, ~300MB)
@@ -189,6 +242,7 @@ impl FluxEngine {
189242
device,
190243
dtype: gpu_dtype,
191244
is_schnell,
245+
is_quantized,
192246
});
193247

194248
tracing::info!(model = %self.model_name, "all model components loaded successfully");
@@ -250,12 +304,28 @@ impl InferenceEngine for FluxEngine {
250304
};
251305
tracing::info!("CLIP encoding complete (moved to GPU)");
252306

253-
// 3. Generate initial noise
307+
// 3. Generate initial noise (F32 for quantized, gpu_dtype for BF16)
308+
let noise_dtype = if loaded.is_quantized {
309+
DType::F32
310+
} else {
311+
loaded.dtype
312+
};
254313
let img =
255-
flux::sampling::get_noise(1, height, width, &loaded.device)?.to_dtype(loaded.dtype)?;
314+
flux::sampling::get_noise(1, height, width, &loaded.device)?.to_dtype(noise_dtype)?;
315+
316+
// For quantized model, state tensors must be F32
317+
let (t5_emb_state, clip_emb_state, img_state) = if loaded.is_quantized {
318+
(
319+
t5_emb.to_dtype(DType::F32)?,
320+
clip_emb.to_dtype(DType::F32)?,
321+
img.to_dtype(DType::F32)?,
322+
)
323+
} else {
324+
(t5_emb.clone(), clip_emb.clone(), img.clone())
325+
};
256326

257327
// 4. Build sampling state
258-
let state = flux::sampling::State::new(&t5_emb, &clip_emb, &img)?;
328+
let state = flux::sampling::State::new(&t5_emb_state, &clip_emb_state, &img_state)?;
259329

260330
// 5. Get timestep schedule
261331
let timesteps = if loaded.is_schnell {
@@ -264,11 +334,14 @@ impl InferenceEngine for FluxEngine {
264334
flux::sampling::get_schedule(req.steps as usize, Some((state.img.dim(1)?, 0.5, 1.15)))
265335
};
266336

267-
tracing::info!(steps = timesteps.len(), "running denoising loop...");
337+
tracing::info!(
338+
steps = timesteps.len(),
339+
quantized = loaded.is_quantized,
340+
"running denoising loop..."
341+
);
268342

269343
// 6. Denoise
270-
let img = flux::sampling::denoise(
271-
&loaded.flux_model,
344+
let img = loaded.flux_model.denoise(
272345
&state.img,
273346
&state.img_ids,
274347
&state.txt,

0 commit comments

Comments
 (0)