slurm_helpers.rs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. use std::collections::HashMap;
  2. use std::process::Command;
  3. use anyhow::Context;
  4. #[derive(Debug)]
  5. pub struct NodeInfo {
  6. pub partition: String,
  7. pub node: String,
  8. pub cpu_total: i32,
  9. pub cpu_free: i32,
  10. pub gpu_total: i32,
  11. pub gpu_free: i32,
  12. pub gpu_names: Vec<String>, // <- models/types for GRES
  13. }
  14. fn parse_kv_line(line: &str) -> HashMap<String, String> {
  15. let mut map = HashMap::new();
  16. for token in line.split_whitespace() {
  17. if let Some(eq) = token.find('=') {
  18. let (k, v) = token.split_at(eq);
  19. map.insert(k.to_string(), v[1..].to_string());
  20. }
  21. }
  22. map
  23. }
  24. fn parse_gpu_from_tres(s: &str) -> u32 {
  25. // handles "gres/gpu=4" and "gres/gpu:V100=4"
  26. if let Some(idx) = s.find("gres/gpu") {
  27. let rest = &s[idx + "gres/gpu".len()..];
  28. let rest = rest.strip_prefix(':').unwrap_or(rest); // optional ":TYPE"
  29. let rest = rest.split('=').nth(1).unwrap_or("");
  30. let num: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
  31. return num.parse().unwrap_or(0);
  32. }
  33. 0
  34. }
  35. fn parse_gpu_from_gres(s: &str) -> u32 {
  36. // handles "gpu:4" or "gpu:V100:4"
  37. if let Some(idx) = s.find("gpu:") {
  38. let rest = &s[idx + "gpu:".len()..];
  39. // could be "V100:4" or "4"
  40. let parts: Vec<&str> = rest.split([':', '(']).collect();
  41. // last numeric field is count
  42. for p in parts.iter().rev() {
  43. if let Ok(v) = p.parse() {
  44. return v;
  45. }
  46. }
  47. }
  48. 0
  49. }
  50. fn extract_total_gpus(fields: &HashMap<String, String>) -> u32 {
  51. if let Some(cfg) = fields.get("CfgTRES") {
  52. let n = parse_gpu_from_tres(cfg);
  53. if n > 0 {
  54. return n;
  55. }
  56. }
  57. if let Some(gres) = fields.get("Gres") {
  58. return parse_gpu_from_gres(gres);
  59. }
  60. 0
  61. }
  62. fn extract_alloc_gpus(fields: &HashMap<String, String>) -> u32 {
  63. if let Some(alloc) = fields.get("AllocTRES") {
  64. let n = parse_gpu_from_tres(alloc);
  65. if n > 0 {
  66. return n;
  67. }
  68. }
  69. if let Some(gres_used) = fields.get("GresUsed") {
  70. return parse_gpu_from_gres(gres_used);
  71. }
  72. 0
  73. }
  74. fn extract_gpu_names_from_gres(s: &str) -> Vec<String> {
  75. // Examples:
  76. // "gpu:V100:4(S:0-3)"
  77. // "gpu:A100:8(S:0-7)"
  78. // "gpu:4(S:0-3)" -> no type
  79. let mut names = Vec::new();
  80. for item in s.split(',') {
  81. if let Some(idx) = item.find("gpu:") {
  82. let rest = &item[idx + "gpu:".len()..];
  83. // strip trailing "(S:...)" etc.
  84. let rest = rest.split('(').next().unwrap_or(rest);
  85. let parts: Vec<&str> = rest.split(':').collect();
  86. if parts.len() == 1 {
  87. // "4" -> no explicit type
  88. continue;
  89. } else {
  90. // "V100:4" -> type = parts[0]
  91. let name = parts[0].trim();
  92. if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
  93. names.push(name.to_string());
  94. }
  95. }
  96. }
  97. }
  98. names.sort();
  99. names.dedup();
  100. names
  101. }
  102. fn extract_gpu_names_from_cfg_tres(s: &str) -> Vec<String> {
  103. // Example:
  104. // "cpu=64,gres/gpu:V100=4,gres/gpu:A100=2"
  105. let mut names = Vec::new();
  106. for item in s.split(',') {
  107. if let Some(idx) = item.find("gres/gpu") {
  108. let rest = &item[idx + "gres/gpu".len()..];
  109. if let Some(rest) = rest.strip_prefix(':') {
  110. // rest = "V100=4"
  111. let name = rest.split('=').next().unwrap_or("").trim();
  112. if !name.is_empty() {
  113. names.push(name.to_string());
  114. }
  115. }
  116. }
  117. }
  118. names.sort();
  119. names.dedup();
  120. names
  121. }
  122. fn extract_gpu_names(fields: &HashMap<String, String>) -> Vec<String> {
  123. // Prefer CfgTRES if it has types, else Gres
  124. if let Some(cfg) = fields.get("CfgTRES") {
  125. let names = extract_gpu_names_from_cfg_tres(cfg);
  126. if !names.is_empty() {
  127. return names;
  128. }
  129. }
  130. if let Some(gres) = fields.get("Gres") {
  131. return extract_gpu_names_from_gres(gres);
  132. }
  133. Vec::new()
  134. }
  135. fn parse_node(fields: HashMap<String, String>) -> NodeInfo {
  136. let partition = fields.get("Partitions").cloned().unwrap_or_default();
  137. let node = fields.get("NodeName").cloned().unwrap_or_default();
  138. let cpu_total: i32 = fields
  139. .get("CPUTot")
  140. .and_then(|s| s.parse().ok())
  141. .unwrap_or(0);
  142. let cpu_alloc: i32 = fields
  143. .get("CPUAlloc")
  144. .and_then(|s| s.parse().ok())
  145. .unwrap_or(0);
  146. let cpu_free = cpu_total - cpu_alloc;
  147. let gpu_total = extract_total_gpus(&fields) as i32;
  148. let gpu_alloc = extract_alloc_gpus(&fields) as i32;
  149. let gpu_free = gpu_total - gpu_alloc;
  150. let gpu_names = extract_gpu_names(&fields);
  151. NodeInfo {
  152. partition,
  153. node,
  154. cpu_total,
  155. cpu_free,
  156. gpu_total,
  157. gpu_free,
  158. gpu_names,
  159. }
  160. }
  161. pub fn slurm_availables() -> anyhow::Result<Vec<NodeInfo>> {
  162. let output = Command::new("scontrol")
  163. .args(["show", "nodes", "-o"])
  164. .output()
  165. .context("failed to run scontrol")?;
  166. let stdout = String::from_utf8_lossy(&output.stdout);
  167. let mut results = Vec::new();
  168. for line in stdout.lines() {
  169. let kv = parse_kv_line(line);
  170. let info = parse_node(kv);
  171. results.push(info);
  172. }
  173. Ok(results)
  174. }
  175. pub fn max_gpu_per_node(gpu_name: &str) -> Option<i32> {
  176. let nodes = slurm_availables().ok()?;
  177. nodes
  178. .iter()
  179. .filter(|n| n.gpu_names.iter().any(|g| g.eq_ignore_ascii_case(gpu_name)))
  180. .map(|n| n.gpu_free)
  181. .max()
  182. }
  183. /// Return the **maximum idle CPUs on a single non-drained node** in `shortq`.
  184. pub fn max_cpus_per_task_shortq() -> anyhow::Result<u32> {
  185. let out = Command::new("sinfo")
  186. .args(["-p", "shortq", "-N", "-h", "-o", "%C %t"])
  187. .output()?;
  188. let stdout = String::from_utf8_lossy(&out.stdout);
  189. let mut max_idle = 0;
  190. for line in stdout.lines() {
  191. let mut parts = line.split_whitespace();
  192. let cpus = parts.next().unwrap_or("");
  193. let state = parts.next().unwrap_or("").to_lowercase();
  194. if state.contains("drain") {
  195. continue;
  196. }
  197. // CPUS(A/I/O/T) → take idle `I` at index 1
  198. if let Some(i) = cpus.split('/').nth(1).and_then(|v| v.parse::<u32>().ok()) {
  199. max_idle = max_idle.max(i);
  200. }
  201. }
  202. Ok(max_idle)
  203. }
  204. // pub fn available_cpus_shortq() -> Result<u32> {
  205. // use std::process::Command;
  206. //
  207. // let out = Command::new("sinfo")
  208. // .args(["-p", "shortq", "-N", "-h", "-o", "%C %t"])
  209. // .output()?;
  210. //
  211. // let stdout = String::from_utf8_lossy(&out.stdout);
  212. // let mut idle = 0;
  213. //
  214. // for line in stdout.lines() {
  215. // let mut parts = line.split_whitespace();
  216. // let cpus = parts.next().unwrap_or("");
  217. // let state = parts.next().unwrap_or("").to_lowercase();
  218. // if state.contains("drain") {
  219. // continue;
  220. // }
  221. //
  222. // // CPUS(A/I/O/T) → idle is index 1
  223. // if let Some(i) = cpus.split('/').nth(1).and_then(|v| v.parse::<u32>().ok()) {
  224. // idle = idle.saturating_add(i);
  225. // }
  226. // }
  227. //
  228. // Ok(idle)
  229. // }
  230. #[cfg(test)]
  231. mod tests {
  232. use crate::slurm_helpers::{max_cpus_per_task_shortq, max_gpu_per_node, slurm_availables};
  233. #[test]
  234. fn slurm_info() -> anyhow::Result<()> {
  235. println!(
  236. "{:15} {:15} {:>7} {:>8} {:>7} {:>8} GPU_NAMES",
  237. "Partition", "Node", "CPU_TOT", "CPU_FREE", "GPU_TOT", "GPU_FREE"
  238. );
  239. for info in slurm_availables()? {
  240. let names = if info.gpu_names.is_empty() {
  241. "-".to_string()
  242. } else {
  243. info.gpu_names.join(",")
  244. };
  245. println!(
  246. "{:15} {:15} {:7} {:8} {:7} {:8} {}",
  247. info.partition,
  248. info.node,
  249. info.cpu_total,
  250. info.cpu_free,
  251. info.gpu_total,
  252. info.gpu_free,
  253. names
  254. );
  255. }
  256. let u = max_gpu_per_node("h100");
  257. println!("H100 max available: {}", u.unwrap_or(0));
  258. let u = max_gpu_per_node("a100");
  259. println!("A100 max available: {}", u.unwrap_or(0));
  260. let u = max_cpus_per_task_shortq();
  261. println!("CPU max available: {}", u.unwrap_or(0));
  262. Ok(())
  263. }
  264. }