mod.rs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. pub mod commands;
  2. pub mod discover;
  3. pub mod ops;
  4. pub mod serve;
  5. pub mod session;
  6. use anyhow::Result;
  7. use crate::cli::TmuxCommands;
  8. use crate::config::{Config, NcclConfig, Node, Recipe};
  9. /// Manages tmux sessions for model inference across a DGX Spark cluster.
  10. #[derive(Clone)]
  11. pub struct TmuxManager;
  12. impl TmuxManager {
  13. /// Generates a namespaced session name for a recipe and node rank.
  14. /// Format: `{recipe_name}-{engine}-r{rank}`
  15. pub fn session_name(&self, recipe: &Recipe, rank: u32) -> String {
  16. // Replace shell-unsafe characters (parentheses, quotes, etc.)
  17. // with underscores so the session name doesn't trigger glob
  18. // expansion or shell injection when used in tmux commands.
  19. let name = recipe
  20. .name
  21. .chars()
  22. .map(|c| {
  23. if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
  24. c
  25. } else {
  26. '_'
  27. }
  28. })
  29. .collect::<String>();
  30. format!("{}-{}-r{}", name, recipe.engine, rank)
  31. }
  32. /// Builds the list of `export` commands for NCCL environment variables.
  33. pub fn build_nccl_setup(&self, nccl: Option<&NcclConfig>) -> Vec<String> {
  34. match nccl {
  35. Some(nccl) => vec![
  36. format!("export NCCL_IB_HCA=\"{}\"", nccl.ib_hca),
  37. format!("export NCCL_SOCKET_IFNAME=\"{}\"", nccl.socket_ifname),
  38. format!(
  39. "export NCCL_IB_GID_INDEX={}",
  40. // Unresolved (dry-run) renders `auto`; real launches
  41. // always have the per-node detected/pinned value.
  42. nccl.ib_gid_index
  43. .map_or_else(|| "auto".to_string(), |g| g.to_string())
  44. ),
  45. format!("export NCCL_IB_DISABLE={}", nccl.ib_disable),
  46. ],
  47. None => Vec::new(),
  48. }
  49. }
  50. /// Builds engine-specific host IP environment variable.
  51. pub fn build_host_ip_export(&self, engine: &str, node: &Node) -> Vec<String> {
  52. let ip = node.effective_ib_ip();
  53. match engine {
  54. "vllm" => vec![format!("export VLLM_HOST_IP={}", ip)],
  55. "sglang" => vec![format!("export SGLANG_WORKER_IP={}", ip)],
  56. _ => vec![
  57. format!("export VLLM_HOST_IP={}", ip),
  58. format!("export SGLANG_WORKER_IP={}", ip),
  59. ],
  60. }
  61. }
  62. }
  63. /// Detailed health status of a tmux session.
  64. #[derive(Debug, Clone)]
  65. pub struct SessionHealth {
  66. pub session_exists: bool,
  67. pub process_alive: bool,
  68. pub gpu_ok: bool,
  69. pub api_ok: bool,
  70. }
  71. impl SessionHealth {
  72. pub fn is_healthy(&self) -> bool {
  73. self.session_exists && self.process_alive && self.gpu_ok && self.api_ok
  74. }
  75. pub fn is_degraded(&self) -> bool {
  76. self.session_exists && !self.process_alive
  77. }
  78. }
  79. /// Top-level handler for all `cargo run tmux <subcommand>` commands.
  80. pub async fn handle_tmux(config: &Config, cmd: TmuxCommands, dry_run: bool) -> Result<()> {
  81. match cmd {
  82. TmuxCommands::Serve {
  83. recipe,
  84. cluster,
  85. skip_before_serve,
  86. } => {
  87. serve::tmux_serve(
  88. config,
  89. &recipe,
  90. &cluster,
  91. skip_before_serve,
  92. dry_run,
  93. false,
  94. false,
  95. )
  96. .await?;
  97. }
  98. TmuxCommands::Stop { recipe, cluster } => {
  99. ops::tmux_stop(config, &recipe, &cluster, dry_run).await?;
  100. }
  101. TmuxCommands::Status { recipe, cluster } => {
  102. ops::tmux_status(config, &recipe, &cluster, dry_run).await?;
  103. }
  104. TmuxCommands::Logs {
  105. recipe,
  106. follow,
  107. cluster,
  108. } => {
  109. ops::tmux_logs(config, &recipe, &cluster, follow, dry_run).await?;
  110. }
  111. TmuxCommands::Attach { recipe, cluster } => {
  112. ops::tmux_attach(config, &recipe, &cluster, dry_run).await?;
  113. }
  114. }
  115. Ok(())
  116. }