Skip to content

Commit 3bab7d5

Browse files
authored
Merge pull request #2288 from transformerlab/fix/remote_launch_cwd
Fix remote working directory by storing in a file
2 parents a51398b + 6b9dd1e commit 3bab7d5

8 files changed

Lines changed: 144 additions & 3 deletions

File tree

api/localprovider_pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies = [
3232
"soundfile==0.13.1",
3333
"tensorboardX==2.6.2.2",
3434
"timm==1.0.15",
35-
"transformerlab==0.1.42",
35+
"transformerlab==0.1.43",
3636
"transformerlab-inference==0.2.52",
3737
"transformers==4.57.1",
3838
"wandb==0.23.1",

api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies = [
2727
"python-dotenv==1.2.2",
2828
"python-multipart==0.0.32",
2929
"sqlalchemy[asyncio]==2.0.50",
30-
"transformerlab==0.1.42",
30+
"transformerlab==0.1.43",
3131
"transformerlab-inference==0.2.52",
3232
"uvicorn==0.35.0",
3333
"watchfiles==1.2.0",

api/test/test_remote_workdir_cd.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Tests for prepend_remote_workdir_cd.
2+
3+
Regression coverage for the `//main.py` quirk: on remote providers the run command
4+
executes from the provider's default cwd (e.g. `/` for Lambda cloud-init), not the
5+
directory where lab.copy_file_mounts() synced the task files. prepend_remote_workdir_cd
6+
prefixes a `cd` into the recorded workdir so a bare `python main.py` resolves.
7+
"""
8+
9+
from transformerlab.services.compute_provider.launch_credentials import WORKDIR_FILE_PATH
10+
from transformerlab.services.compute_provider.launch_template import prepend_remote_workdir_cd
11+
from transformerlab.shared.models.models import ProviderType
12+
13+
EXPECTED_PREFIX = f'cd "$(cat {WORKDIR_FILE_PATH} 2>/dev/null || echo "$HOME")" && '
14+
15+
16+
def test_prefixes_cd_for_remote_provider_with_file_mounts():
17+
for provider_type in (
18+
ProviderType.LAMBDA.value,
19+
ProviderType.RUNPOD.value,
20+
ProviderType.SKYPILOT.value,
21+
ProviderType.VASTAI.value,
22+
ProviderType.DSTACK.value,
23+
):
24+
out = prepend_remote_workdir_cd(
25+
"python main.py",
26+
task_id="task-123",
27+
file_mounts=True,
28+
provider_type=provider_type,
29+
)
30+
assert out == EXPECTED_PREFIX + "python main.py", provider_type
31+
32+
33+
def test_local_provider_is_not_prefixed():
34+
# Local already forces cwd == $HOME == the file-drop dir.
35+
out = prepend_remote_workdir_cd(
36+
"python main.py",
37+
task_id="task-123",
38+
file_mounts=True,
39+
provider_type=ProviderType.LOCAL.value,
40+
)
41+
assert out == "python main.py"
42+
43+
44+
def test_no_prefix_when_file_mounts_not_enabled():
45+
# No synced files (inlined run, or dict-form file_mounts handled natively):
46+
# copy_file_mounts isn't injected, so there's no workdir file and nothing to cd into.
47+
for file_mounts in (False, None, {"/remote": "/local"}):
48+
out = prepend_remote_workdir_cd(
49+
"python main.py",
50+
task_id="task-123",
51+
file_mounts=file_mounts,
52+
provider_type=ProviderType.LAMBDA.value,
53+
)
54+
assert out == "python main.py", file_mounts
55+
56+
57+
def test_no_prefix_without_task_id():
58+
out = prepend_remote_workdir_cd(
59+
"python main.py",
60+
task_id=None,
61+
file_mounts=True,
62+
provider_type=ProviderType.LAMBDA.value,
63+
)
64+
assert out == "python main.py"
65+
66+
67+
def test_preserves_shell_operators_in_command():
68+
# The whole command is later shlex.quoted as one payload to tfl-remote-trap, so
69+
# operators must be carried through verbatim after the cd prefix.
70+
cmd = "python main.py --epochs 3 && echo done"
71+
out = prepend_remote_workdir_cd(
72+
cmd,
73+
task_id="task-123",
74+
file_mounts=True,
75+
provider_type=ProviderType.RUNPOD.value,
76+
)
77+
assert out == EXPECTED_PREFIX + cmd

api/transformerlab/services/compute_provider/launch_credentials.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
# lab.init() not required; copy_file_mounts uses _TFL_JOB_ID, _TFL_EXPERIMENT_ID / TFL_EXPERIMENT_ID, and job_data
1010
COPY_FILE_MOUNTS_SETUP = 'python -c "from lab import lab; lab.copy_file_mounts()"'
1111

12+
# File that lab.copy_file_mounts() writes on the remote box, recording the absolute
13+
# directory where task files were synced. The remote run command cd's into it so a bare
14+
# `python main.py` resolves. Keep in sync with lab-sdk's copy_file_mounts().
15+
WORKDIR_FILE_PATH = "/tmp/.tfl_task_workdir"
16+
1217
# RunPod (and similar) use /workspace as a writable persistent path
1318
RUNPOD_AWS_CREDENTIALS_DIR = "/workspace/.aws"
1419

api/transformerlab/services/compute_provider/launch_template.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from transformerlab.services.compute_provider.launch_credentials import (
1717
COPY_FILE_MOUNTS_SETUP,
1818
RUNPOD_AWS_CREDENTIALS_DIR,
19+
WORKDIR_FILE_PATH,
1920
generate_aws_credentials_setup,
2021
generate_azure_credentials_setup,
2122
generate_gcp_credentials_setup,
@@ -55,6 +56,36 @@
5556
from werkzeug.utils import secure_filename
5657

5758

59+
def prepend_remote_workdir_cd(
60+
command: str,
61+
*,
62+
task_id: Optional[str],
63+
file_mounts: Any,
64+
provider_type: str,
65+
) -> str:
66+
"""Prefix a remote run command with a `cd` into the synced task-file directory.
67+
68+
When task files are synced to a remote box via lab.copy_file_mounts() (any non-Local
69+
provider with file_mounts enabled), the files land in the directory copy_file_mounts
70+
resolved at runtime (~/sky_workdir or $HOME) and recorded in the WORKDIR_FILE_PATH
71+
file. The remote run command, however, executes from the provider's default cwd —
72+
`/` for Lambda cloud-init, the image WORKDIR for RunPod — so a bare `python main.py`
73+
can't find the synced file (CPython reports it as `//main.py` when cwd is `/`). cd into
74+
the recorded workdir first so the user's run command sees its files, matching the Local
75+
provider's cwd == $HOME invariant.
76+
77+
We read the workdir file rather than hardcoding $HOME because the run shell's $HOME may
78+
not match expanduser("~") used during sync (e.g. Lambda cloud-init runs as root with
79+
HOME unset, so bash $HOME is empty while expanduser("~") resolves to /root). The `||
80+
echo "$HOME"` is a best-effort fallback for the case where the file is somehow absent.
81+
82+
Local is excluded because it already forces cwd == $HOME == the file-drop dir.
83+
"""
84+
if task_id and file_mounts is True and provider_type != ProviderType.LOCAL.value:
85+
return f'cd "$(cat {WORKDIR_FILE_PATH} 2>/dev/null || echo "$HOME")" && {command}'
86+
return command
87+
88+
5889
async def launch_template_on_provider(
5990
provider_id: str,
6091
request: ProviderTemplateLaunchRequest,
@@ -669,6 +700,15 @@ async def launch_template_on_provider(
669700
post_hook=str(post_setup_hook) if post_setup_hook is not None else None,
670701
)
671702

703+
# On remote providers, cd into the directory where lab.copy_file_mounts() synced the
704+
# task files so a bare `python main.py` resolves (see prepend_remote_workdir_cd).
705+
command_with_hooks = prepend_remote_workdir_cd(
706+
command_with_hooks,
707+
task_id=request.task_id,
708+
file_mounts=request.file_mounts,
709+
provider_type=provider.type,
710+
)
711+
672712
# Wrap the user command with tfl-remote-trap so we can track live_status in job_data.
673713
# This uses the tfl-remote-trap helper from the transformerlab SDK, which:
674714
# - sets job_data.live_status="started" when execution begins

lab-sdk/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "transformerlab"
7-
version = "0.1.42"
7+
version = "0.1.43"
88
description = "Python SDK for Transformer Lab"
99
readme = "README.md"
1010
requires-python = ">=3.10"

lab-sdk/src/lab/lab_facade.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,20 @@ async def _copy_file_mounts_async(self, job_id: str) -> None:
376376
else:
377377
dest_dir = home_dir
378378

379+
# Record the resolved destination so the remote run command can `cd` into the
380+
# exact directory where these task files land. The launch template prepends
381+
# `cd "$(cat /tmp/.tfl_task_workdir ...)"` to the run command on remote
382+
# providers. We write the absolute path here rather than letting the run shell
383+
# recompute it because the run shell's $HOME may not match expanduser("~") used
384+
# during sync — e.g. Lambda's cloud-init runs as root with HOME unset, so bash
385+
# `$HOME` is empty while expanduser("~") resolves to /root. (Keep this path in
386+
# sync with launch_template.py's cd prefix.)
387+
try:
388+
with open("/tmp/.tfl_task_workdir", "w") as workdir_file:
389+
workdir_file.write(dest_dir)
390+
except Exception:
391+
logger.debug("Failed to write task workdir file", exc_info=True)
392+
379393
# Determine if we're working with a remote URI (e.g., s3://)
380394
is_remote = storage.is_remote_path(task_dir)
381395
protocol_prefix = ""

lab-sdk/tests/test_lab_facade.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,11 @@ async def test_copy_file_mounts_without_lab_init(tmp_path, monkeypatch):
12911291
with open(dest) as f:
12921292
assert f.read() == "hi"
12931293

1294+
# The workdir file records the absolute dir where files landed, so the remote
1295+
# run command can `cd` into it (no ~/sky_workdir here, so dest is $HOME).
1296+
with open("/tmp/.tfl_task_workdir") as f:
1297+
assert f.read() == str(home)
1298+
12941299

12951300
def test_download_registry_model(tmp_path, monkeypatch):
12961301
_fresh(monkeypatch)

0 commit comments

Comments
 (0)