Skip to main content

bge_m3_embedding_server/embedder/
session.rs

1// Copyright (c) 2026 J. Patrick Fulton
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! ORT execution-provider configuration and session loading.
16
17use std::path::Path;
18
19use anyhow::Result;
20
21use super::error::ort_err;
22use super::model_files::download_model_files;
23use super::tokenize::load_tokenizer;
24#[cfg(all(not(target_os = "macos"), feature = "tensorrt"))]
25use super::trt_cache;
26use crate::config::{EpSelection, ModelVariant};
27
28/// Returns the execution providers to use for this platform and EP selection.
29///
30/// On macOS: always uses the `CoreML` EP with `MLProgram` format and
31/// `FastPrediction` specialisation strategy (overridable via
32/// `BGE_M3_COREML_STRATEGY=default`), regardless of `ep`.
33///
34/// On Linux with the `tensorrt` feature: selects `TensorRT` when
35/// `ep == EpSelection::TensorRt`, with engine caching, FP16, and the
36/// specified `device_id` enabled. When `trt_max_workspace_bytes` is `Some`,
37/// the workspace cap is forwarded to the TRT EP via `with_max_workspace_size`;
38/// otherwise ORT's built-in default is used.
39///
40/// On Linux with the `cuda` feature: selects CUDA when
41/// `ep == EpSelection::Cuda`, pinned to `device_id`. When
42/// `gpu_mem_limit_bytes` is `Some`, the device memory limit is forwarded via
43/// `with_memory_limit`; otherwise the EP uses all available device memory.
44///
45/// CPU fallback: returns an empty list so ORT falls back to MLAS.
46///
47/// `device_id` is computed by `EmbedPool::spawn` as
48/// `worker_index % gpu_count` and is ignored on CPU EP and macOS.
49///
50/// Emits a single `INFO` log line tagged `"ORT execution providers configured"`
51/// describing the configured EP, the EP that was actually built (the "active"
52/// EP), and any cache paths handed to it. This is the source of truth in
53/// `CloudWatch` for "is `TensorRT` really active or did we silently fall back?".
54pub(super) fn execution_providers(
55    cache_dir: &Path,
56    ep: EpSelection,
57    device_id: u32,
58    trt_max_workspace_bytes: Option<usize>,
59    gpu_mem_limit_bytes: Option<usize>,
60) -> Vec<ort::ep::ExecutionProviderDispatch> {
61    // macOS: always CoreML regardless of BGE_M3_EP.
62    // The cfg blocks are mutually exclusive so only one branch is compiled per target.
63    #[cfg(target_os = "macos")]
64    {
65        let _ = device_id;
66        let _ = (trt_max_workspace_bytes, gpu_mem_limit_bytes);
67        let coreml_cache = cache_dir.join("coreml");
68        let strategy = match std::env::var("BGE_M3_COREML_STRATEGY").ok().as_deref() {
69            Some("default") => ort::ep::coreml::SpecializationStrategy::Default,
70            _ => ort::ep::coreml::SpecializationStrategy::FastPrediction,
71        };
72        let builder = ort::ep::CoreML::default()
73            .with_model_format(ort::ep::coreml::ModelFormat::MLProgram)
74            .with_specialization_strategy(strategy)
75            .with_model_cache_dir(coreml_cache.display().to_string());
76        #[cfg(feature = "coreml-profile")]
77        let builder = builder.with_profile_compute_plan(true);
78        tracing::info!(
79            ep_selection = %ep,
80            ep_active = "CoreML",
81            coreml_cache_path = %coreml_cache.display(),
82            "ORT execution providers configured"
83        );
84        vec![builder.build()]
85    }
86
87    #[cfg(not(target_os = "macos"))]
88    {
89        // Linux TensorRT (feature-gated).
90        // ort 2.0.0-rc.12 uses `with_engine_cache` / `with_fp16` — not
91        // `with_engine_cache_enable` / `with_fp16_enable` which don't exist.
92        #[cfg(feature = "tensorrt")]
93        if ep == EpSelection::TensorRt {
94            // Inspect the cache directory BEFORE handing the path to ORT so
95            // operators see in CloudWatch whether engine reuse is working.
96            // Two consecutive cold starts producing the same compile time is
97            // the symptom of an EFS mount that isn't actually persisting —
98            // surfacing the count here is the fastest way to diagnose it.
99            let cache_info = trt_cache::ensure_and_inspect(cache_dir);
100            trt_cache::log_cache_state(&cache_info);
101
102            let timing_cache = trt_cache::timing_cache_path(cache_dir);
103            // The timing cache stores per-tactic kernel timings so the TRT
104            // builder can skip the tactic-selection step on each subsequent
105            // engine build. It is complementary to the engine cache — even
106            // a cold engine cache benefits from a warm timing cache when
107            // multiple shapes are compiled in the same warmup sweep.
108            tracing::info!(
109                ep_selection = %ep,
110                ep_active = "TensorRT",
111                device_id,
112                engine_cache_path = %cache_info.path.display(),
113                timing_cache_path = %timing_cache.display(),
114                fp16 = true,
115                error_on_failure = true,
116                "ORT execution providers configured"
117            );
118            // `.error_on_failure()` upgrades the default silent-CPU-fallback
119            // path to a hard error. ORT's `apply_execution_providers` defaults
120            // `error_on_failure = false`, which means a failed registration
121            // (e.g. `libonnxruntime_providers_tensorrt.so` missing from the
122            // image — a common silent-CPU-fallback root cause) is logged as
123            // a `WARN`/`ERROR` via the `ort` crate's internal tracing macros
124            // and the loop falls back to CPU/MLAS without surfacing the
125            // failure. With this set, `Session::builder().with_execution_providers(...)`
126            // returns the error verbatim, which `load_session` already
127            // converts into a worker-load failure — the worker exits non-zero
128            // instead of silently serving CPU inference. Greppable in
129            // CloudWatch via the new field `error_on_failure: true` on the
130            // "ORT execution providers configured" event.
131            let mut trt_ep = ort::ep::TensorRT::default()
132                .with_device_id(device_id.cast_signed())
133                .with_engine_cache(true)
134                .with_engine_cache_path(cache_info.path.display().to_string())
135                .with_timing_cache(true)
136                .with_timing_cache_path(timing_cache.display().to_string())
137                .with_fp16(true);
138            if let Some(cap) = trt_max_workspace_bytes {
139                trt_ep = trt_ep.with_max_workspace_size(cap);
140            }
141            return vec![trt_ep.build().error_on_failure()];
142        }
143
144        // Linux CUDA (feature-gated).
145        #[cfg(feature = "cuda")]
146        if ep == EpSelection::Cuda {
147            let _ = cache_dir;
148            tracing::info!(
149                ep_selection = %ep,
150                ep_active = "CUDA",
151                device_id,
152                error_on_failure = true,
153                "ORT execution providers configured"
154            );
155            let mut cuda_ep = ort::ep::CUDA::default().with_device_id(device_id.cast_signed());
156            if let Some(limit) = gpu_mem_limit_bytes {
157                cuda_ep = cuda_ep.with_memory_limit(limit);
158            }
159            return vec![cuda_ep.build().error_on_failure()];
160        }
161
162        // CPU fallback (always available).
163        let _ = (
164            cache_dir,
165            device_id,
166            trt_max_workspace_bytes,
167            gpu_mem_limit_bytes,
168        );
169        tracing::info!(
170            ep_selection = %ep,
171            ep_active = "CPU/MLAS",
172            "ORT execution providers configured"
173        );
174        vec![]
175    }
176}
177
178/// Builds an ORT session from the ONNX model file with the given execution providers.
179///
180/// `intra_threads` controls intra-op parallelism for matmul / attention kernels
181/// inside a single `session.run()` call. The default (`1`) keeps per-worker RSS
182/// predictable for the workspace probe; raise it to `floor(num_cpus / workers)`
183/// on under-utilized hosts to recover CPU headroom. See
184/// [`crate::config::Config::intra_threads`] for the operator-facing knob.
185pub(super) fn load_session(
186    model_path: &Path,
187    eps: Vec<ort::ep::ExecutionProviderDispatch>,
188    intra_threads: usize,
189) -> Result<ort::session::Session> {
190    let mut builder = ort::session::Session::builder().map_err(ort_err)?;
191    if !eps.is_empty() {
192        builder = builder.with_execution_providers(eps).map_err(ort_err)?;
193    }
194    let session = builder
195        .with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)
196        .map_err(ort_err)?
197        .with_intra_threads(intra_threads.max(1))
198        .map_err(ort_err)?
199        .commit_from_file(model_path)
200        .map_err(ort_err)?;
201    Ok(session)
202}
203
204/// Configuration for loading an ORT session and tokenizer for a single GPU worker.
205///
206/// Bundles the parameters that were previously passed individually to [`load_models`],
207/// removing the 9-argument list and the associated `#[allow(clippy::too_many_arguments)]`
208/// suppression. All fields map 1:1 to the corresponding `WorkerConfig` fields.
209pub(super) struct GpuSessionConfig<'a> {
210    /// Path to the ONNX model cache directory.
211    pub cache_dir: &'a Path,
212    /// ONNX model variant (FP32, FP16, INT8).
213    pub model_variant: ModelVariant,
214    /// Maximum tokenized sequence length.
215    pub max_seq_length: usize,
216    /// Intra-op thread count for ORT sessions.
217    pub intra_threads: usize,
218    /// Execution provider selection.
219    pub ep: EpSelection,
220    /// GPU device ID for this worker.
221    pub device_id: u32,
222    /// Optional TRT EP workspace size cap in bytes.
223    pub trt_max_workspace_bytes: Option<usize>,
224    /// Optional CUDA EP device memory limit in bytes.
225    pub gpu_mem_limit_bytes: Option<usize>,
226}
227
228/// Downloads (if not already cached) and loads both the ORT session and the
229/// tokenizer for the given model variant, returning them as a pair.
230///
231/// `cfg.device_id` selects the CUDA/TRT GPU device for this session. Computed
232/// by `EmbedPool::spawn` as `worker_index % gpu_count`. Ignored on CPU EP and
233/// macOS.
234///
235/// `cfg.trt_max_workspace_bytes` and `cfg.gpu_mem_limit_bytes` are forwarded
236/// verbatim to [`execution_providers`]; see that function's documentation for
237/// semantics.
238pub(super) fn load_models(
239    cfg: &GpuSessionConfig<'_>,
240    show_download_progress: bool,
241) -> Result<(ort::session::Session, tokenizers::Tokenizer)> {
242    let files = download_model_files(cfg.cache_dir, show_download_progress, cfg.model_variant)?;
243    let tokenizer = load_tokenizer(&files.tokenizer_path, cfg.max_seq_length)?;
244    let eps = execution_providers(
245        cfg.cache_dir,
246        cfg.ep,
247        cfg.device_id,
248        cfg.trt_max_workspace_bytes,
249        cfg.gpu_mem_limit_bytes,
250    );
251    let session = load_session(&files.onnx_path, eps, cfg.intra_threads)?;
252    Ok((session, tokenizer))
253}