Skip to content

Commit 7f0706e

Browse files
committed
litert-lm: fix null backends, suppress logs, patch GPU factory method
Three fixes that together make end-to-end text generation work on CPU and unblock GPU: 1. Null vision/audio backend: the C API (engine.cc:299) checks `if (vision_backend_str)` before parsing — null means "auto-detect from model metadata, skip if absent". Passing non-null ("GPU"/"CPU") forced the engine to require encoder sections, failing on text-only models. EngineSettings now defaults vision/audio to None (= null). 2. Log suppression: Engine::new calls litert_lm_set_min_log_level(3) (ERROR-only) to suppress the wall of absl INFO/WARNING output that litert-lm-sys dumps during init. Clean output now shows only the prompt + response. 3. SamplerParams::top_p() now implies Sampler::TopP (was leaving the default TopK, which the CPU backend rejects as unimplemented). 4. GPU fix (build workflow): patch engine.cc to call EngineFactory::CreateAny instead of CreateDefault. The CLI (litert_lm_main.cc) uses CreateAny which picks an optimized engine type; CreateDefault forces kLiteRTCompiledModel which hangs during Metal shader compilation. Rebuild required — triggered separately. Verified: Qwen3-0.6B generates text on CPU via litert-lm safe API.
1 parent 18d168c commit 7f0706e

4 files changed

Lines changed: 69 additions & 32 deletions

File tree

.github/workflows/build-litert-lm.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ jobs:
3232
git clone --depth=1 --branch=${{ env.LITERT_LM_TAG }} \
3333
https://github.com/google-ai-edge/litert-lm.git /tmp/litert-lm
3434
35+
# Patch engine.cc: CreateDefault → CreateAny so the GPU path uses the
36+
# optimized engine type (same as the upstream litert-lm CLI).
37+
- name: patch engine.cc (CreateDefault → CreateAny)
38+
run: |
39+
sed -i 's/EngineFactory::CreateDefault/EngineFactory::CreateAny/g' \
40+
/tmp/litert-lm/c/engine.cc
41+
grep -n 'EngineFactory::Create' /tmp/litert-lm/c/engine.cc
42+
3543
# Inject a cc_binary(linkshared=True) target so Bazel itself produces
3644
# the .so with full dep resolution — no manual post-link needed.
3745
- name: create shared lib BUILD target
@@ -83,6 +91,12 @@ jobs:
8391
git clone --depth=1 --branch=${{ env.LITERT_LM_TAG }} \
8492
https://github.com/google-ai-edge/litert-lm.git /tmp/litert-lm
8593
94+
- name: patch engine.cc (CreateDefault → CreateAny)
95+
run: |
96+
sed -i '' 's/EngineFactory::CreateDefault/EngineFactory::CreateAny/g' \
97+
/tmp/litert-lm/c/engine.cc
98+
grep -n 'EngineFactory::Create' /tmp/litert-lm/c/engine.cc
99+
86100
- name: create shared lib BUILD target
87101
run: |
88102
mkdir -p /tmp/litert-lm/shared_build

litert-lm/examples/llm_generate.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,31 @@ fn main() -> Result<(), Box<dyn Error>> {
4747
println!("model: {} ({})", model_path.display(), model_info.label);
4848
println!("loading engine...");
4949

50-
let use_cpu = args.iter().any(|a| a == "--cpu");
51-
let backend = if use_cpu { Backend::Cpu } else { Backend::Gpu };
50+
let backend = if args.iter().any(|a| a == "--cpu") {
51+
Backend::Cpu
52+
} else {
53+
Backend::Gpu
54+
};
5255
println!("backend: {backend:?}");
5356
let cache_dir = std::env::temp_dir().join("litert-lm-cache");
5457
fs::create_dir_all(&cache_dir)?;
55-
let mut settings = EngineSettings::new(&model_path)
56-
.backend(backend)
57-
.max_num_tokens(512)
58-
.cache_dir(&cache_dir);
59-
// For text-only models on CPU: vision/audio backends must be GPU
60-
// (upstream quirk — CPU path fatally requires encoder sections).
61-
if use_cpu {
62-
settings = settings
63-
.vision_backend(Backend::Gpu)
64-
.audio_backend(Backend::Gpu);
65-
}
66-
let engine = Engine::new(settings)?;
58+
let engine = Engine::new(
59+
EngineSettings::new(&model_path)
60+
.backend(backend)
61+
.max_num_tokens(512)
62+
.cache_dir(&cache_dir),
63+
)?;
6764

6865
println!("creating session...");
69-
let mut session =
70-
engine.create_session(SamplerParams::default().top_k(40).temperature(0.7).seed(42))?;
66+
// Use TopP (nucleus) sampling — matches the model's own metadata and
67+
// is supported on both CPU and GPU backends. TopK is not yet implemented
68+
// on CPU upstream.
69+
let mut session = engine.create_session(
70+
SamplerParams::default()
71+
.top_p(0.95)
72+
.temperature(0.7)
73+
.seed(42),
74+
)?;
7175

7276
let prompt = "Explain Rust lifetimes in one sentence.";
7377
println!("prompt: {prompt}");

litert-lm/src/engine.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,25 @@ impl Backend {
4949
pub struct EngineSettings {
5050
model_path: PathBuf,
5151
backend: Backend,
52-
vision_backend: Backend,
53-
audio_backend: Backend,
52+
/// None = auto-detect from model metadata (null in C API).
53+
vision_backend: Option<Backend>,
54+
/// None = auto-detect from model metadata (null in C API).
55+
audio_backend: Option<Backend>,
5456
max_num_tokens: Option<i32>,
5557
cache_dir: Option<PathBuf>,
5658
}
5759

5860
impl EngineSettings {
5961
/// Create settings for a model file at the given path.
60-
/// Defaults to [`Backend::Gpu`] for all modalities.
62+
/// Main backend defaults to [`Backend::Gpu`]; vision/audio auto-detect
63+
/// from model metadata (null in C API → `EngineSettings::CreateDefault`
64+
/// decides).
6165
pub fn new(model_path: impl Into<PathBuf>) -> Self {
6266
Self {
6367
model_path: model_path.into(),
6468
backend: Backend::default(),
65-
vision_backend: Backend::default(),
66-
audio_backend: Backend::default(),
69+
vision_backend: None,
70+
audio_backend: None,
6771
max_num_tokens: None,
6872
cache_dir: None,
6973
}
@@ -76,19 +80,19 @@ impl EngineSettings {
7680
self
7781
}
7882

79-
/// Set the vision encoder backend. Models without a vision encoder
80-
/// ignore this setting.
83+
/// Set the vision encoder backend. `None` (default) auto-detects from
84+
/// model metadata; text-only models skip vision entirely.
8185
#[must_use]
8286
pub fn vision_backend(mut self, backend: Backend) -> Self {
83-
self.vision_backend = backend;
87+
self.vision_backend = Some(backend);
8488
self
8589
}
8690

87-
/// Set the audio encoder backend. Models without an audio encoder
88-
/// ignore this setting.
91+
/// Set the audio encoder backend. `None` (default) auto-detects from
92+
/// model metadata; text-only models skip audio entirely.
8993
#[must_use]
9094
pub fn audio_backend(mut self, backend: Backend) -> Self {
91-
self.audio_backend = backend;
95+
self.audio_backend = Some(backend);
9296
self
9397
}
9498

@@ -141,17 +145,31 @@ impl Engine {
141145
/// # Ok::<(), litert_lm::Error>(())
142146
/// ```
143147
pub fn new(settings: EngineSettings) -> Result<Self> {
148+
// Suppress the wall of INFO/WARNING logs from absl + LiteRT internals.
149+
// 2 = WARNING, 3 = ERROR. We set to ERROR-only by default.
150+
unsafe { sys::litert_lm_set_min_log_level(3) };
151+
144152
let model_str = path_to_cstring(&settings.model_path)?;
145153
let backend_str = CString::new(settings.backend.as_str()).unwrap();
146-
let vision_str = CString::new(settings.vision_backend.as_str()).unwrap();
147-
let audio_str = CString::new(settings.audio_backend.as_str()).unwrap();
154+
155+
// Vision/audio: None → null → C API auto-detects from model metadata.
156+
// Only pass a non-null string if the user explicitly set a backend
157+
// (e.g., for multimodal models with vision/audio encoder sections).
158+
let vision_cstr = settings
159+
.vision_backend
160+
.map(|b| CString::new(b.as_str()).unwrap());
161+
let audio_cstr = settings
162+
.audio_backend
163+
.map(|b| CString::new(b.as_str()).unwrap());
148164

149165
let raw_settings = unsafe {
150166
sys::litert_lm_engine_settings_create(
151167
model_str.as_ptr(),
152168
backend_str.as_ptr(),
153-
vision_str.as_ptr(),
154-
audio_str.as_ptr(),
169+
vision_cstr
170+
.as_ref()
171+
.map_or(std::ptr::null(), |s| s.as_ptr()),
172+
audio_cstr.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
155173
)
156174
};
157175
if raw_settings.is_null() {

litert-lm/src/sampler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ impl SamplerParams {
6666
self
6767
}
6868

69-
/// Set the nucleus probability threshold.
69+
/// Set the nucleus probability threshold. Implies [`Sampler::TopP`].
7070
#[must_use]
7171
pub fn top_p(mut self, v: f32) -> Self {
7272
self.top_p = v;
73+
self.sampler = Sampler::TopP;
7374
self
7475
}
7576

0 commit comments

Comments
 (0)