Skip to content

Commit dbdf016

Browse files
committed
Zipping files for faster transfer
1 parent 2d526be commit dbdf016

9 files changed

Lines changed: 472 additions & 233 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## v0.5.4
4+
5+
### New features
6+
7+
- **Input bundling for SimCenter apps** (`ds.jobs.prepare_inputs`): the Tapis transfers service pays a fixed per-file scheduling cost (measured ~40s/file under tenant load), so staging many small input files dominates job wall time. For `simcenter-*` apps, `prepare_inputs` now zips the prepared `tmp.SimCenter/` tree into a single `tmpSimCenter.zip` — which the app wrapper unpacks natively on the execution system — inside a staging directory (`<input_dir>_staged`). Staging transfers ~2 objects instead of one per file.
8+
- Default **on** for SimCenter apps (matching what quoFEM desktop submits); pass `bundle=False` to stage the loose tree as before. Apps without a profile are unaffected.
9+
- Every `prepare_inputs` summary now includes `staged_dir` — the directory to pass to `ds.files.to_uri()`; the original input directory is never modified beyond the workflow-JSON patch.
10+
311
## v0.5.3
412

513
### New features

‎dapi/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
TAPIS_TERMINAL_STATES,
5353
)
5454

55-
__version__ = "0.5.3"
55+
__version__ = "0.5.4"
5656

5757
__all__ = [
5858
"DSClient",

‎dapi/client.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -713,19 +713,24 @@ def prepare_inputs(
713713
Dispatches to the app's profile when one is registered; a no-op
714714
beyond existence checking for apps that need no preparation. For
715715
SimCenter apps (``simcenter-*``) this rewrites the workflow JSON's
716-
backend paths and reports the UQ engine, random variables, and EDPs.
716+
backend paths, reports the UQ engine, random variables, and EDPs,
717+
and by default bundles the inputs into a single ``tmpSimCenter.zip``
718+
(unpacked natively by the app wrapper) so staging transfers one
719+
file instead of many. Stage the returned ``staged_dir``.
717720
718721
Args:
719722
app_id (str): The Tapis app id the inputs are being prepared for.
720723
input_dir (str): Local path to the job input directory.
721724
**options: Profile-specific options (e.g. ``backend_dir``,
722-
``input_filename`` for SimCenter apps).
725+
``input_filename``, ``bundle=False`` for SimCenter apps).
723726
724727
Returns:
725-
Dict[str, Any]: Summary of the preparation performed.
728+
Dict[str, Any]: Summary of the preparation performed. Always
729+
includes ``staged_dir`` — the directory to stage.
726730
727731
Example:
728-
>>> ds.jobs.prepare_inputs("simcenter-uq-stampede3", "./DS_input")
732+
>>> info = ds.jobs.prepare_inputs("simcenter-uq-stampede3", "./DS_input")
733+
>>> input_uri = ds.files.to_uri(info["staged_dir"])
729734
"""
730735
return jobs_module.prepare_job_inputs(app_id, input_dir, **options)
731736

‎dapi/jobs.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,16 +535,19 @@ def prepare_job_inputs(app_id: str, input_dir: str, **options: Any) -> Dict[str,
535535
536536
For SimCenter apps (``simcenter-*``) this rewrites the workflow JSON's
537537
``remoteAppDir``/``remoteAppWorkingDir`` to the backend installation on
538-
the execution system and returns a summary of the UQ workflow.
538+
the execution system, and by default bundles the inputs into a single
539+
``tmpSimCenter.zip`` (unpacked natively by the app wrapper) so staging
540+
transfers one file instead of many. Stage the returned ``staged_dir``.
539541
540542
Args:
541543
app_id (str): The Tapis app id the inputs are being prepared for.
542544
input_dir (str): Local path to the job input directory.
543545
**options: Profile-specific options (e.g. ``backend_dir``,
544-
``input_filename`` for SimCenter apps).
546+
``input_filename``, ``bundle=False`` for SimCenter apps).
545547
546548
Returns:
547-
Dict[str, Any]: Summary of the preparation performed.
549+
Dict[str, Any]: Summary of the preparation performed. Always
550+
includes ``staged_dir`` — the directory callers should stage.
548551
549552
Raises:
550553
ValueError: If input_dir does not exist, or a profile rejects the
@@ -559,7 +562,12 @@ def prepare_job_inputs(app_id: str, input_dir: str, **options: Any) -> Dict[str,
559562
profile = profiles.find(app_id)
560563
if profile is None:
561564
print(f"App '{app_id}' requires no input preparation.")
562-
return {"app_id": app_id, "input_dir": input_dir, "prepared": False}
565+
return {
566+
"app_id": app_id,
567+
"input_dir": input_dir,
568+
"prepared": False,
569+
"staged_dir": input_dir,
570+
}
563571
print(f"Preparing inputs with '{profile.name}' app profile.")
564572
return profile.prepare_inputs(input_dir, app_id=app_id, **options)
565573

‎dapi/profiles.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,15 @@ def prepare_inputs(
5353
**options: Profile-specific options.
5454
5555
Returns:
56-
Dict[str, Any]: Summary of what was prepared.
56+
Dict[str, Any]: Summary of what was prepared. Always includes
57+
``staged_dir`` — the directory callers should stage.
5758
"""
58-
return {"app_id": app_id, "input_dir": input_dir, "prepared": False}
59+
return {
60+
"app_id": app_id,
61+
"input_dir": input_dir,
62+
"prepared": False,
63+
"staged_dir": input_dir,
64+
}
5965

6066
@classmethod
6167
def finalize_job_request(cls, job_request: Dict[str, Any]) -> Dict[str, Any]:

‎dapi/simcenter.py‎

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io
2727
import json
2828
import os
29+
import shutil
2930
import zipfile
3031
from dataclasses import dataclass, field
3132
from typing import Any, Dict, List, Optional, Union
@@ -40,6 +41,10 @@
4041
# The env variable name the wrapper script expects for the staged input dir.
4142
INPUT_DIR_ENV_KEY = "inputDirectory"
4243

44+
# Archive name the SimCenter wrapper script unpacks natively on the exec
45+
# system ("unzip tmpSimCenter.zip" is its first action in the input dir).
46+
BUNDLE_NAME = "tmpSimCenter.zip"
47+
4348
# SimCenter backend installations per app id. These paths change when
4449
# SimCenter redeploys the backend; update alongside a dapi release.
4550
DEFAULT_BACKEND_DIRS: Dict[str, str] = {
@@ -54,38 +59,55 @@ def prepare_inputs(
5459
input_filename: str = "scInput.json",
5560
backend_dir: Optional[str] = None,
5661
app_id: str = "simcenter-uq-stampede3",
62+
bundle: bool = True,
63+
staged_dir: Optional[str] = None,
5764
) -> Dict[str, Any]:
58-
"""Point a SimCenter workflow JSON at the backend on the execution system.
65+
"""Point a SimCenter workflow JSON at the backend and bundle the inputs.
5966
6067
The SimCenter desktop applications write local paths into the workflow
6168
JSON (``remoteAppDir``/``remoteAppWorkingDir``). Before submission these
6269
must be rewritten to the backend installation on the execution system,
6370
which the app wrapper reads back with ``jq`` to locate the UQ engine.
6471
72+
By default the prepared ``tmp.SimCenter/`` tree is then zipped into a
73+
single ``tmpSimCenter.zip`` inside a staging directory. The SimCenter
74+
wrapper unpacks this archive natively on the execution system, and
75+
staging one file instead of many avoids the Tapis transfers service's
76+
per-file overhead (which can add ~40s per file under tenant load).
77+
Stage the returned ``staged_dir``, not ``input_dir``, when bundling.
78+
6579
Args:
6680
input_dir (str): Local path to the job input directory (the folder
67-
containing ``tmp.SimCenter/templatedir/``).
81+
containing ``tmp.SimCenter/templatedir/``). Never modified
82+
beyond the workflow JSON patch.
6883
input_filename (str, optional): Workflow JSON filename inside
6984
``tmp.SimCenter/templatedir/``. Defaults to "scInput.json".
7085
backend_dir (str, optional): SimCenter backend installation path on
7186
the execution system. If None, looked up from
7287
``DEFAULT_BACKEND_DIRS`` by app_id.
7388
app_id (str, optional): Tapis app id used for the backend lookup.
7489
Defaults to "simcenter-uq-stampede3".
90+
bundle (bool, optional): Zip ``tmp.SimCenter/`` into
91+
``tmpSimCenter.zip`` in a staging directory. Defaults to True.
92+
Pass False to stage the loose directory tree as before.
93+
staged_dir (str, optional): Where to place the bundle. Defaults to
94+
``<input_dir>_staged`` next to the input directory.
7595
7696
Returns:
7797
Dict[str, Any]: Summary of the prepared workflow with keys
7898
``workflow_json`` (path), ``backend_dir``, ``uq_engine``,
79-
``uq_type``, ``random_variables``, and ``edps``.
99+
``uq_type``, ``random_variables``, ``edps``, and ``staged_dir``
100+
(the directory to stage: the bundle directory when bundling,
101+
otherwise input_dir). When bundling, also ``bundle`` (zip path)
102+
and ``bundled_files`` (count).
80103
81104
Raises:
82105
ValueError: If no backend directory is known for app_id and none given.
83106
FileNotFoundError: If the workflow JSON cannot be found.
84107
85108
Example:
86109
>>> info = ds.jobs.prepare_inputs("simcenter-uq-stampede3", "./DS_input")
87-
>>> info["random_variables"]
88-
['Dr', 'G0', 'hpo']
110+
>>> input_uri = ds.files.to_uri(info["staged_dir"])
89111
"""
90112
if backend_dir is None:
91113
backend_dir = DEFAULT_BACKEND_DIRS.get(app_id)
@@ -123,14 +145,54 @@ def prepare_inputs(
123145
"uq_type": uq.get("uqType"),
124146
"random_variables": [rv["name"] for rv in workflow.get("randomVariables", [])],
125147
"edps": [edp["name"] for edp in workflow.get("EDP", [])],
148+
"staged_dir": input_dir,
126149
}
127150
print(f"Updated remoteAppDir in {workflow_path}")
128151
print(f"UQ engine: {summary['uq_engine']} ({summary['uq_type']})")
129152
print(f"Random variables: {summary['random_variables']}")
130153
print(f"EDPs: {summary['edps']}")
154+
155+
if bundle:
156+
summary.update(_bundle_inputs(input_dir, staged_dir))
131157
return summary
132158

133159

160+
def _bundle_inputs(input_dir: str, staged_dir: Optional[str]) -> Dict[str, Any]:
161+
"""Zip ``tmp.SimCenter/`` into ``tmpSimCenter.zip`` in a staging dir.
162+
163+
The archive preserves the ``tmp.SimCenter/...`` layout the wrapper
164+
expects when it runs ``unzip tmpSimCenter.zip``. Any other top-level
165+
entries of input_dir are copied into the staging dir unchanged.
166+
"""
167+
staged = staged_dir or input_dir.rstrip("/") + "_staged"
168+
os.makedirs(staged, exist_ok=True)
169+
170+
bundle_src = os.path.join(input_dir, "tmp.SimCenter")
171+
zip_path = os.path.join(staged, BUNDLE_NAME)
172+
bundled_files = 0
173+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
174+
for root, _dirs, files in os.walk(bundle_src):
175+
for name in files:
176+
full_path = os.path.join(root, name)
177+
zf.write(full_path, os.path.relpath(full_path, input_dir))
178+
bundled_files += 1
179+
180+
# Preserve any sibling entries of tmp.SimCenter/ the app may expect.
181+
for entry in os.listdir(input_dir):
182+
if entry == "tmp.SimCenter":
183+
continue
184+
src = os.path.join(input_dir, entry)
185+
dst = os.path.join(staged, entry)
186+
if os.path.isfile(src):
187+
shutil.copy2(src, dst)
188+
elif os.path.isdir(src) and os.path.abspath(src) != os.path.abspath(staged):
189+
shutil.copytree(src, dst, dirs_exist_ok=True)
190+
191+
print(f"Bundled {bundled_files} files into {zip_path}")
192+
print(f"Stage this directory: {staged}")
193+
return {"staged_dir": staged, "bundle": zip_path, "bundled_files": bundled_files}
194+
195+
134196
def finalize_job_request(job_request: Dict[str, Any]) -> Dict[str, Any]:
135197
"""Apply the SimCenter wrapper contract to a generated job request.
136198

0 commit comments

Comments
 (0)