cold.rs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //! Cold-start overrides.
  2. //!
  3. //! A recipe may declare `cold_args` — a flat list of engine flags in exactly
  4. //! the same shape as the main `args` (so it is engine-agnostic: a vLLM recipe
  5. //! writes `--gpu-memory-utilization 0.75 --max-model-len 32768`, an SGLang
  6. //! recipe writes `--mem-fraction-static 0.6 …`). `serve --cold` overlays those
  7. //! onto the main args: any flag that appears in `cold_args` **replaces** its
  8. //! value in the main args (or is appended if the recipe omitted it). Flags not
  9. //! mentioned in `cold_args` are left untouched.
  10. //!
  11. //! This is the "boot it safely first, then serve full" mode: the first launch
  12. //! (or a launch onto a tight node) uses the conservative `cold_args` to avoid a
  13. //! KV-cache OOM or a GB10 unified-memory *stall*, without editing the recipe
  14. //! per launch. The values are taken as written — the recipe author is expected
  15. //! to write cold values lower than the serving values.
  16. /// Overlay `cold` onto `main`: for each flag in `cold`, replace that flag's
  17. /// value in `main` (or append `flag value` when `main` lacks it). Handles the
  18. /// space form (`--max-len 32768`), the `=` form (`--max-len=32768`), and
  19. /// boolean flags (a flag with no following value is just ensured present).
  20. /// Returns a new vec; `main` is unchanged.
  21. pub fn apply_cold(main: &[String], cold: &[String]) -> Vec<String> {
  22. let mut out = main.to_vec();
  23. let mut i = 0;
  24. while i < cold.len() {
  25. let tok = cold[i].as_str();
  26. if !tok.starts_with('-') {
  27. // A stray value token with no preceding flag — skip defensively.
  28. i += 1;
  29. continue;
  30. }
  31. if let Some(eq) = tok.find('=') {
  32. // `--flag=value` form.
  33. set_flag(&mut out, &tok[..eq], tok[eq + 1..].to_string());
  34. i += 1;
  35. continue;
  36. }
  37. // Space form: the value is the next token iff it exists and is not
  38. // itself a flag.
  39. let has_value = i + 1 < cold.len() && !cold[i + 1].starts_with('-');
  40. if has_value {
  41. set_flag(&mut out, tok, cold[i + 1].clone());
  42. i += 2;
  43. } else {
  44. // Boolean flag: ensure it is present in `main`.
  45. if !out.iter().any(|a| a == tok) {
  46. out.push(tok.to_string());
  47. }
  48. i += 1;
  49. }
  50. }
  51. out
  52. }
  53. /// Replace the value of `flag` in `args`, or append `flag value` when absent.
  54. /// Handles both the space form (`--max-len 32768`) and the `=` form
  55. /// (`--max-len=32768`).
  56. fn set_flag(args: &mut Vec<String>, flag: &str, value: String) {
  57. let eq = format!("{flag}=");
  58. if let Some(i) = args.iter().position(|a| a == flag || a.starts_with(&eq)) {
  59. if args[i] == flag {
  60. // Space form: the value is the next token.
  61. if i + 1 < args.len() {
  62. args[i + 1] = value;
  63. } else {
  64. args.push(value);
  65. }
  66. } else {
  67. // `--flag=value` form: rewrite the whole token.
  68. args[i] = format!("{flag}={value}");
  69. }
  70. } else {
  71. args.push(flag.to_string());
  72. args.push(value);
  73. }
  74. }
  75. #[cfg(test)]
  76. mod tests {
  77. use super::*;
  78. fn main_args() -> Vec<String> {
  79. [
  80. "--served-model-name",
  81. "m",
  82. "--max-model-len",
  83. "500000",
  84. "--max-num-seqs",
  85. "4",
  86. "--gpu-memory-utilization",
  87. "0.87",
  88. ]
  89. .iter()
  90. .map(|s| s.to_string())
  91. .collect()
  92. }
  93. fn get(args: &[String], flag: &str) -> String {
  94. args.iter()
  95. .position(|a| a == flag)
  96. .map(|i| args[i + 1].clone())
  97. .unwrap_or_default()
  98. }
  99. #[test]
  100. fn cold_replaces_present_flags_and_appends_absent() {
  101. let cold = [
  102. "--gpu-memory-utilization",
  103. "0.75",
  104. "--max-model-len",
  105. "32768",
  106. "--max-num-seqs",
  107. "2",
  108. "--max-num-batched-tokens",
  109. "2048",
  110. ]
  111. .iter()
  112. .map(|s| s.to_string())
  113. .collect::<Vec<_>>();
  114. let out = apply_cold(&main_args(), &cold);
  115. // Each flag appears exactly once (replaced, not duplicated).
  116. for f in [
  117. "--max-model-len",
  118. "--max-num-seqs",
  119. "--gpu-memory-utilization",
  120. ] {
  121. assert_eq!(
  122. out.iter().filter(|a| a.as_str() == f).count(),
  123. 1,
  124. "{f} duplicated"
  125. );
  126. }
  127. assert_eq!(get(&out, "--max-model-len"), "32768");
  128. assert_eq!(get(&out, "--max-num-seqs"), "2");
  129. assert_eq!(get(&out, "--gpu-memory-utilization"), "0.75");
  130. // Absent flag appended.
  131. assert_eq!(get(&out, "--max-num-batched-tokens"), "2048");
  132. // Untouched flags stay.
  133. assert_eq!(get(&out, "--served-model-name"), "m");
  134. }
  135. #[test]
  136. fn cold_handles_eq_form_and_appends() {
  137. let base = vec!["--max-model-len=500000".to_string()];
  138. let cold = ["--max-model-len=16384", "--max-num-seqs", "1"]
  139. .iter()
  140. .map(|s| s.to_string())
  141. .collect::<Vec<_>>();
  142. let out = apply_cold(&base, &cold);
  143. assert_eq!(out, vec!["--max-model-len=16384", "--max-num-seqs", "1"]);
  144. }
  145. #[test]
  146. fn cold_boolean_flag_ensured_present_once() {
  147. let base = vec!["--enable-prefix-caching".to_string(), "--foo".to_string()];
  148. let cold = ["--enable-prefix-caching", "--bar"]
  149. .iter()
  150. .map(|s| s.to_string())
  151. .collect::<Vec<_>>();
  152. let out = apply_cold(&base, &cold);
  153. assert_eq!(
  154. out.iter()
  155. .filter(|a| a.as_str() == "--enable-prefix-caching")
  156. .count(),
  157. 1
  158. );
  159. assert!(out.iter().any(|a| a.as_str() == "--bar"));
  160. }
  161. #[test]
  162. fn empty_cold_is_noop() {
  163. assert_eq!(apply_cold(&main_args(), &[]), main_args());
  164. }
  165. #[test]
  166. fn engine_agnostic_sglang_flags() {
  167. // A cold block is not tied to vLLM flag names — any flag overlays.
  168. let base = vec!["--mem-fraction-static".to_string(), "0.9".to_string()];
  169. let cold = ["--mem-fraction-static", "0.6"]
  170. .iter()
  171. .map(|s| s.to_string())
  172. .collect::<Vec<_>>();
  173. let out = apply_cold(&base, &cold);
  174. assert_eq!(get(&out, "--mem-fraction-static"), "0.6");
  175. }
  176. }