-
Notifications
You must be signed in to change notification settings - Fork 322
507 lines (429 loc) · 18.5 KB
/
Copy pathgetting-started.yml
File metadata and controls
507 lines (429 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File: getting-started.yml
# Code: Claude Code and Codex
# Review: Ryoichi Ando (ryoichi.ando@zozo.com)
# License: Apache v2.0
#
# Builds the real CUDA ppf-contact-solver and runs the single headless example
# (examples/headless.py) end-to-end on a GPU. Like the per-example workflows
# (see template/aws-template.yml), the GitHub-hosted ubuntu-latest runner is only
# an orchestrator: it assumes an AWS OIDC role, launches one ephemeral Deep
# Learning GPU EC2 instance, drives it over an EC2 Instance Connect SSH tunnel,
# and always terminates it. The GPU must be Ampere-or-newer (sm_86+): the CUDA
# build ships an sm_86 SASS anchor plus compute_86 PTX that only JITs forward, so
# the default g6/g6e (L4/L40S, sm_89) instances run it while an older card (e.g.
# a T4, sm_75) has no runnable kernel image.
name: Getting Started
on:
workflow_dispatch:
inputs:
instance_type:
description: 'EC2 instance type'
required: true
default: 'g6e.2xlarge'
type: choice
options:
- g6.2xlarge
- g6e.2xlarge
region:
description: 'AWS Region'
required: true
default: 'us-east-2'
type: choice
options:
- us-east-1
- us-east-2
- ap-northeast-1
jobs:
headless:
name: Run headless on GPU Instance
runs-on: ubuntu-latest
# EC2 boot + AMI/VPC lookup + full real-CUDA cargo build now happen inside
# the job (not on a pre-provisioned runner), so allow generous headroom.
timeout-minutes: 90
permissions:
id-token: write
contents: read
env:
AWS_REGION: ${{ github.event.inputs.region }}
INSTANCE_TYPE: ${{ github.event.inputs.instance_type }}
BRANCH: ${{ github.ref_name }}
WORKDIR: /home/ubuntu
USER: ubuntu
steps:
- name: Show input parameters
run: |
echo "## Input Parameters"
echo "Branch: ${{ github.ref_name }}"
echo "Instance Type: ${{ github.event.inputs.instance_type }}"
echo "Region: ${{ github.event.inputs.region }}"
- name: Checkout repository
uses: actions/checkout@v5
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 21600
- name: Verify AWS authentication
run: |
echo "Testing AWS authentication..."
aws sts get-caller-identity
echo "AWS Region: $AWS_REGION"
echo "Instance Type: $INSTANCE_TYPE"
echo "Branch: $BRANCH"
- name: Find Deep Learning AMI and network resources
id: setup
run: |
echo "Finding latest Deep Learning AMI with GPU support..."
AMI_ID=$(aws ec2 describe-images \
--owners amazon \
--filters \
"Name=name,Values=Deep Learning Base OSS Nvidia Driver GPU AMI (Ubuntu 24.04)*" \
"Name=state,Values=available" \
"Name=architecture,Values=x86_64" \
--query 'sort_by(Images, &CreationDate)[-1].ImageId' \
--region "$AWS_REGION" \
--output text)
if [ "$AMI_ID" = "None" ] || [ -z "$AMI_ID" ]; then
echo "ERROR: Deep Learning AMI not found in region $AWS_REGION"
exit 1
fi
echo "AMI_ID=$AMI_ID" >> $GITHUB_OUTPUT
echo "Found AMI: $AMI_ID"
# Get GitHub Actions dedicated VPC, subnet, and security group
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=github-actions-vpc" --query 'Vpcs[0].VpcId' --output text)
if [ "$VPC_ID" = "None" ] || [ -z "$VPC_ID" ]; then
echo "ERROR: github-actions-vpc not found in region $AWS_REGION"
exit 1
fi
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query 'Subnets[0].SubnetId' --output text)
if [ "$SUBNET_ID" = "None" ] || [ -z "$SUBNET_ID" ]; then
echo "ERROR: no subnet found in github-actions-vpc ($VPC_ID) in region $AWS_REGION"
exit 1
fi
SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=github-actions-sg" --query 'SecurityGroups[0].GroupId' --output text)
if [ "$SG_ID" = "None" ] || [ -z "$SG_ID" ]; then
echo "ERROR: github-actions-sg not found in github-actions-vpc ($VPC_ID) in region $AWS_REGION"
exit 1
fi
echo "::add-mask::$VPC_ID"
echo "::add-mask::$SUBNET_ID"
echo "::add-mask::$SG_ID"
echo "SUBNET_ID=$SUBNET_ID" >> $GITHUB_OUTPUT
echo "SG_ID=$SG_ID" >> $GITHUB_OUTPUT
echo "VPC: $VPC_ID, Subnet: $SUBNET_ID, SG: $SG_ID"
- name: Generate unique identifiers and SSH key
id: ids
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
RANDOM_SUFFIX=$(head /dev/urandom | tr -dc a-z0-9 | head -c 6)
TEMP_INSTANCE_ID="temp-${TIMESTAMP}-${RANDOM_SUFFIX}"
echo "TIMESTAMP=$TIMESTAMP" >> $GITHUB_OUTPUT
echo "TEMP_INSTANCE_ID=$TEMP_INSTANCE_ID" >> $GITHUB_OUTPUT
echo "Temporary Instance ID: $TEMP_INSTANCE_ID"
# Generate SSH key early so we can embed it in user-data
rm -f /tmp/ec2key /tmp/ec2key.pub
ssh-keygen -t rsa -f /tmp/ec2key -N "" -q
echo "SSH key generated"
- name: Create user data script
run: |
SSH_PUBKEY=$(cat /tmp/ec2key.pub)
cat > /tmp/user-data.sh << EOF
#!/bin/bash
set -x
exec > >(tee /var/log/user-data.log) 2>&1
echo "=== User Data Script Started ==="
# Wait for system to be ready
sleep 5
# Setup SSH key for persistent authentication (no 60s expiry)
mkdir -p /home/ubuntu/.ssh
echo "${SSH_PUBKEY}" >> /home/ubuntu/.ssh/authorized_keys
chown -R ubuntu:ubuntu /home/ubuntu/.ssh
chmod 700 /home/ubuntu/.ssh
chmod 600 /home/ubuntu/.ssh/authorized_keys
echo "SSH key installed permanently"
# Verify nvidia-smi is available
if command -v nvidia-smi &> /dev/null; then
echo "NVIDIA drivers confirmed"
nvidia-smi
else
echo "Warning: nvidia-smi not found"
fi
# Create workspace directory
mkdir -p /home/ubuntu/workspace
chown -R ubuntu:ubuntu /home/ubuntu/workspace
nvidia-smi | tee /tmp/nvidia-smi-output.txt
touch /tmp/setup-complete
echo "=== User Data Script Complete ==="
EOF
- name: Launch EC2 instance
id: instance
run: |
echo "Launching EC2 instance..."
# Base64 encode for AWS
USER_DATA=$(base64 -w 0 /tmp/user-data.sh)
INSTANCE_ID=$(aws ec2 run-instances \
--image-id "${{ steps.setup.outputs.AMI_ID }}" \
--instance-type "$INSTANCE_TYPE" \
--subnet-id "${{ steps.setup.outputs.SUBNET_ID }}" \
--security-group-ids "${{ steps.setup.outputs.SG_ID }}" \
--associate-public-ip-address \
--user-data "$USER_DATA" \
--block-device-mappings "DeviceName=/dev/sda1,Ebs={VolumeSize=256,VolumeType=gp3,DeleteOnTermination=true}" \
--tag-specifications \
"ResourceType=instance,Tags=[\
{Key=Name,Value=gpu-runner-${{ steps.ids.outputs.TIMESTAMP }}},\
{Key=ManagedBy,Value=GitHubActions},\
{Key=Purpose,Value=GPURunner},\
{Key=Workflow,Value=${{ github.workflow }}},\
{Key=RunId,Value=${{ github.run_id }}},\
{Key=Branch,Value=${{ env.BRANCH }}},\
{Key=Example,Value=getting-started}\
]" \
"ResourceType=volume,Tags=[\
{Key=Name,Value=gpu-runner-${{ steps.ids.outputs.TIMESTAMP }}-volume},\
{Key=ManagedBy,Value=GitHubActions},\
{Key=Purpose,Value=GPURunner},\
{Key=Workflow,Value=${{ github.workflow }}},\
{Key=Example,Value=getting-started}\
]" \
--instance-initiated-shutdown-behavior terminate \
--query 'Instances[0].InstanceId' \
--region "$AWS_REGION" \
--output text)
echo "INSTANCE_ID=$INSTANCE_ID" >> $GITHUB_OUTPUT
echo "Instance launched: $INSTANCE_ID"
- name: Wait for instance and establish tunnel
run: |
INSTANCE_ID="${{ steps.instance.outputs.INSTANCE_ID }}"
echo "Waiting for instance to be running..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" --region "$AWS_REGION"
echo "Instance is running, waiting for user-data to complete..."
# Wait for setup-complete (user-data finished, SSH key installed)
MAX_ATTEMPTS=60
for i in $(seq 1 $MAX_ATTEMPTS); do
# Open tunnel temporarily
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 3
# Check if setup is complete (includes SSH connectivity check)
if ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
ubuntu@localhost "test -f /tmp/setup-complete && echo READY" 2>/dev/null | grep -q READY; then
echo "Instance setup completed on attempt $i"
kill $TUNNEL_PID 2>/dev/null || true
break
fi
kill $TUNNEL_PID 2>/dev/null || true
if [ $i -eq $MAX_ATTEMPTS ]; then
echo "Setup timeout after $MAX_ATTEMPTS attempts, continuing anyway..."
break
fi
echo "Attempt $i/$MAX_ATTEMPTS: Setup not complete, waiting 10s..."
sleep 10
done
# Save instance ID for subsequent steps
echo "$INSTANCE_ID" > /tmp/instance_id.txt
- name: Create archive of repository
run: |
echo "Creating repository archive..."
git archive --format=tar.gz --output=/tmp/repo.tar.gz HEAD
- name: Transfer repository to instance
run: |
echo "Transferring repository to instance..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
rsync -avz -e "ssh -i /tmp/ec2key -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
/tmp/repo.tar.gz ubuntu@localhost:${WORKDIR}/
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
ubuntu@localhost \
"cd ${WORKDIR} && tar -xzf repo.tar.gz && rm repo.tar.gz"
kill $TUNNEL_PID 2>/dev/null || true
- name: Setup Python environment and run warmup
run: |
echo "Setting up Python environment and running warmup.py..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=10 \
ubuntu@localhost << 'ENDSSH'
set -e
cd ${WORKDIR}
# Run warmup.py
echo "Running warmup.py..."
python3 warmup.py --skip-confirmation
echo "Warmup completed"
ENDSSH
kill $TUNNEL_PID 2>/dev/null || true
- name: Build Rust project
run: |
echo "Building the real CUDA solver with cargo..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=10 \
ubuntu@localhost << 'ENDSSH'
set -e
cd ${WORKDIR}
# Setup Rust environment (cargo is not on the non-login SSH PATH)
source "$HOME/.cargo/env"
# Default features = the REAL CUDA backend. Never pass
# --features emulated / --no-default-features here: the emulated
# backend has no physics and would overwrite target/release.
echo "Running cargo build --release..."
cargo build --release
echo "Cargo build completed"
ENDSSH
kill $TUNNEL_PID 2>/dev/null || true
- name: Verify ppf-cts-py importable via frontend
# `cargo build --release` above builds the PyO3 cdylib into
# target/release/. frontend/__init__.py loads that cdylib by
# absolute path and registers it as `_ppf_cts_py`, so any python
# that imports `frontend` first can then use the Rust extension.
run: |
echo "Verifying ppf-cts-py importable via frontend on remote..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=10 \
ubuntu@localhost << 'ENDSSH'
set -e
cd ${WORKDIR}
source ~/.local/share/ppf-cts/venv/bin/activate
python -c "from frontend import _rust as m; print('schema_version', m.schema_version())"
echo "ppf-cts-py verified"
ENDSSH
kill $TUNNEL_PID 2>/dev/null || true
- name: Run headless
run: |
echo "Running examples/headless.py on the GPU instance..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=10 \
ubuntu@localhost \
"set -o pipefail && mkdir -p /tmp/ci && echo 'getting-started' > ${WORKDIR}/frontend/.CI && cd ${WORKDIR} && source ~/.local/share/ppf-cts/venv/bin/activate && PYTHONPATH=${WORKDIR} python3 examples/headless.py 2>&1 | tee /tmp/ci/headless.log"
kill $TUNNEL_PID 2>/dev/null || true
- name: Collect results
if: success() || failure()
run: |
echo "Collecting results..."
mkdir -p ci
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
# Delete large binary files on remote before copying to save bandwidth
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
ubuntu@localhost \
"find ~/.cache/ppf-cts/ci -type f \( -name '*.bin' -o -name '*.pickle' -o -name '*.ply' -o -name '*.gz' \) -delete 2>/dev/null" || true
# Copy CI output from ppf-cts cache directory
rsync -avz -e "ssh -i /tmp/ec2key -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
ubuntu@localhost:~/.cache/ppf-cts/ci/ ./ci/ || echo "No ppf-cts CI files found"
# Also copy logs from /tmp/ci
rsync -avz -e "ssh -i /tmp/ec2key -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
ubuntu@localhost:/tmp/ci/ ./ci/ || echo "No log files found"
kill $TUNNEL_PID 2>/dev/null || true
echo "## Collected Files:"
ls -laR ci/ | head -100 || echo "No files collected"
- name: Upload artifact
if: success() || failure()
uses: actions/upload-artifact@v6
with:
name: ci-getting-started
path: ci
retention-days: 3
- name: GPU information
if: success() || failure()
run: |
echo "Getting GPU information..."
INSTANCE_ID=$(cat /tmp/instance_id.txt)
aws ec2-instance-connect open-tunnel \
--instance-id "$INSTANCE_ID" \
--local-port 2222 &
TUNNEL_PID=$!
sleep 5
ssh -i /tmp/ec2key -p 2222 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
ubuntu@localhost \
"nvidia-smi" || echo "Failed to get GPU info"
kill $TUNNEL_PID 2>/dev/null || true
- name: Cleanup - SSH keys
if: always()
continue-on-error: true
run: |
# Clean up SSH keys
rm -f /tmp/ec2key /tmp/ec2key.pub /tmp/instance_id.txt
- name: Re-authenticate for cleanup
if: always()
continue-on-error: true
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 21600
- name: Cleanup - Terminate Instance
if: always()
continue-on-error: true
run: |
if [ -n "${{ steps.instance.outputs.INSTANCE_ID }}" ]; then
echo "Initiating instance termination: ${{ steps.instance.outputs.INSTANCE_ID }}"
aws ec2 terminate-instances \
--instance-ids "${{ steps.instance.outputs.INSTANCE_ID }}" \
--region "$AWS_REGION" || true
echo "Termination initiated. Instance will terminate in the background."
else
echo "No instance to terminate"
fi
- name: Summary
if: always()
run: |
echo "## Workflow Summary"
echo "- Region: $AWS_REGION"
echo "- Instance Type: $INSTANCE_TYPE"
echo "- Branch: $BRANCH"
echo "- Example: getting-started"
echo "- Instance ID: ${{ steps.instance.outputs.INSTANCE_ID || 'Not launched' }}"