bge_m3_embedding_server/embedder/worker/
startup.rs1use std::path::Path;
18use std::sync::atomic::Ordering;
19
20use anyhow::Result;
21use tokio::runtime::Handle;
22use tokio::sync::mpsc;
23use tracing::{info, info_span};
24
25use super::config::WorkerConfig;
26use super::prewarm_strict::should_fail_readiness;
27use super::probe::probe_run_dense;
28use crate::config::EpSelection;
29use crate::embedder::session::{GpuSessionConfig, load_models};
30use crate::embedder::sm_detect::detect_sm_for_device;
31use crate::embedder::trt_cache;
32#[cfg(feature = "cache-gc")]
33use crate::embedder::trt_cache_gc;
34use crate::embedder::trt_warmup::{
35 prewarm_persistence_postcondition_failed, prewarm_persistence_suspicious_undercount,
36 trt_prewarm,
37};
38use crate::sysinfo;
39
40pub(super) struct StartupOutcome {
42 pub initial_models: (ort::session::Session, tokenizers::Tokenizer),
43 pub detected_sm: Option<String>,
44}
45
46pub(super) fn detected_sm_for_ep(ep: EpSelection, device_id: u32) -> Option<String> {
48 if ep == EpSelection::TensorRt {
49 detect_sm_for_device(device_id)
50 } else {
51 None
52 }
53}
54
55#[allow(clippy::too_many_lines)]
57pub(super) fn startup_worker(
58 id: usize,
59 cache_dir: &Path,
60 ready_tx: &mpsc::Sender<Result<usize>>,
61 config: &WorkerConfig,
62 rt: &Handle,
63) -> Result<StartupOutcome> {
64 let span = info_span!("worker", id = id);
65 let _span_guard = span.enter();
66
67 tracing::debug!(
68 worker_id = id,
69 gpu_device = config.device_id,
70 ep = %config.ep,
71 "worker assigned to GPU device"
72 );
73 info!("Loading models (worker {id})...");
74 let load_start = std::time::Instant::now();
75
76 let pre_load_rss = sysinfo::read_process_rss_bytes().unwrap_or(0);
77 let mut initial_models = match load_models(
78 &GpuSessionConfig {
79 cache_dir,
80 model_variant: config.model_variant,
81 max_seq_length: config.max_seq_length,
82 intra_threads: config.intra_threads,
83 ep: config.ep,
84 device_id: config.device_id,
85 trt_max_workspace_bytes: config.trt_max_workspace_bytes,
86 gpu_mem_limit_bytes: config.gpu_mem_limit_bytes,
87 },
88 id == 0,
89 ) {
90 Ok(mut models) => {
91 let prime_ids = ndarray::Array2::<i64>::zeros((1, 8));
106 let prime_mask = ndarray::Array2::<i64>::ones((1, 8));
107 match probe_run_dense(&mut models.0, &prime_ids, &prime_mask) {
108 Ok(_) => {
109 tracing::debug!("Worker {id} arena primed");
110 }
111 Err(e) => {
112 tracing::warn!(
113 error = %e,
114 "Worker {id} arena prime failed; first probe shape on this \
115 worker will include arena init delta"
116 );
117 }
118 }
119
120 let post_load_rss = sysinfo::read_process_rss_bytes().unwrap_or(pre_load_rss);
121 tracing::info!(
122 elapsed_ms = load_start.elapsed().as_millis(),
123 rss_delta_mb = post_load_rss.saturating_sub(pre_load_rss) / (1024 * 1024),
124 "Models loaded (worker {id})"
125 );
126 models
127 }
128 Err(e) => {
129 let _ =
130 rt.block_on(ready_tx.send(Err(anyhow::anyhow!("Worker {id} failed to load: {e}"))));
131 return Err(e);
132 }
133 };
134
135 #[cfg(feature = "cache-gc")]
145 if id == 0 && config.trt_cache_gc_enabled && config.ep != EpSelection::Cpu {
146 let engine_cache_dir = trt_cache::engine_cache_path(cache_dir);
147 match detect_sm_for_device(config.device_id) {
148 Some(current_sm) => {
149 tracing::warn!(
150 target: "bge_m3_embedding_server::trt_cache_gc",
151 worker_id = id,
152 gpu_device = config.device_id,
153 current_sm = %current_sm,
154 cache_path = %engine_cache_dir.display(),
155 sidecar_suffixes = ?trt_cache_gc::ENGINE_SIDE_SUFFIXES,
156 "destructive cache GC about to run: will delete every \
157 `_smXX.engine` whose XX != current SM, plus aligned \
158 sidecars. DO NOT use this build against a shared EFS \
159 engine cache in a multi-SM ASG"
160 );
161 let stats = trt_cache_gc::gc_stale_sm_plans(&engine_cache_dir, ¤t_sm);
162 tracing::warn!(
163 target: "bge_m3_embedding_server::trt_cache_gc",
164 worker_id = id,
165 gpu_device = config.device_id,
166 current_sm = %current_sm,
167 plans_deleted = stats.plans_deleted,
168 bytes_freed = stats.bytes_freed,
169 other_sms_observed = ?stats.other_sms_observed,
170 cache_path = %engine_cache_dir.display(),
171 "destructive cache GC ran: deleted other-SM engine plans \
172 from shared cache"
173 );
174 }
175 None => {
176 tracing::warn!(
177 target: "bge_m3_embedding_server::trt_cache_gc",
178 worker_id = id,
179 gpu_device = config.device_id,
180 "destructive cache GC requested but current GPU compute \
181 capability could not be detected (no nvidia-smi or \
182 unparseable output); skipping GC"
183 );
184 }
185 }
186 }
187
188 let detected_sm: Option<String> = if config.ep == EpSelection::TensorRt {
198 let sm = detected_sm_for_ep(config.ep, config.device_id);
199 if sm.is_none() {
200 tracing::warn!(
201 worker_id = id,
202 gpu_device = config.device_id,
203 "trt cache: nvidia-smi compute-capability detection failed; \
204 falling back to unfiltered engine cache counts. This re-introduces \
205 the heterogeneous-SM false-positive risk — install nvidia-smi on \
206 the container or check that the GPU is accessible."
207 );
208 }
209 sm
210 } else {
211 None
212 };
213
214 if config.ep == EpSelection::TensorRt && !config.trt_warmup_shapes.is_empty() {
222 let engine_cache_dir = trt_cache::engine_cache_path(cache_dir);
223 let total_engine_count = trt_cache::count_engine_files(&engine_cache_dir);
224 let matching_engine_count =
225 trt_cache::count_engine_files_for_sm(&engine_cache_dir, detected_sm.as_deref());
226 tracing::info!(
232 target: "bge_m3_embedding_server::trt_cache",
233 worker_id = id,
234 device_id = config.device_id,
235 detected_sm = detected_sm.as_deref().unwrap_or("unfiltered"),
236 cache_path = %engine_cache_dir.display(),
237 matching_engine_count,
238 total_engine_count,
239 "trt cache: SM-filtered engine plan enumeration"
240 );
241 tracing::info!(
242 worker_id = id,
243 gpu_device = config.device_id,
244 detected_sm = detected_sm.as_deref().unwrap_or("unfiltered"),
245 shape_count = config.trt_warmup_shapes.len(),
246 shapes = ?config.trt_warmup_shapes,
247 "TensorRT pre-warm: worker compiling shard \
248 (first run per shape takes 30–170 s; subsequent starts reuse cache)"
249 );
250 trt_cache::log_engine_basenames_before_prewarm_for_sm(
251 &engine_cache_dir,
252 detected_sm.as_deref(),
253 );
254 let stats = trt_prewarm(
255 &mut initial_models.0,
256 &config.trt_warmup_shapes,
257 id,
258 cache_dir,
259 detected_sm.as_deref(),
260 );
261 if prewarm_persistence_postcondition_failed(stats.fresh_compiles, stats.engine_count_after)
268 {
269 tracing::error!(
270 worker_id = id,
271 gpu_device = config.device_id,
272 detected_sm = detected_sm.as_deref().unwrap_or("unfiltered"),
273 fresh_compiles = stats.fresh_compiles,
274 engine_count_before = stats.engine_count_before,
275 engine_count_after = stats.engine_count_after,
276 engine_count_delta = stats.engine_count_delta,
277 cache_path = %engine_cache_dir.display(),
278 "TensorRT pre-warm postcondition failed: \
279 compile-success events present but no .engine files on disk \
280 for this SM; TRT EP may be silently failing to persist engine \
281 plan files (or building plans for a different SM than this \
282 worker's device)"
283 );
284 } else if prewarm_persistence_suspicious_undercount(
285 stats.fresh_compiles,
286 stats.engine_count_after,
287 ) {
288 tracing::warn!(
296 worker_id = id,
297 gpu_device = config.device_id,
298 detected_sm = detected_sm.as_deref().unwrap_or("unfiltered"),
299 fresh_compiles = stats.fresh_compiles,
300 engine_count_before = stats.engine_count_before,
301 engine_count_after = stats.engine_count_after,
302 engine_count_delta = stats.engine_count_delta,
303 cache_path = %engine_cache_dir.display(),
304 "TensorRT pre-warm: engine_count_delta is suspiciously low \
305 relative to fresh_compiles (delta * 2 < fresh_compiles); \
306 some engine plans may not have persisted to disk despite \
307 session.run() reporting Ok — investigate cache path \
308 resolution and EFS mount durability"
309 );
310 }
311 tracing::info!(
312 worker_id = id,
313 detected_sm = detected_sm.as_deref().unwrap_or("unfiltered"),
314 warmed = stats.warmed,
315 skipped = stats.skipped,
316 fully_cached = stats.fully_cached,
317 fresh_compiles = stats.fresh_compiles,
318 engine_count_before = stats.engine_count_before,
319 engine_count_after = stats.engine_count_after,
320 engine_count_delta = stats.engine_count_delta,
321 total = config.trt_warmup_shapes.len(),
322 total_compile_ms = stats.total_compile_ms,
323 total_fsync_ms = stats.total_fsync_ms,
324 max_warmed_seq = stats.max_warmed_seq,
325 "TensorRT pre-warm complete"
326 );
327
328 let prev_ceiling = config
336 .warmed_seq_ceiling
337 .fetch_max(stats.max_warmed_seq, Ordering::AcqRel);
338 tracing::info!(
339 target: "bge_m3_embedding_server::trt_warmup",
340 worker_id = id,
341 max_warmed_seq = stats.max_warmed_seq,
342 warmed_seq_ceiling = prev_ceiling.max(stats.max_warmed_seq),
343 "in-band JIT guard: warmed-seq ceiling updated after prewarm"
344 );
345
346 if should_fail_readiness(
355 stats.fresh_compiles,
356 stats.engine_count_after,
357 config.prewarm_strict,
358 ) {
359 tracing::error!(
360 target: "bge_m3_embedding_server::prewarm",
361 worker_id = id,
362 gpu_device = config.device_id,
363 fresh_compiles = stats.fresh_compiles,
364 engine_count_before = stats.engine_count_before,
365 engine_count_after = stats.engine_count_after,
366 cache_path = %trt_cache::engine_cache_path(cache_dir).display(),
367 prewarm_strict = config.prewarm_strict,
368 "Prewarm postcondition failed and prewarm_strict=1: \
369 refusing to signal ready"
370 );
371 let err = anyhow::anyhow!(
372 "Worker {id} prewarm postcondition failed \
373 (fresh_compiles={}, engine_count_after={}); \
374 prewarm_strict=1: refusing to signal ready",
375 stats.fresh_compiles,
376 stats.engine_count_after,
377 );
378 return Err(err);
379 }
380 }
381
382 let post_load_rss = sysinfo::read_process_rss_bytes().unwrap_or(pre_load_rss);
385 let rss_delta = post_load_rss.saturating_sub(pre_load_rss);
386 info!(
387 "Worker {id} models loaded — signaling ready (rss_delta_mb={})",
388 rss_delta / (1024 * 1024)
389 );
390 let _ = rt.block_on(ready_tx.send(Ok(rss_delta)));
391
392 Ok(StartupOutcome {
393 initial_models,
394 detected_sm,
395 })
396}