Skip to content

Commit 063acab

Browse files
committed
Switch train_tf_ps.py to ClusterCoordinator custom training.
Problems with the callback of chief
1 parent ce33292 commit 063acab

2 files changed

Lines changed: 86 additions & 33 deletions

File tree

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
#!/usr/bin/env bash
2-
set -euo pipefail
1+
#!/usr/bin/env sh
2+
set -eu
33

44
# This script is intended to run inside the bastion container defined in infra/local/external_workloads/docker-compose.yml
55
# It discovers LoadBalancer IPs for TF worker/ps services, ensures TensorFlow is installed,
66
# and launches the training coordinator that parallelizes across the K8s pods.
77

88
# Config
9-
WORKER_SERVICES=(tf-trainer-0 tf-trainer-1)
10-
PS_SERVICE=tf-trainer-ps-0
9+
WORKER_SERVICES="tf-trainer-0 tf-trainer-1"
10+
PS_SERVICE="tf-trainer-ps-0"
1111
PORT=2222
1212
DATA_PATH=${DATA_PATH:-/data/health.csv}
1313
OUTPUT_DIR=${OUTPUT_DIR:-/workloads/output/$(date +%Y%m%d_%H%M%S)}
@@ -22,55 +22,65 @@ fi
2222

2323
# Resolve IPs from LoadBalancer services
2424
get_lb_ip() {
25-
local svc="$1"
26-
local ip
25+
svc="$1"
2726
ip=$(kubectl get svc "$svc" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
28-
if [[ -z "$ip" ]]; then
27+
if [ -z "$ip" ]; then
2928
# Some environments set hostname instead of ip
3029
ip=$(kubectl get svc "$svc" -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || true)
3130
fi
32-
if [[ -z "$ip" ]]; then
31+
if [ -z "$ip" ]; then
3332
echo "Service $svc has no LoadBalancer IP/hostname assigned yet. Ensure MetalLB or an LB is configured." >&2
3433
exit 2
3534
fi
3635
echo "$ip"
3736
}
3837

39-
WORKER_ADDRS=()
40-
for svc in "${WORKER_SERVICES[@]}"; do
38+
WORKER_ADDRS_CSV=""
39+
WORKER_COUNT=0
40+
for svc in $WORKER_SERVICES; do
4141
ip=$(get_lb_ip "$svc")
42-
WORKER_ADDRS+=("${ip}:${PORT}")
43-
echo "Worker $svc -> ${ip}:${PORT}"
42+
addr="${ip}:${PORT}"
43+
if [ -z "$WORKER_ADDRS_CSV" ]; then
44+
WORKER_ADDRS_CSV="$addr"
45+
else
46+
WORKER_ADDRS_CSV="$WORKER_ADDRS_CSV,$addr"
47+
fi
48+
WORKER_COUNT=$((WORKER_COUNT + 1))
49+
echo "Worker $svc -> $addr"
4450
done
51+
4552
PS_IP=$(get_lb_ip "$PS_SERVICE")
46-
PS_ADDRS=("${PS_IP}:${PORT}")
53+
PS_ADDRS_CSV="${PS_IP}:${PORT}"
54+
PS_COUNT=1
4755
echo "PS $PS_SERVICE -> ${PS_IP}:${PORT}"
4856

49-
# Ensure python and pip exist
50-
if ! command -v python >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
51-
alias python=python3
52-
fi
53-
if ! command -v python >/dev/null 2>&1; then
57+
# Ensure python exists; prefer python then python3
58+
PYTHON=""
59+
if command -v python >/dev/null 2>&1; then
60+
PYTHON=python
61+
elif command -v python3 >/dev/null 2>&1; then
62+
PYTHON=python3
63+
else
5464
echo "Python is required inside bastion. Aborting." >&2
5565
exit 3
5666
fi
5767

5868
# Install TensorFlow if missing
59-
if ! python -c "import tensorflow as tf; print(tf.__version__)" >/dev/null 2>&1; then
69+
if ! "$PYTHON" -c "import tensorflow as tf; print(tf.__version__)" >/dev/null 2>&1; then
6070
echo "Installing TensorFlow (CPU) inside bastion container..."
61-
pip install --no-cache-dir --upgrade pip >/dev/null
62-
pip install --no-cache-dir tensorflow >/dev/null
71+
"$PYTHON" -m pip install --no-cache-dir --upgrade pip >/dev/null 2>&1 || true
72+
"$PYTHON" -m pip install --no-cache-dir tensorflow >/dev/null
6373
fi
6474

6575
mkdir -p "$OUTPUT_DIR"
6676

67-
python /workloads/local_cluster_workloads/train_tf_ps.py \
77+
"$PYTHON" /workloads/local_cluster_workloads/train_tf_ps.py \
6878
--data-path "$DATA_PATH" \
6979
--output-dir "$OUTPUT_DIR" \
7080
--epochs "$EPOCHS" \
7181
--batch-size "$BATCH_SIZE" \
7282
--use-ps \
73-
--worker-replicas ${#WORKER_ADDRS[@]} \
74-
--ps-replicas ${#PS_ADDRS[@]} \
75-
--worker-addrs "$(IFS=,; echo "${WORKER_ADDRS[*]}")" \
76-
--ps-addrs "$(IFS=,; echo "${PS_ADDRS[*]}")"
83+
--worker-replicas "$WORKER_COUNT" \
84+
--ps-replicas "$PS_COUNT" \
85+
--worker-addrs "$WORKER_ADDRS_CSV" \
86+
--ps-addrs "$PS_ADDRS_CSV"

workloads/local_cluster_workloads/train_tf_ps.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ def make_parameter_server_strategy(worker_replicas: int, ps_replicas: int, port:
122122

123123
resolver = tf.distribute.cluster_resolver.SimpleClusterResolver(
124124
cluster_spec=tf.train.ClusterSpec(cluster_def),
125-
task_type="chief",
126-
task_id=0,
127125
rpc_layer="grpc",
128126
)
129127
variable_partitioner = tf.distribute.experimental.partitioners.MinSizePartitioner(
@@ -177,17 +175,62 @@ def run_training(
177175
print("PS addrs:", ps_addrs)
178176
strategy = make_parameter_server_strategy(worker_replicas, ps_replicas, port, worker_addrs, ps_addrs)
179177

180-
# DatasetCreator lets Keras coordinate dataset distribution for PS strategy
181-
def dataset_fn(input_context: tf.distribute.InputContext):
178+
# Switch to ClusterCoordinator-based custom training loop (DatasetCreator removed)
179+
def per_worker_dataset_fn(input_context: Optional[tf.distribute.InputContext] = None):
182180
local_ds = tf.data.Dataset.from_tensor_slices((X, y))
183-
local_ds = local_ds.shard(input_context.num_input_pipelines, input_context.input_pipeline_id)
181+
if input_context is not None:
182+
local_ds = local_ds.shard(input_context.num_input_pipelines, input_context.input_pipeline_id)
184183
local_ds = local_ds.shuffle(buffer_size=min(10000, len(X))).batch(batch_size).repeat()
185184
return local_ds
186185

187186
with strategy.scope():
188187
model = build_sequential_model(input_dim, num_classes)
189-
creator = tf.keras.utils.experimental.DatasetCreator(dataset_fn)
190-
history = model.fit(creator, epochs=epochs, steps_per_epoch=steps_per_epoch)
188+
# Build losses/optimizer/metrics explicitly for custom loop
189+
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
190+
loss_obj = tf.keras.losses.SparseCategoricalCrossentropy()
191+
train_acc = tf.keras.metrics.SparseCategoricalAccuracy()
192+
train_loss = tf.keras.metrics.Mean()
193+
194+
# Create a ClusterCoordinator to drive training from the chief/coordinator process
195+
coordinator = tf.distribute.coordinator.ClusterCoordinator(strategy)
196+
per_worker_ds = coordinator.create_per_worker_dataset(per_worker_dataset_fn)
197+
per_worker_iter = iter(per_worker_ds)
198+
199+
@tf.function
200+
def per_worker_train_step(iterator):
201+
def step_fn(inputs):
202+
features, labels = inputs
203+
with tf.GradientTape() as tape:
204+
logits = model(features, training=True)
205+
loss = loss_obj(labels, logits)
206+
# Add possible regularization losses
207+
loss += tf.add_n(model.losses) if model.losses else 0.0
208+
grads = tape.gradient(loss, model.trainable_variables)
209+
optimizer.apply_gradients(zip(grads, model.trainable_variables))
210+
train_acc.update_state(labels, logits)
211+
train_loss.update_state(loss)
212+
return loss
213+
214+
return strategy.run(step_fn, args=(next(iterator),))
215+
216+
# Training loop
217+
for epoch in range(epochs):
218+
print(f"Starting epoch {epoch+1}/{epochs}...")
219+
train_acc.reset_state()
220+
train_loss.reset_state()
221+
222+
# Schedule one step per required step_per_epoch; wait for completion
223+
futures = []
224+
for _ in range(steps_per_epoch):
225+
f = coordinator.schedule(per_worker_train_step, args=(per_worker_iter,))
226+
futures.append(f)
227+
# Block until all scheduled steps finish
228+
coordinator.join()
229+
230+
print(f"Epoch {epoch+1} - loss: {train_loss.result().numpy():.4f} - accuracy: {train_acc.result().numpy():.4f}")
231+
232+
# Mimic Keras History-like output for downstream logging
233+
history = type("_H", (), {"history": {"accuracy": [train_acc.result().numpy()]}})()
191234
else:
192235
print("Running single-process (no distributed strategy).")
193236
model = build_sequential_model(input_dim, num_classes)

0 commit comments

Comments
 (0)