Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The `Function` class wraps user functions decorated with `@hog.function()` to en
- remote
- submit
- local
- batch_submit
- batch_local

## Method Class

Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/functions-and-harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def train_model(dataset: str, epochs: int) -> dict:
return {"accuracy": 0.95}
```

Functions provide four execution modes:
Functions provide several execution modes:

| Method | Where it runs | Behavior |
|--------|---------------|----------|
Expand Down Expand Up @@ -111,6 +111,6 @@ hog run script.py -- --epochs=20 # Runs main with epochs=20

## Next steps

- **[Parallel Execution](../examples/parallel-execution.md)** - Use `.submit()` to run functions concurrently
- **[Parallel Execution](../examples/parallel-execution.md)** - Using `.batch_*` methods to run functions concurrently
- **[Parameterized Harness Example](../examples/parameterized-harness.md)** - Complete example with CLI arguments
- **[Remote Execution Flow](remote-execution.md)** - Understand what happens when you call `.remote()`
2 changes: 1 addition & 1 deletion docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ These examples cover the basics of using Groundhog:

Examples showing how to handle typical workflows:

- **[Parallel Execution](parallel-execution.md)** - Using `.submit()` for concurrent remote execution
- **[Parallel Execution](parallel-execution.md)** - Using `.batch_submit()` or `.batch_local()` for concurrent execution
- **[Parameterized Harnesses](parameterized-harness.md)** - Harnesses that accept CLI arguments for runtime configuration
- **[Endpoint Configuration](configuration.md)** - How the configuration system merges settings from multiple sources (PEP 723, decorators, call-time overrides)
- **[PyTorch from Custom Sources](pytorch_custom_index.md)** - Configuring uv to install packages from cluster-specific indexes, local paths, or internal mirrors
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,5 @@ Using .local() - runs in subprocess with numpy installed:

## Next Steps

- **[Parallel Execution](parallel-execution.md)** - Run multiple functions concurrently with `.submit()`
- **[Parallel Execution](parallel-execution.md)** - Run multiple functions concurrently with `.batch_submit()` or `.batch_local()`
- **[Configuration](configuration.md)** - Configure multiple endpoints
113 changes: 95 additions & 18 deletions docs/examples/parallel-execution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Parallel Execution

This example demonstrates the difference between sequential execution with `.remote()` and parallel execution with `.submit()`.
This example demonstrates sequential execution with `.remote()`, parallel execution with `.submit()`, and batch execution with `.batch_submit()` and `.batch_local()`.

## When to Use Each Method

Expand All @@ -16,19 +16,29 @@ This example demonstrates the difference between sequential execution with `.rem

**Use `.submit()` when:**

- You have multiple independent tasks that can run concurrently
- You don't care for the console display
- You need access to the `GroundhogFuture` object

## Full Example
**Use `.batch_submit()` when:**

- You're submitting many tasks to the same remote endpoint
- You want to avoid Globus Compute rate limits (batching is one API call instead of N)
- All tasks use the same function with different arguments

**Use `.batch_local()` when:**

- You want to run many tasks in parallel locally
- You want immediate `GroundhogFuture`s instead of `.local()`'s blocking behavior

## Example: Remote vs Submit

```python title="parallel_execution.py"
# /// script
# requires-python = ">=3.12,<3.13"
# dependencies = []
#
# [tool.uv]
# exclude-newer = "2025-12-02T19:48:40Z"
# exclude-newer = "2026-03-06T00:00:00Z"
#
# [tool.hog.anvil]
# endpoint = "5aafb4c1-27b2-40d8-a038-a0277611868f"
Expand Down Expand Up @@ -66,6 +76,28 @@ def main():
results = [f.result() for f in futures] # (3)!
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s (approximately 2s)")


@hog.harness()
def batch():
"""Run with: hog run parallel_execution.py batch"""
# .batch_submit() registers the function once and sends all tasks in a
# single API request, avoiding the per-task rate limits of a .submit() loop.
print("Batch remote submission:")
futures = slow_square.batch_submit(
args=[(0,), (1,), (2,), (3,), (4,)],
)
results = [f.result() for f in futures]
print(f" Results: {results}") # [0, 1, 4, 9, 16]

# .batch_local() runs each task in its own subprocess in parallel.
print("Batch local execution:")
futures = slow_square.batch_local(
args=[(0,), (1,), (2,), (3,), (4,)],
executor_kwargs={"max_workers": 4},
)
results = [f.result() for f in futures]
print(f" Results: {results}") # [0, 1, 4, 9, 16]
```

1. `.remote()` blocks until the function completes. Each call waits for the previous one to finish. Total time: 3 tasks x 2 seconds = ~6 seconds.
Expand All @@ -74,45 +106,90 @@ def main():

3. Calling `.result()` on each future blocks until that task completes. Since all tasks run in parallel, total time is ~2 seconds.


## Example: Batching Locally / Remotely

A loop of `.submit()` calls makes one API request per task and can hit Globus Compute rate limits at large N. `.batch_submit()` registers the function once and sends all tasks in a single request.

```python
# Instead of this (N separate API calls):
futures = [slow_square.submit(i) for i in range(5)]

# Use batch_submit (one API call):
futures = slow_square.batch_submit(
args=[(0,), (1,), (2,), (3,), (4,)], # (1)!
)
results = [f.result() for f in futures]
# [0, 1, 4, 9, 16]
```

1. Each tuple is unpacked as positional arguments for one task. Pass `kwargs=[...]` alongside `args` to mix positional and keyword arguments — when the two lists have different lengths, the shorter one fills with `()` or `{}`.

`.batch_local()` runs each task in its own subprocess with an isolated temporary directory:

```python
futures = slow_square.batch_local(
args=[(0,), (1,), (2,), (3,), (4,)],
executor_kwargs={"max_workers": 4}, # (1)!
)
results = [f.result() for f in futures]
# [0, 1, 4, 9, 16]
```

1. `executor_kwargs` is forwarded directly to `ThreadPoolExecutor`. Omit it to use the default worker count.

## Working with GroundhogFutures

`.submit()` and both batch methods return `GroundhogFuture` objects. They behave like standard `concurrent.futures.Future` objects, with additional Groundhog-specific properties.

```python
future = slow_square.submit(5)

# Get the deserialized return value (blocks until ready)
result = future.result()
result = future.result(timeout=10) # Raises TimeoutError if not ready

# Check if done (non-blocking)
if future.done():
print("Task completed!")

# Get the result (blocks until ready)
result = future.result()

# Get result with timeout
result = future.result(timeout=10) # Raises TimeoutError if not ready

# Cancel a pending task
future.cancel()

# Inspect the underlying ShellResult
# Inspect raw shell execution metadata
print(future.shell_result.returncode)
print(future.shell_result.stderr)

# Capture stdout from print() calls inside the remote function
if future.user_stdout:
print(future.user_stdout)

# Inspect the resolved configuration that was actually passed to the endpoint
print(future.user_endpoint_config) # {"account": "...", "partition": "..."}
print(future.task_id) # Globus Compute task ID
print(future.function_name) # "slow_square"
```

## Running the Example

```bash
# sequential vs batch timing comparison (local methods)
hog run examples/parallel_execution.py

# .remote vs .submit vs .batch_submit
hog run examples/parallel_execution.py remote
```

Expected output:
Expected output from `main`:

```
Sequential execution with .remote():
Results: [0, 1, 4]
Time: 6.2s (approximately 6s)
Sequential execution with .local():
Results: [0, 1, 4, 9, 16]
Time: 11.1s

Parallel execution with .submit():
Results: [0, 1, 4]
Time: 2.1s (approximately 2s)
Parallel execution with .batch_local():
Results: [0, 1, 4, 9, 16]
Time: 2.2s
```

## Next Steps
Expand Down
9 changes: 5 additions & 4 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ The comment block at the top uses [PEP 723](https://peps.python.org/pep-0723/) i

- **`requires-python`**: Python version requirement for remote execution
- **`dependencies`**: Python packages needed by your function (managed by uv)
- **`[tool.uv]`**: Optional configuration read by `uv run` when creating the ephemeral remote environment (see also: [full uv settings reference](https://docs.astral.sh/uv/reference/settings/))
- **`[tool.hog.my-endpoint]`**: Endpoint configuration with HPC-specific settings like account, partition, walltime, etc.
- **`[tool.uv]`**: Optional configuration read by `uv venv` and `uv pip install` when creating the remote environment (see also: [full uv settings reference](https://docs.astral.sh/uv/reference/settings/))
- **`[tool.hog.my-endpoint]`**: Endpoint configuration with HPC-specific settings like account, partition, walltime, etc. Recognized configuration options depend on the particular endpoint.


### Functions and harnesses

- **`@hog.function()`**: Decorates a Python function to make it executable remotely
- **`@hog.harness()`**: Decorates an orchestrator function that coordinates remote calls. Harnesses can accept parameters passed as CLI arguments (see [Functions and Harnesses](../concepts/functions-and-harnesses.md))
- **`.remote()`**: Executes the function remotely and blocks until complete (alternatively, use **`.submit()`** for async execution)
- **`.remote()`**: Executes the function remotely and blocks until complete (alternatively, use **`.submit()`** for async execution or **`batch_submit`** for many submissions)

## Add dependencies

Expand Down Expand Up @@ -100,7 +101,7 @@ def compute_mean(data: list[float]) -> float:
```

!!! tip "Updating Python version"
You can also use `hog add` to update the Python version requirement:
You can also use `hog add` to update the Python version requirement, not just add dependencies:

```bash
hog add hello.py --python 3.11
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ hog run analysis.py
## What Makes Groundhog Different?

**Environment and code stay coupled**
: Change your Python version or dependencies by editing the PEP 723 block in your script. The remote environment rebuilds automatically on the next run.
: Change your Python version or dependencies by editing the PEP 723 block in your script. The remote environment rebuilds automatically (if necessary) on the next run.

**Globus Compute under the hood**
: Built on [Globus Compute](https://www.globus.org/compute) for robust, secure HPC job submission.
Expand Down
52 changes: 41 additions & 11 deletions examples/parallel_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
# dependencies = []
#
# [tool.uv]
# exclude-newer = "2025-12-02T19:48:40Z"
# exclude-newer = "2026-03-06T00:00:00Z"
#
# [tool.hog.anvil]
# endpoint = "5aafb4c1-27b2-40d8-a038-a0277611868f"
# account = "cis250461"
# requirements = ""
# ///
"""
Example showing parallel execution with .submit() vs sequential with .remote().
Example showing parallel and batch execution patterns.

Use .remote() when you want to wait for each result before continuing.
Use .submit() when you want to run multiple tasks in parallel.
Use .batch_submit() to submit many tasks without hitting rate limits.
Use .batch_local() for parallel local execution on the login node.
"""

import groundhog_hpc as hog
Expand All @@ -30,21 +31,50 @@ def slow_square(n: int) -> int:


@hog.harness()
def main():
"""Run with: hog run parallel_execution.py"""
def main(n: int = 5):
"""Run like: hog run parallel_execution.py -- --n=5"""
import time

# Sequential: each .remote() blocks until complete
print("Sequential execution with .local():")
start = time.time()
results = [slow_square.local(i) for i in range(n)]
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s \n")

print("Parallel execution with .batch_local():")
start = time.time()
futures = slow_square.batch_local(args=[(i,) for i in range(n)])
results = [f.result() for f in futures]
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s ")


@hog.harness()
def remote(n: int = 5):
"""Run like: hog run parallel_execution.py remote -- --n=5"""
import time

args_list = [(i,) for i in range(n)]
# Sequential: each .remote() blocks until complete
print("Sequential execution with .remote():")
start = time.time()
results = [slow_square.remote(i) for i in range(3)]
results = [slow_square.remote(*args) for args in args_list]
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s (approximately 6s)\n")
print(f" Time: {time.time() - start:.1f}s \n")

# Parallel: .submit() returns immediately, tasks run concurrently
# Parallel: .submit() returns immediately, tasks run ~concurrently (N globus api calls)
print("Parallel execution with .submit():")
start = time.time()
futures = [slow_square.submit(i) for i in range(3)]
results = [f.result() for f in futures] # Wait for all results
futures = [slow_square.submit(*args) for args in args_list]
results = [f.result() for f in futures]
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s ")

# Parallel: .batch_submit() returns immediately, tasks run concurrently (1 globus api call)
print("Parallel execution with .batch_submit():")
start = time.time()
futures = slow_square.batch_submit(args=args_list)
results = [f.result() for f in futures]
print(f" Results: {results}")
print(f" Time: {time.time() - start:.1f}s (approximately 2s)")
print(f" Time: {time.time() - start:.1f}s ")
Loading
Loading