1use crate::module::{Module, ModuleVisitor, Param};
2
3use alloc::string::String;
4use alloc::vec;
5use alloc::vec::Vec;
6#[cfg(feature = "std")]
7use regex::Regex;
8
9use burn_std::id::ParamId;
10use burn_std::sync::Arc;
11use burn_tensor::{Bool, Int, Tensor};
12
13#[derive(Debug)]
15pub enum ParamGroupError {
16 InvalidPatternError(String),
18}
19
20#[derive(Default)]
21struct ParamIdCollector {
22 ids: Vec<ParamId>,
23}
24
25impl ParamIdCollector {
26 pub fn ids(&self) -> Vec<ParamId> {
27 self.ids.clone()
28 }
29}
30
31impl ModuleVisitor for ParamIdCollector {
32 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
33 self.ids.push(param.id);
34 }
35
36 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {
37 self.ids.push(param.id);
38 }
39
40 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {
41 self.ids.push(param.id);
42 }
43
44 fn visit_flag(&mut self, flag: &crate::module::Param<crate::module::Flag>) {
47 self.ids.push(flag.id);
48 }
49}
50
51#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
53pub struct ParamGroup {
54 matcher: ParamGroupMatcher,
55 excludes: Option<ParamGroupMatcher>,
56}
57
58impl ParamGroup {
59 pub fn matches(&self, id: &ParamId, path: Option<&str>) -> bool {
61 let matched = self.matcher.matches(id, path);
62
63 let excluded = if let Some(exclude_matcher) = &self.excludes {
64 exclude_matcher.matches(id, path)
65 } else {
66 false
67 };
68
69 matched && !excluded
70 }
71
72 pub fn ids_from_module<M: Module>(module: M) -> Self {
74 let mut collector = ParamIdCollector::default();
75 module.visit(&mut collector);
76 Self {
77 matcher: ParamGroupMatcher::from_ids(collector.ids()),
78 excludes: None,
79 }
80 }
81
82 pub fn from_path(path: impl Into<String>) -> Self {
84 ParamGroup::from_paths(vec![path])
85 }
86
87 pub fn from_paths(paths: Vec<impl Into<String>>) -> Self {
89 Self {
90 matcher: ParamGroupMatcher::Path(Arc::new(PathMatcher::Exact(
91 paths.into_iter().map(|p| p.into()).collect(),
92 ))),
93 excludes: None,
94 }
95 }
96
97 pub fn from_predicate(path: impl Into<String>) -> Self {
99 ParamGroup::from_predicates(vec![path])
100 }
101
102 pub fn from_predicates(paths: Vec<impl Into<String>>) -> Self {
105 Self {
106 matcher: ParamGroupMatcher::Path(Arc::new(PathMatcher::Include(
107 paths.into_iter().map(|p| p.into()).collect(),
108 ))),
109 excludes: None,
110 }
111 }
112
113 pub fn from_any_predicates(paths: Vec<impl Into<String>>) -> Self {
116 let mut matchers: Vec<ParamGroupMatcher> = paths
117 .into_iter()
118 .map(|p| ParamGroupMatcher::Path(Arc::new(PathMatcher::Include(vec![p.into()]))))
119 .collect();
120 let mut main_matcher = if let Some(value) = matchers.pop() {
121 value
122 } else {
123 return Self {
124 matcher: ParamGroupMatcher::Path(Arc::new(PathMatcher::Include(vec![]))),
125 excludes: None,
126 };
127 };
128
129 matchers
130 .iter()
131 .for_each(|m| main_matcher = main_matcher.clone().fuse(m));
132
133 Self {
134 matcher: main_matcher,
135 excludes: None,
136 }
137 }
138
139 #[cfg(feature = "std")]
140 pub fn from_regex<S: AsRef<str>>(pattern: S) -> Result<Self, ParamGroupError> {
145 ParamGroup::from_regexes(vec![pattern])
146 }
147
148 #[cfg(feature = "std")]
149 pub fn from_regexes<S: AsRef<str>>(patterns: Vec<S>) -> Result<Self, ParamGroupError> {
155 let mut new_patterns = vec![];
156 for pattern in patterns {
157 match Regex::new(pattern.as_ref()) {
158 Ok(re) => new_patterns.push(re),
159 Err(e) => {
160 return Err(ParamGroupError::InvalidPatternError(format!(
161 "Invalid regex pattern: {e}"
162 )));
163 }
164 }
165 }
166 Ok(Self {
167 matcher: ParamGroupMatcher::Path(Arc::new(PathMatcher::Regex(new_patterns))),
168 excludes: None,
169 })
170 }
171
172 #[cfg(feature = "std")]
173 pub fn from_any_regexes<S: AsRef<str>>(patterns: Vec<S>) -> Result<Self, ParamGroupError> {
179 let mut matchers = vec![];
180 for pattern in patterns {
181 match Regex::new(pattern.as_ref()) {
182 Ok(re) => {
183 matchers.push(ParamGroupMatcher::Path(Arc::new(PathMatcher::Regex(vec![
184 re,
185 ]))))
186 }
187 Err(e) => {
188 return Err(ParamGroupError::InvalidPatternError(format!(
189 "Invalid regex pattern: {e}"
190 )));
191 }
192 }
193 }
194
195 let mut main_matcher = if let Some(value) = matchers.pop() {
196 value
197 } else {
198 return Ok(Self {
199 matcher: ParamGroupMatcher::Path(Arc::new(PathMatcher::Include(vec![]))),
200 excludes: None,
201 });
202 };
203
204 matchers
205 .iter()
206 .for_each(|m| main_matcher = main_matcher.clone().fuse(m));
207
208 Ok(Self {
209 matcher: main_matcher,
210 excludes: None,
211 })
212 }
213
214 pub fn all() -> Self {
216 Self {
217 matcher: ParamGroupMatcher::All,
218 excludes: None,
219 }
220 }
221
222 pub fn from_ids(ids: Vec<ParamId>) -> Self {
224 Self {
225 matcher: ParamGroupMatcher::Explicit(Arc::new(ids)),
226 excludes: None,
227 }
228 }
229
230 pub fn fuse(self, other: &Self) -> Self {
232 Self {
233 matcher: self.matcher.fuse(&other.matcher),
234 excludes: None,
235 }
236 }
237
238 pub fn exclude(mut self, group: Self) -> Self {
240 self.excludes = match &self.excludes {
241 Some(excluded) => Some(excluded.clone().fuse(&group.matcher)),
242 None => Some(group.matcher.clone()),
243 };
244 self
245 }
246}
247
248mod arc_serde {
249 use serde::{Deserialize, Deserializer, Serialize, Serializer};
250
251 use super::*;
252
253 pub fn serialize<S, T>(val: &Arc<T>, serializer: S) -> Result<S::Ok, S::Error>
254 where
255 S: Serializer,
256 T: Serialize,
257 {
258 val.as_ref().serialize(serializer)
259 }
260
261 pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Arc<T>, D::Error>
262 where
263 D: Deserializer<'de>,
264 T: Deserialize<'de>,
265 {
266 let v = T::deserialize(deserializer)?;
267 Ok(Arc::new(v))
268 }
269}
270
271#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
272enum ParamGroupMatcher {
273 All,
274 #[serde(with = "arc_serde")]
275 Explicit(Arc<Vec<ParamId>>),
276 #[serde(with = "arc_serde")]
277 Path(Arc<PathMatcher>),
278 #[serde(with = "arc_serde")]
279 Combined(Arc<Vec<Self>>),
280}
281
282impl ParamGroupMatcher {
283 pub fn from_ids(ids: Vec<ParamId>) -> Self {
284 Self::Explicit(Arc::new(ids))
285 }
286
287 pub(crate) fn matches(&self, id: &ParamId, path: Option<&str>) -> bool {
288 match self {
289 Self::All => true,
290 Self::Explicit(ids) => ids.contains(id),
291 Self::Path(matcher) => path.is_some_and(|p| matcher.matches(p)),
292 Self::Combined(matchers) => matchers.iter().any(|m| m.matches(id, path)),
293 }
294 }
295
296 fn push_combined(self, other: Self) -> Self {
297 match self {
298 ParamGroupMatcher::Combined(param_group_matchers) => match other.clone() {
299 ParamGroupMatcher::All => Self::All,
300 ParamGroupMatcher::Explicit(_) => {
301 let mut matchers = (*param_group_matchers).clone();
302 matchers.push(other);
303 Self::Combined(Arc::new(matchers))
304 }
305 ParamGroupMatcher::Path(_) => {
306 let mut matchers = (*param_group_matchers).clone();
307 matchers.push(other);
308 Self::Combined(Arc::new(matchers))
309 }
310 ParamGroupMatcher::Combined(other_matchers) => {
311 let mut matchers = (*param_group_matchers).clone();
312 matchers.append(&mut (*other_matchers).clone());
313 Self::Combined(Arc::new(matchers))
314 }
315 },
316 _ => panic!(
317 "`push_combined` should only be called on a ParamGroupMatcher::Combined variant."
318 ),
319 }
320 }
321
322 pub(crate) fn fuse(self, other: &Self) -> Self {
323 match (self.clone(), other.clone()) {
324 (ParamGroupMatcher::All, _) => Self::All,
325 (_, ParamGroupMatcher::All) => Self::All,
326 (ParamGroupMatcher::Explicit(_), ParamGroupMatcher::Combined(_)) => {
327 other.clone().push_combined(self)
328 }
329 (ParamGroupMatcher::Path(_), ParamGroupMatcher::Combined(_)) => {
330 other.clone().push_combined(self)
331 }
332 (ParamGroupMatcher::Combined(_), ParamGroupMatcher::Explicit(_)) => {
333 self.push_combined(other.clone())
334 }
335 (ParamGroupMatcher::Combined(_), ParamGroupMatcher::Path(_)) => {
336 self.push_combined(other.clone())
337 }
338 (ParamGroupMatcher::Combined(_), ParamGroupMatcher::Combined(_)) => {
339 self.push_combined(other.clone())
340 }
341 _ => ParamGroupMatcher::Combined(Arc::new(vec![self, other.clone()])),
342 }
343 }
344}
345
346#[cfg(feature = "std")]
347mod regex_serde {
348 use serde::{Deserialize, Deserializer, Serialize, Serializer};
349
350 use super::*;
351
352 pub fn serialize<S>(regexes: &[Regex], serializer: S) -> Result<S::Ok, S::Error>
353 where
354 S: Serializer,
355 {
356 let v: Vec<&str> = regexes.iter().map(|r| r.as_str()).collect();
357 v.serialize(serializer)
358 }
359
360 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Regex>, D::Error>
361 where
362 D: Deserializer<'de>,
363 {
364 let strings: Vec<String> = Vec::deserialize(deserializer)?;
365 strings
366 .into_iter()
367 .map(|s| Regex::new(&s).map_err(serde::de::Error::custom))
368 .collect()
369 }
370}
371
372#[derive(Clone, serde::Serialize, serde::Deserialize)]
373enum PathMatcher {
374 Exact(Vec<String>),
375 #[cfg(feature = "std")]
376 #[serde(with = "regex_serde")]
377 Regex(Vec<Regex>),
378 Include(Vec<String>),
379}
380
381impl PathMatcher {
382 pub(crate) fn matches(&self, path: &str) -> bool {
383 match self {
384 PathMatcher::Exact(paths) => paths.iter().any(|p| p == path),
385 #[cfg(feature = "std")]
386 PathMatcher::Regex(patterns) => patterns.iter().all(|r| r.is_match(path)),
387 PathMatcher::Include(includes) => includes.iter().all(|inc| path.contains(inc)),
388 }
389 }
390}
391
392impl alloc::fmt::Debug for PathMatcher {
393 fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
394 match self {
395 Self::Exact(arg0) => f.debug_tuple("Exact").field(arg0).finish(),
396 #[cfg(feature = "std")]
397 Self::Regex(arg0) => f.debug_tuple("Regex").field(arg0).finish(),
398 Self::Include(arg0) => f.debug_tuple("Include").field(arg0).finish(),
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::test_utils::SimpleLinear;
407
408 #[test]
409 fn all_matches_any_parameter() {
410 let group = ParamGroup::all();
411 let id = ParamId::new();
412
413 assert!(group.matches(&id, None));
414 }
415
416 #[test]
417 fn explicit_matches_only_selected_ids() {
418 let id = ParamId::new();
419 let other_id = ParamId::new();
420 let group = ParamGroup::from_ids(vec![id]);
421
422 assert!(group.matches(&id, None));
423 assert!(!group.matches(&other_id, None));
424 }
425
426 #[test]
427 fn path_matcher_requires_path_and_matches_exactly() {
428 let group = ParamGroup::from_path("model.backbone.weight");
429 let id = ParamId::new();
430
431 assert!(group.matches(&id, Some("model.backbone.weight")));
432 assert!(!group.matches(&id, Some("model.backbone.bias")));
433 assert!(!group.matches(&id, None));
434 }
435
436 #[test]
437 fn predicate_matcher_matches_substrings() {
438 let group = ParamGroup::from_predicate("backbone");
439 let id = ParamId::new();
440
441 assert!(group.matches(&id, Some("model.backbone.weight")));
442 assert!(!group.matches(&id, Some("model.other.weight")));
443 }
444
445 #[cfg(feature = "std")]
446 #[test]
447 fn regex_matcher_matches_pattern() {
448 let group = ParamGroup::from_regex(r"^model\.layer\.[0-9]+\.weight$").unwrap();
449 let id = ParamId::new();
450
451 assert!(group.matches(&id, Some("model.layer.3.weight")));
452 assert!(!group.matches(&id, Some("model.layer.weight")));
453 }
454
455 #[test]
456 fn ids_from_module_collects_all_param_ids() {
457 let device = crate::test_device();
458 let module = SimpleLinear::new(4, 8, &device);
459 let weight_id = module.weight.id;
460 let bias_id = module.bias.as_ref().unwrap().id;
461 let group = ParamGroup::ids_from_module(module);
462
463 assert!(group.matches(&weight_id, Some("weight")));
464 assert!(group.matches(&bias_id, Some("bias")));
465 }
466
467 #[test]
468 fn fuse_combines_multiple_groups() {
469 let id = ParamId::new();
470 let group1 = ParamGroup::from_ids(vec![id]);
471 let group2 = ParamGroup::from_path("model.layer.weight");
472 let fused = group1.fuse(&group2);
473
474 assert!(fused.matches(&id, Some("model.other.bias")));
475 assert!(fused.matches(&ParamId::new(), Some("model.layer.weight")));
476 assert!(!fused.matches(&ParamId::new(), Some("model.layer.bias")));
477 }
478
479 #[test]
480 fn exclude_removes_matching_ids_from_a_group() {
481 let id = ParamId::new();
482 let excluded_id = ParamId::new();
483 let exclude_group = ParamGroup::from_ids(vec![excluded_id]);
484 let group = ParamGroup::from_ids(vec![id, excluded_id]).exclude(exclude_group);
485
486 assert!(group.matches(&id, None));
487 assert!(!group.matches(&excluded_id, None));
488 }
489
490 #[test]
491 fn exclude_filters_path_matches() {
492 let id = ParamId::new();
493 let group = ParamGroup::from_predicate("backbone")
494 .exclude(ParamGroup::from_path("model.backbone.bias"));
495
496 assert!(group.matches(&id, Some("model.backbone.weight")));
497 assert!(!group.matches(&id, Some("model.backbone.bias")));
498 }
499
500 #[test]
501 fn exclude_ignores_excludes_on_the_provided_group() {
502 let id = ParamId::new();
503 let excluded = ParamGroup::from_path("model.backbone.bias")
504 .exclude(ParamGroup::from_path("model.backbone.weight"));
505 let group = ParamGroup::from_path("model.backbone.weight").exclude(excluded);
506
507 assert!(group.matches(&id, Some("model.backbone.weight")));
508 assert!(!group.matches(&id, Some("model.backbone.bias")));
509 }
510
511 #[test]
512 fn from_any_predicates_matches_any_predicate() {
513 let group = ParamGroup::from_any_predicates(vec!["backbone", "encoder"]);
514 let id = ParamId::new();
515
516 assert!(group.matches(&id, Some("model.backbone.weight")));
517 assert!(group.matches(&id, Some("model.encoder.weight")));
518 assert!(!group.matches(&id, Some("model.decoder.weight")));
519 }
520
521 #[cfg(feature = "std")]
522 #[test]
523 fn from_any_regexes_matches_any_pattern() {
524 let group =
525 ParamGroup::from_any_regexes(vec![r"^model\.layer\.[0-9]+\.weight$", r"^model\.bias$"])
526 .unwrap();
527 let id = ParamId::new();
528
529 assert!(group.matches(&id, Some("model.layer.3.weight")));
530 assert!(group.matches(&id, Some("model.bias")));
531 assert!(!group.matches(&id, Some("model.layer.weight")));
532 assert!(!group.matches(&id, Some("model.other.weight")));
533 }
534
535 #[cfg(feature = "std")]
536 #[test]
537 fn from_regexes_with_invalid_pattern() {
538 let result = ParamGroup::from_regex(r"[invalid(");
539 assert!(result.is_err());
540
541 let result = ParamGroup::from_regexes(vec![r"[invalid("]);
542 assert!(result.is_err());
543
544 let result = ParamGroup::from_any_regexes(vec![r"[invalid("]);
545 assert!(result.is_err());
546 }
547}