discover.sh 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # Sourced by entrypoint.sh. Fills in the networking and the head/worker role
  2. # from mentat when compose/.env leaves them out. Every function returns without
  3. # asking mentat when its values are already set.
  4. #
  5. # dev/patch-tests/_entrypoint_discovery_test.sh sources this file with
  6. # mentat_query and sleep stubbed.
  7. # The one place that asks mentat anything, so the test can answer from
  8. # fixtures. `local` asks this box's daemon over HTTP. The control port would
  9. # relay the question to mentat's head, and the head answers about its own box. `group` is the head's view of MENTAT_GROUP, where the agents are.
  10. mentat_query() {
  11. case "$1" in
  12. local) curl -sf --max-time 5 http://127.0.0.1:6380/status ;;
  13. group) ray status --address="$RAY_ADDRESS" --group "$MENTAT_GROUP" --json 2>/dev/null ;;
  14. esac
  15. }
  16. # The local daemon's view as "node_ip|lan address|fabric prefixes". The lan
  17. # address falls back to node_ip when nothing is tagged lan, since mentat names
  18. # the box by node_ip either way. A prefix is "a.b.c.", the form of CLUSTER_SUBNET
  19. # and FABRIC_SUBNETS, one per rdma-tagged address and sorted, so every box
  20. # lists its PCIe roots in the same order.
  21. mentat_local_view() {
  22. mentat_query local | python3 -c '
  23. import ipaddress, json, sys
  24. try:
  25. s = json.load(sys.stdin)
  26. except ValueError:
  27. sys.exit(1)
  28. tags = s.get("addr_tags") or {}
  29. node = s.get("node_ip") or ""
  30. lan = [a for a in s.get("addrs") or [] if "lan" in tags.get(a, [])]
  31. fabric = {a.rsplit(".", 1)[0] + "." for a, t in tags.items() if "rdma" in t and ":" not in a}
  32. fabric = sorted(fabric, key=lambda p: ipaddress.ip_address(p + "0"))
  33. print(node + "|" + (lan[0] if lan else node) + "|" + " ".join(fabric))
  34. '
  35. }
  36. # Sets whichever of VLLM_HOST_IP, CLUSTER_SUBNET and FABRIC_SUBNETS are unset,
  37. # and MENTAT_NODE_IP when the boxes will elect a head. Waits for the daemon,
  38. # and for its rdma addresses when the fabric is needed: they are also absent
  39. # while the switch reboots, and exiting then turns a two-minute outage into a
  40. # restart loop racing it. FABRIC_WAIT_S bounds the wait; 0 waits forever.
  41. discover_network() {
  42. local want_ip="" want_fabric="" want_node="" view node lan fabric waited=0
  43. [[ -z "${VLLM_HOST_IP:-}" ]] && want_ip=1
  44. [[ -z "${CLUSTER_SUBNET:-}" && -z "${FABRIC_SUBNETS:-}" ]] && want_fabric=1
  45. # Every box registers with its own daemon, and an agent that reaches its
  46. # daemon over loopback leaves its address out of its agent id. The relay to
  47. # the head fills in the address but not the id, so four boxes would register
  48. # as one agent and replace each other in a loop. Naming the box keeps the
  49. # ids apart. It must be the daemon's own node_ip, or mentat refuses it.
  50. [[ -z "${ROLE:-}" && -z "${HEAD_HOST:-}" && -z "${MENTAT_NODE_IP:-}" ]] && want_node=1
  51. [[ -n "$want_ip$want_fabric$want_node" ]] || return 0
  52. while :; do
  53. view=$(mentat_local_view) || view=""
  54. IFS='|' read -r node lan fabric <<< "$view"
  55. [[ -n "$node" && ( -z "$want_fabric" || -n "$fabric" ) ]] && break
  56. if (( ${FABRIC_WAIT_S:-0} > 0 && waited >= ${FABRIC_WAIT_S:-0} )); then
  57. if [[ -z "$node" ]]; then
  58. echo "FATAL: no answer from mentatd at 127.0.0.1:6380 after ${waited}s." >&2
  59. echo "Start mentatd on this box, or set VLLM_HOST_IP, CLUSTER_SUBNET, ROLE and HEAD_HOST in .env." >&2
  60. else
  61. echo "FATAL: mentatd lists no address tagged rdma after ${waited}s." >&2
  62. echo "Tag the ConnectX interfaces in MENTAT_ANNOUNCE_IFACES, or set FABRIC_SUBNETS in .env." >&2
  63. fi
  64. exit 1
  65. fi
  66. if (( waited % 60 == 0 )); then
  67. if [[ -z "$node" ]]; then echo "waiting for mentatd at 127.0.0.1:6380 (${waited}s)"
  68. else echo "waiting for mentatd to list an rdma address (${waited}s)"; fi
  69. fi
  70. sleep 10
  71. waited=$(( waited + 10 ))
  72. done
  73. [[ -n "$want_ip" ]] && VLLM_HOST_IP="$lan"
  74. [[ -n "$want_fabric" ]] && FABRIC_SUBNETS="$fabric"
  75. [[ -n "$want_node" ]] && export MENTAT_NODE_IP="$node"
  76. echo "mentat: node $node, VLLM_HOST_IP=$VLLM_HOST_IP, fabric ${FABRIC_SUBNETS:-${CLUSTER_SUBNET:-}}"
  77. }
  78. # The group's live GPU agents as "count|addresses", addresses sorted
  79. # numerically. An agent with an empty GPU list is a `python -m ray.register`
  80. # announcement, so the count skips it. A box counts once even if an
  81. # old registration of it lingers.
  82. mentat_group_view() {
  83. mentat_query group | python3 -c '
  84. import ipaddress, json, sys
  85. try:
  86. s = json.load(sys.stdin)
  87. except ValueError:
  88. sys.exit(1)
  89. agents = ((s.get("groups") or {}).get(sys.argv[1]) or {}).get("agents") or {}
  90. ips = {a.get("node_ip") for a in agents.values()
  91. if a.get("alive") and (a.get("machine") or {}).get("gpus") and a.get("node_ip")}
  92. ips = sorted(ips, key=lambda a: (ipaddress.ip_address(a).version, ipaddress.ip_address(a)))
  93. print(str(len(ips)) + "|" + " ".join(ips))
  94. ' "$MENTAT_GROUP"
  95. }
  96. # Waits until MENTAT_GROUP has TP live GPU agents, then makes the lowest
  97. # address the head. Every box runs this against the same list and reaches the
  98. # same answer without talking to the others. Sets and exports HEAD_HOST and
  99. # ROLE. WORKER_WAIT_S bounds the wait. 0 waits forever, so a switch reboot
  100. # is a pause.
  101. elect_head() {
  102. local view n ips waited=0
  103. while :; do
  104. view=$(mentat_group_view) || view=""
  105. IFS='|' read -r n ips <<< "$view"
  106. (( ${n:-0} >= TP )) && break
  107. if (( ${WORKER_WAIT_S:-0} > 0 && waited >= ${WORKER_WAIT_S:-0} )); then
  108. echo "FATAL: only ${n:-0} of $TP agents in group $MENTAT_GROUP after ${waited}s (${ips:-none})" >&2
  109. exit 1
  110. fi
  111. (( waited % 60 )) || echo "electing: waiting for $TP agents in group $MENTAT_GROUP, have ${n:-0} (${waited}s)"
  112. sleep 5
  113. waited=$(( waited + 5 ))
  114. done
  115. HEAD_HOST="${ips%% *}"
  116. if [[ "$HEAD_HOST" == "$MENTAT_NODE_IP" ]]; then ROLE=head; else ROLE=worker; fi
  117. export HEAD_HOST ROLE
  118. echo "election: candidates $ips; head $HEAD_HOST; this box ($MENTAT_NODE_IP) is $ROLE"
  119. }
  120. # With HEAD_HOST set and ROLE not, this box is the head if it holds HEAD_HOST.
  121. # Leaves the head's resolved address in _head_ip for the entrypoint's checks.
  122. fixed_role() {
  123. _head_ip=$(getent ahostsv4 "$HEAD_HOST" 2>/dev/null | awk 'NR==1 {print $1}' || true)
  124. _head_ip=${_head_ip:-$HEAD_HOST}
  125. if [[ -z "${ROLE:-}" ]]; then
  126. if [[ "$VLLM_HOST_IP" == "$_head_ip" ]]; then ROLE=head; else ROLE=worker; fi
  127. fi
  128. export ROLE
  129. }