1use crate::weight_registry::WeightRegistry;
26use std::collections::VecDeque;
27
28#[derive(Debug, Clone)]
32pub struct LoraRequest {
33 pub id: u64,
34 pub adapter: Option<String>,
35 pub payload: LoraPayload,
38}
39
40#[derive(Debug, Clone)]
44pub struct LoraPayload {
45 pub prompt_tokens: Vec<u32>,
46 pub max_new_tokens: usize,
47}
48
49#[derive(Debug)]
52pub struct LoraBatch {
53 pub adapter: Option<String>,
54 pub requests: Vec<LoraRequest>,
55}
56
57impl LoraBatch {
58 pub fn len(&self) -> usize {
59 self.requests.len()
60 }
61 pub fn is_empty(&self) -> bool {
62 self.requests.is_empty()
63 }
64}
65
66pub struct LoraScheduler {
68 pending: VecDeque<LoraRequest>,
70 pub max_batch: usize,
72 registry: Option<*const WeightRegistry>,
77}
78
79unsafe impl Send for LoraScheduler {}
84
85impl LoraScheduler {
86 pub fn new(max_batch: usize) -> Self {
87 Self {
88 pending: VecDeque::new(),
89 max_batch,
90 registry: None,
91 }
92 }
93
94 pub fn bind_registry(&mut self, registry: &WeightRegistry) {
97 self.registry = Some(registry as *const _);
98 }
99
100 pub fn push(&mut self, req: LoraRequest) -> Result<(), UnknownAdapter> {
103 if let (Some(reg_ptr), Some(adapter)) = (self.registry, &req.adapter) {
104 let reg = unsafe { &*reg_ptr };
106 if reg.lora_adapter_handles(adapter).is_empty() {
107 return Err(UnknownAdapter {
108 name: adapter.clone(),
109 });
110 }
111 }
112 self.pending.push_back(req);
113 Ok(())
114 }
115
116 pub fn peek_adapter(&self) -> Option<Option<String>> {
118 self.pending.front().map(|r| r.adapter.clone())
119 }
120
121 pub fn drain_batch(&mut self) -> Option<LoraBatch> {
124 let head = self.pending.pop_front()?;
125 let target = head.adapter.clone();
126 let mut requests = vec![head];
127 while requests.len() < self.max_batch {
128 match self.pending.front() {
129 Some(next) if next.adapter == target => {
130 requests.push(self.pending.pop_front().unwrap());
131 }
132 _ => break,
133 }
134 }
135 Some(LoraBatch {
136 adapter: target,
137 requests,
138 })
139 }
140
141 pub fn pending(&self) -> usize {
142 self.pending.len()
143 }
144 pub fn is_empty(&self) -> bool {
145 self.pending.is_empty()
146 }
147}
148
149#[derive(Debug, Clone)]
150pub struct UnknownAdapter {
151 pub name: String,
152}
153
154impl std::fmt::Display for UnknownAdapter {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "adapter `{}` is not registered", self.name)
157 }
158}
159impl std::error::Error for UnknownAdapter {}
160
161pub fn naive_swap_count(reqs: &[LoraRequest]) -> usize {
165 let mut swaps: usize = 0;
166 let mut last: Option<&Option<String>> = None;
167 for r in reqs {
168 if last.map(|l| l != &r.adapter).unwrap_or(true) {
169 swaps += 1;
170 }
171 last = Some(&r.adapter);
172 }
173 swaps.saturating_sub(1) }
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::weight_registry::{WeightKind, WeightRegistry};
180 use rlx_ir::{DType, Shape};
181 use std::sync::Arc;
182
183 fn req(id: u64, adapter: Option<&str>) -> LoraRequest {
184 LoraRequest {
185 id,
186 adapter: adapter.map(|s| s.to_string()),
187 payload: LoraPayload {
188 prompt_tokens: vec![],
189 max_new_tokens: 4,
190 },
191 }
192 }
193
194 #[test]
195 fn coalesces_same_adapter_runs() {
196 let mut s = LoraScheduler::new(8);
197 for r in [
200 req(1, Some("code")),
201 req(2, Some("code")),
202 req(3, Some("math")),
203 req(4, Some("math")),
204 req(5, Some("code")),
205 req(6, None),
206 req(7, None),
207 ] {
208 s.push(r).unwrap();
209 }
210
211 let b1 = s.drain_batch().unwrap();
212 assert_eq!(b1.adapter.as_deref(), Some("code"));
213 assert_eq!(b1.len(), 2); let b2 = s.drain_batch().unwrap();
216 assert_eq!(b2.adapter.as_deref(), Some("math"));
217 assert_eq!(b2.len(), 2);
218
219 let b3 = s.drain_batch().unwrap();
220 assert_eq!(b3.adapter.as_deref(), Some("code"));
221 assert_eq!(b3.len(), 1);
222
223 let b4 = s.drain_batch().unwrap();
224 assert!(b4.adapter.is_none());
225 assert_eq!(b4.len(), 2);
226
227 assert!(s.drain_batch().is_none());
228 }
229
230 #[test]
231 fn respects_max_batch_cap() {
232 let mut s = LoraScheduler::new(3);
233 for i in 0..10 {
234 s.push(req(i, Some("code"))).unwrap();
235 }
236 let b = s.drain_batch().unwrap();
237 assert_eq!(b.len(), 3, "max_batch=3 should split a long run");
238 assert_eq!(s.pending(), 7);
239 }
240
241 #[test]
242 fn registry_validation_rejects_unknown_adapter() {
243 let mut reg = WeightRegistry::new();
244 reg.register(
245 "ffn",
246 Shape::new(&[8, 8], DType::F32),
247 Arc::from(vec![0u8; 256]),
248 WeightKind::Base,
249 );
250 reg.register(
251 "ffn.lora.a",
252 Shape::new(&[8, 4], DType::F32),
253 Arc::from(vec![0u8; 128]),
254 WeightKind::LoraAdapter {
255 adapter: "code".into(),
256 },
257 );
258
259 let mut s = LoraScheduler::new(4);
260 s.bind_registry(®);
261
262 assert!(s.push(req(1, Some("code"))).is_ok());
264 assert!(s.push(req(2, None)).is_ok());
266 let err = s.push(req(3, Some("nonexistent"))).unwrap_err();
268 assert_eq!(err.name, "nonexistent");
269 }
270
271 #[test]
272 fn swap_count_metric() {
273 let reqs = [
274 req(1, Some("a")),
275 req(2, Some("a")),
276 req(3, Some("b")),
277 req(4, Some("a")),
278 ];
279 assert_eq!(naive_swap_count(&reqs), 2);
282 }
283}