Skip to content

Commit c04cc0b

Browse files
committed
Finally is alive!!
Add support for chief (coordinator) node in TensorFlow distributed training The actual train_tf_ps.py is only a placeholder for development testings and must be refined and correctly commented for a real workload. - Extend `train_tf_ps.py` to include a chief address and port in cluster configuration. - Update `make_parameter_server_strategy` to validate and configure the chief node, including `TF_CONFIG` setup. - Modify `run_tf_training_from_bastion.sh` to detect and validate a routable IPv4 address for the chief node. - Adjust `.gitignore` to exclude `output/`. - Add gRPC port mapping for the chief node in `docker-compose.yml`. - Update model saving path in `train_tf_ps.py` to include `.keras` extension.
1 parent 063acab commit c04cc0b

5 files changed

Lines changed: 99 additions & 10 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
*.terraform*
44
*tfstate*
55

6-
*config-kind-in-container*
6+
*config-kind-in-container*
7+
*output/

infra/local/external_workloads/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ services:
3333
- ../tf/config-kind-in-container:/root/.kube/config:ro
3434
environment:
3535
- KUBECONFIG=/root/.kube/config
36+
ports:
37+
- "2223:2223" # TensorFlow coordinator gRPC port
3638
command: [ "bash", "-lc", "sleep infinity" ]
3739
networks:
3840
- kind

infra/local/tf/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
FROM tensorflow/tensorflow:latest-gpu
22
LABEL authors="greg-ogs"
3+
# For bastion container outside the kubernetes cluster
34

45
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates curl gnupg
56

workloads/local_cluster_workloads/run_tf_training_from_bastion.sh

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env sh
2-
set -eu
2+
set -e
3+
set -u
34

45
# This script is intended to run inside the bastion container defined in infra/local/external_workloads/docker-compose.yml
56
# It discovers LoadBalancer IPs for TF worker/ps services, ensures TensorFlow is installed,
@@ -13,6 +14,35 @@ DATA_PATH=${DATA_PATH:-/data/health.csv}
1314
OUTPUT_DIR=${OUTPUT_DIR:-/workloads/output/$(date +%Y%m%d_%H%M%S)}
1415
EPOCHS=${EPOCHS:-3}
1516
BATCH_SIZE=${BATCH_SIZE:-64}
17+
CHIEF_PORT=${CHIEF_PORT:-2223}
18+
# Determine a routable IPv4 for the coordinator (bastion). Respect CHIEF_ADDR if provided.
19+
if [ -z "${CHIEF_ADDR:-}" ]; then
20+
# Try to detect default IPv4 via routing table
21+
if command -v ip >/dev/null 2>&1; then
22+
CANDIDATE=$(ip -4 route get 8.8.8.8 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
23+
if [ -n "$CANDIDATE" ]; then
24+
CHIEF_ADDR="$CANDIDATE"
25+
fi
26+
fi
27+
fi
28+
if [ -z "${CHIEF_ADDR:-}" ]; then
29+
# Fallback: hostname -I (capital i) and pick first IPv4 token
30+
if command -v hostname >/dev/null 2>&1; then
31+
for tok in $(hostname -I 2>/dev/null); do
32+
case "$tok" in
33+
*:*) ;; # skip IPv6 tokens
34+
*.*) CHIEF_ADDR="$tok"; break ;;
35+
esac
36+
done
37+
fi
38+
fi
39+
# Final validation: must be IPv4 (avoid IPv6 which breaks TF_CONFIG host:port handling)
40+
if [ -z "${CHIEF_ADDR:-}" ] || echo "$CHIEF_ADDR" | grep -q ":"; then
41+
echo "Unable to auto-detect a valid IPv4 CHIEF_ADDR. Set CHIEF_ADDR to an IPv4 reachable from K8s pods (e.g., 172.x/192.168.x)." >&2
42+
exit 4
43+
fi
44+
45+
echo "Chief (coordinator) will advertise ${CHIEF_ADDR}:${CHIEF_PORT}"
1646

1747
# Ensure kubectl available
1848
if ! command -v kubectl >/dev/null 2>&1; then
@@ -74,6 +104,19 @@ fi
74104

75105
mkdir -p "$OUTPUT_DIR"
76106

107+
# Ensure gRPC to chief bypasses proxies (avoid unexpected :443 redirection)
108+
if [ -n "${no_proxy:-}" ]; then
109+
no_proxy="${no_proxy},${CHIEF_ADDR}"
110+
else
111+
no_proxy="${CHIEF_ADDR}"
112+
fi
113+
if [ -n "${NO_PROXY:-}" ]; then
114+
NO_PROXY="${NO_PROXY},${CHIEF_ADDR}"
115+
else
116+
NO_PROXY="${CHIEF_ADDR}"
117+
fi
118+
export no_proxy NO_PROXY
119+
77120
"$PYTHON" /workloads/local_cluster_workloads/train_tf_ps.py \
78121
--data-path "$DATA_PATH" \
79122
--output-dir "$OUTPUT_DIR" \
@@ -83,4 +126,6 @@ mkdir -p "$OUTPUT_DIR"
83126
--worker-replicas "$WORKER_COUNT" \
84127
--ps-replicas "$PS_COUNT" \
85128
--worker-addrs "$WORKER_ADDRS_CSV" \
86-
--ps-addrs "$PS_ADDRS_CSV"
129+
--ps-addrs "$PS_ADDRS_CSV" \
130+
--chief-addr "$CHIEF_ADDR" \
131+
--chief-port "$CHIEF_PORT"

workloads/local_cluster_workloads/train_tf_ps.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
import time
88
from typing import List, Tuple, Optional
99

10+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
11+
import warnings
12+
warnings.filterwarnings("ignore", category=UserWarning, module='google.protobuf.runtime_version')
13+
1014
import numpy as np
1115
import tensorflow as tf
1216
from urllib.request import urlopen
@@ -100,7 +104,7 @@ def build_sequential_model(input_dim: int, num_classes: int) -> tf.keras.Model:
100104
# Distributed strategy helpers
101105
# ----------------------------
102106

103-
def build_cluster_def(worker_replicas: int, ps_replicas: int, port: int, worker_addrs: Optional[List[str]] = None, ps_addrs: Optional[List[str]] = None) -> dict:
107+
def build_cluster_def(worker_replicas: int, ps_replicas: int, port: int, worker_addrs: Optional[List[str]] = None, ps_addrs: Optional[List[str]] = None, chief_addr: Optional[str] = None, chief_port: int = 2223) -> dict:
104108
# If explicit addresses provided (e.g., from bastion via LoadBalancer IPs), use them
105109
if worker_addrs:
106110
workers = worker_addrs
@@ -113,13 +117,43 @@ def build_cluster_def(worker_replicas: int, ps_replicas: int, port: int, worker_
113117
else:
114118
ps = [f"tf-trainer-ps-{i}.tf-trainer-ps-headless:{port}" for i in range(ps_replicas)]
115119
cluster_def["ps"] = ps
120+
# Include chief if provided (must be routable from the K8s pods)
121+
if chief_addr:
122+
cluster_def["chief"] = [f"{chief_addr}:{chief_port}"]
116123
return cluster_def
117124

118125

119-
def make_parameter_server_strategy(worker_replicas: int, ps_replicas: int, port: int = 2222, worker_addrs: Optional[List[str]] = None, ps_addrs: Optional[List[str]] = None) -> tf.distribute.ParameterServerStrategy:
120-
cluster_def = build_cluster_def(worker_replicas, ps_replicas, port, worker_addrs, ps_addrs)
126+
def make_parameter_server_strategy(worker_replicas: int, ps_replicas: int, port: int = 2222, worker_addrs: Optional[List[str]] = None, ps_addrs: Optional[List[str]] = None, chief_addr: Optional[str] = None, chief_port: int = 2223) -> tf.distribute.ParameterServerStrategy:
127+
cluster_def = build_cluster_def(worker_replicas, ps_replicas, port, worker_addrs, ps_addrs, chief_addr, chief_port)
121128
print("Computed ClusterSpec:", json.dumps(cluster_def), flush=True)
122129

130+
# Basic validation and sanitization for chief address to avoid IPv6 and malformed inputs
131+
if chief_addr:
132+
# Reject IPv6 literals or bracketed addresses and any scheme prefixes
133+
if ":" in chief_addr and "." not in chief_addr:
134+
raise RuntimeError(
135+
f"chief_addr appears to be IPv6 ('{chief_addr}'). Please provide an IPv4 address reachable from K8s pods."
136+
)
137+
if any(sym in chief_addr for sym in ["/", "[", "]", " "]):
138+
raise RuntimeError(
139+
f"chief_addr '{chief_addr}' is malformed. Provide a raw IPv4 like 192.168.1.10 without scheme or brackets."
140+
)
141+
# Optional strict IPv4 check
142+
parts = chief_addr.split(".")
143+
if len(parts) != 4 or any(not p.isdigit() or not (0 <= int(p) <= 255) for p in parts):
144+
raise RuntimeError(
145+
f"chief_addr '{chief_addr}' is not a valid IPv4 address."
146+
)
147+
148+
# If a chief address is provided, declare this process as chief via TF_CONFIG
149+
if chief_addr:
150+
tf_config = {
151+
"cluster": cluster_def,
152+
"task": {"type": "chief", "index": 0},
153+
}
154+
os.environ["TF_CONFIG"] = json.dumps(tf_config)
155+
print("TF_CONFIG set:", os.environ["TF_CONFIG"], flush=True)
156+
123157
resolver = tf.distribute.cluster_resolver.SimpleClusterResolver(
124158
cluster_spec=tf.train.ClusterSpec(cluster_def),
125159
rpc_layer="grpc",
@@ -148,6 +182,8 @@ def run_training(
148182
port: int = 2222,
149183
worker_addrs: Optional[List[str]] = None,
150184
ps_addrs: Optional[List[str]] = None,
185+
chief_addr: Optional[str] = None,
186+
chief_port: int = 2223,
151187
) -> None:
152188
os.makedirs(output_dir, exist_ok=True)
153189

@@ -173,7 +209,7 @@ def run_training(
173209
print("Worker addrs:", worker_addrs)
174210
if ps_addrs:
175211
print("PS addrs:", ps_addrs)
176-
strategy = make_parameter_server_strategy(worker_replicas, ps_replicas, port, worker_addrs, ps_addrs)
212+
strategy = make_parameter_server_strategy(worker_replicas, ps_replicas, port, worker_addrs, ps_addrs, chief_addr, chief_port)
177213

178214
# Switch to ClusterCoordinator-based custom training loop (DatasetCreator removed)
179215
def per_worker_dataset_fn(input_context: Optional[tf.distribute.InputContext] = None):
@@ -237,7 +273,7 @@ def step_fn(inputs):
237273
history = model.fit(ds, epochs=epochs, steps_per_epoch=steps_per_epoch)
238274

239275
# Save model
240-
save_path = os.path.join(output_dir, "model")
276+
save_path = os.path.join(output_dir, "model.keras")
241277
model.save(save_path)
242278
print(f"Model saved to: {save_path}")
243279

@@ -250,7 +286,7 @@ def parse_args(argv: List[str]):
250286
parser = argparse.ArgumentParser(description="Train TF Keras model on health.csv with optional ParameterServerStrategy")
251287
parser.add_argument("--data-path", default=os.environ.get("DATA_PATH", "infra/local/mysql-database/health.csv"), help="Path to CSV (when running on bastion/host)")
252288
parser.add_argument("--data-url", default=os.environ.get("DATA_URL", ""), help="HTTP(S) URL to CSV (used inside cluster if path not mounted)")
253-
parser.add_argument("--output-dir", default=os.environ.get("OUTPUT_DIR", "/tmp/tf-model"))
289+
parser.add_argument("--output-dir", default=os.environ.get("OUTPUT_DIR", "./tf-model"))
254290
parser.add_argument("--epochs", type=int, default=int(os.environ.get("EPOCHS", "3")))
255291
parser.add_argument("--batch-size", type=int, default=int(os.environ.get("BATCH_SIZE", "64")))
256292
parser.add_argument("--use-ps", action="store_true", help="Enable ParameterServerStrategy coordinator mode")
@@ -259,12 +295,14 @@ def parse_args(argv: List[str]):
259295
parser.add_argument("--port", type=int, default=int(os.environ.get("TF_GRPC_PORT", "2222")))
260296
parser.add_argument("--worker-addrs", default=os.environ.get("WORKER_ADDRS", ""), help="Comma-separated worker addresses (host:port) when running outside cluster")
261297
parser.add_argument("--ps-addrs", default=os.environ.get("PS_ADDRS", ""), help="Comma-separated ps addresses (host:port) when running outside cluster")
298+
parser.add_argument("--chief-addr", default=os.environ.get("CHIEF_ADDR", ""), help="Routable IPv4 address of the coordinator (bastion) accessible from K8s pods")
299+
parser.add_argument("--chief-port", type=int, default=int(os.environ.get("CHIEF_PORT", "2223")), help="Coordinator gRPC port (exposed on tf-bastion)")
262300
return parser.parse_args(argv)
263301

264302

265303
if __name__ == "__main__":
266304
args = parse_args(sys.argv[1:])
267-
305+
input("Press enter to continue...")
268306
# Resolve data source: prefer local path; if not existent and data-url provided -> use URL
269307
data_source = args.data_path
270308
if not os.path.exists(data_source):
@@ -288,4 +326,6 @@ def parse_args(argv: List[str]):
288326
port=args.port,
289327
worker_addrs=worker_addrs,
290328
ps_addrs=ps_addrs,
329+
chief_addr=(args.chief_addr if args.chief_addr else None),
330+
chief_port=args.chief_port,
291331
)

0 commit comments

Comments
 (0)