Skip to main content

rlx_runtime/
validators.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Composable request validators (plan #84).
6//!
7//! Borrowed from MAX's `pipelines/core/context_validators.py`. Each
8//! validation rule is a small composable function returning
9//! `Result<(), ValidationError>`. Pipelines pick which to apply for
10//! their request type.
11//!
12//! Why composable instead of a single `validate_request()`?
13//!   - Different request types (text embed, image embed, generation)
14//!     share some rules (max length) but not others (image bounds).
15//!   - Adding a rule = one function, not editing a monolith.
16//!   - Easy to test rules in isolation.
17//!
18//! Used by future serving paths. Today the
19//! benchmark runners can use it for input sanity checks.
20
21use std::fmt;
22
23#[derive(Debug, Clone)]
24pub struct ValidationError {
25    pub rule: &'static str,
26    pub message: String,
27}
28
29impl fmt::Display for ValidationError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "[{}] {}", self.rule, self.message)
32    }
33}
34
35impl std::error::Error for ValidationError {}
36
37pub type ValidationResult = Result<(), ValidationError>;
38
39/// A single check on a context value.
40pub trait Validator<C>: Send + Sync {
41    fn check(&self, ctx: &C) -> ValidationResult;
42    fn name(&self) -> &'static str;
43}
44
45/// Run a chain of validators; return the first error or `Ok(())`.
46pub fn run_chain<C>(ctx: &C, chain: &[&dyn Validator<C>]) -> ValidationResult {
47    for v in chain {
48        v.check(ctx)?;
49    }
50    Ok(())
51}
52
53// ── Text-context validators ─────────────────────────────────────
54
55/// Context fields a text request typically carries.
56#[derive(Debug, Clone)]
57pub struct TextContext {
58    pub seq_len: usize,
59    pub batch_size: usize,
60    pub vocab_id_max: usize,
61    pub max_token_id_seen: usize,
62}
63
64pub struct MaxSeqLen(pub usize);
65impl Validator<TextContext> for MaxSeqLen {
66    fn check(&self, ctx: &TextContext) -> ValidationResult {
67        if ctx.seq_len > self.0 {
68            Err(ValidationError {
69                rule: self.name(),
70                message: format!("seq_len {} exceeds max {}", ctx.seq_len, self.0),
71            })
72        } else {
73            Ok(())
74        }
75    }
76    fn name(&self) -> &'static str {
77        "max_seq_len"
78    }
79}
80
81pub struct MaxBatchSize(pub usize);
82impl Validator<TextContext> for MaxBatchSize {
83    fn check(&self, ctx: &TextContext) -> ValidationResult {
84        if ctx.batch_size > self.0 {
85            Err(ValidationError {
86                rule: self.name(),
87                message: format!("batch_size {} exceeds max {}", ctx.batch_size, self.0),
88            })
89        } else {
90            Ok(())
91        }
92    }
93    fn name(&self) -> &'static str {
94        "max_batch_size"
95    }
96}
97
98pub struct TokenIdsInVocab;
99impl Validator<TextContext> for TokenIdsInVocab {
100    fn check(&self, ctx: &TextContext) -> ValidationResult {
101        if ctx.max_token_id_seen >= ctx.vocab_id_max {
102            Err(ValidationError {
103                rule: self.name(),
104                message: format!(
105                    "saw token_id {} but vocab is {}",
106                    ctx.max_token_id_seen, ctx.vocab_id_max
107                ),
108            })
109        } else {
110            Ok(())
111        }
112    }
113    fn name(&self) -> &'static str {
114        "token_ids_in_vocab"
115    }
116}
117
118// ── Image-context validators ────────────────────────────────────
119
120#[derive(Debug, Clone)]
121pub struct ImageContext {
122    pub width: u32,
123    pub height: u32,
124    pub channels: u32,
125}
126
127pub struct ImageMaxBounds {
128    pub max_w: u32,
129    pub max_h: u32,
130}
131impl Validator<ImageContext> for ImageMaxBounds {
132    fn check(&self, ctx: &ImageContext) -> ValidationResult {
133        if ctx.width > self.max_w || ctx.height > self.max_h {
134            Err(ValidationError {
135                rule: self.name(),
136                message: format!(
137                    "{}×{} exceeds max {}×{}",
138                    ctx.width, ctx.height, self.max_w, self.max_h
139                ),
140            })
141        } else {
142            Ok(())
143        }
144    }
145    fn name(&self) -> &'static str {
146        "image_max_bounds"
147    }
148}
149
150pub struct ChannelsAllowed(pub &'static [u32]);
151impl Validator<ImageContext> for ChannelsAllowed {
152    fn check(&self, ctx: &ImageContext) -> ValidationResult {
153        if !self.0.contains(&ctx.channels) {
154            Err(ValidationError {
155                rule: self.name(),
156                message: format!("channels={} not in allowed set {:?}", ctx.channels, self.0),
157            })
158        } else {
159            Ok(())
160        }
161    }
162    fn name(&self) -> &'static str {
163        "channels_allowed"
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn text_chain_short_circuits() {
173        let ctx = TextContext {
174            seq_len: 600,
175            batch_size: 1,
176            vocab_id_max: 30000,
177            max_token_id_seen: 100,
178        };
179        let max_seq = MaxSeqLen(512);
180        let max_batch = MaxBatchSize(64);
181        let tok = TokenIdsInVocab;
182        let chain: Vec<&dyn Validator<TextContext>> = vec![&max_seq, &max_batch, &tok];
183        let err = run_chain(&ctx, &chain).unwrap_err();
184        assert_eq!(err.rule, "max_seq_len");
185    }
186
187    #[test]
188    fn image_chain_passes() {
189        let ctx = ImageContext {
190            width: 224,
191            height: 224,
192            channels: 3,
193        };
194        let bounds = ImageMaxBounds {
195            max_w: 1024,
196            max_h: 1024,
197        };
198        let chans = ChannelsAllowed(&[1, 3, 4]);
199        let chain: Vec<&dyn Validator<ImageContext>> = vec![&bounds, &chans];
200        assert!(run_chain(&ctx, &chain).is_ok());
201    }
202
203    #[test]
204    fn image_chain_catches_bad_channels() {
205        let ctx = ImageContext {
206            width: 224,
207            height: 224,
208            channels: 2,
209        };
210        let chans = ChannelsAllowed(&[1, 3, 4]);
211        let chain: Vec<&dyn Validator<ImageContext>> = vec![&chans];
212        let err = run_chain(&ctx, &chain).unwrap_err();
213        assert_eq!(err.rule, "channels_allowed");
214    }
215}