Skip to content
Open
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
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ from runpod import App, Model, NetworkVolume, Secret

app = App("inference")

models = NetworkVolume("models", size=100)
checkpoints = NetworkVolume("checkpoints", size=100)
llama = Model("meta-llama/Llama-3.1-8B-Instruct")


Expand All @@ -44,35 +44,56 @@ llama = Model("meta-llama/Llama-3.1-8B-Instruct")
gpu="H100",
workers=(0, 3),
dependencies=["vllm"],
mounts={"/runpod-volume": models},
model=llama,
env={"HF_TOKEN": Secret("hf-token")},
)
def chat(prompt: str):
import vllm

llm = vllm.LLM(model=str(llama.path)) # weights already on disk
return llm.generate(prompt)
result = llm.generate([prompt])[0]
return result.outputs[0].text # results must be JSON values


# one ephemeral pod per call: provisions, runs to completion, terminates
@app.task(gpu="H100", gpu_count=2, mounts={"/models": models})
@app.task(gpu="H100", mounts={"/checkpoints": checkpoints})
def finetune(steps: int = 1000):
...
return {"loss": final_loss}
import torch

model = torch.nn.Linear(1, 1).cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
x = torch.randn(1024, 1, device="cuda")
y = 3 * x + 1
for _ in range(steps):
loss = ((model(x) - y) ** 2).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
torch.save(model.state_dict(), checkpoints.path / "model.pt")
return {"loss": loss.item()}


@runpod.local_entrypoint
def main():
print(chat.remote("why is the sky blue?")) # blocks for the result
job = finetune.spawn(steps=500) # fire and forget -> Job
print(finetune.remote(steps=500)) # pod terminates on return
```

```bash
rp secret add hf-token # store your Hugging Face token once
rp flash dev main.py # live dev session: edit, re-run, logs stream back
rp flash deploy # deploy production endpoints
```

To start a task without blocking, use an async entrypoint:
`job = await finetune.spawn.aio(steps=500)`, then `await job.wait()`.
The task's pod keeps running until you wait on the job or call
`await job.cancel()`, even after your script exits.

The `checkpoints` volume is kept between runs. When you no longer need it,
find its id with `rp volume list` and delete it with
`rp volume delete VOLUME_ID`. More examples are in [`examples/`](examples).

## Contributing

Pull requests and issues are welcome — see the [contributing guide](CONTRIBUTING.md) to get started.
Expand Down
97 changes: 97 additions & 0 deletions tests/test_readme_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""The README's Example section must stay valid code the SDK accepts.

The sample is the first thing new users copy, so these tests check that it
references only names it defines, that app discovery accepts it, and that a
synchronous entrypoint never spawns a task it cannot wait on.
"""

import ast
import builtins
import re
import symtable
from pathlib import Path

from runpod.apps.discovery import discover_apps

README = Path(__file__).resolve().parent.parent / "README.md"
EXAMPLE_HEADING = "## Example"
PYTHON_BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL)


def _example_source() -> str:
text = README.read_text(encoding="utf-8")
_, _, section = text.partition(EXAMPLE_HEADING)
match = PYTHON_BLOCK.search(section)
assert match, "README Example section has no python code block"
return match.group(1)


def _module_bindings(table: symtable.SymbolTable) -> set:
return {
sym.get_name()
for sym in table.get_symbols()
if sym.is_assigned() or sym.is_imported()
}


def _undefined_globals(source: str) -> set:
top = symtable.symtable(source, "README example", "exec")
known = _module_bindings(top) | set(dir(builtins))
undefined = set()

def visit(table: symtable.SymbolTable) -> None:
for sym in table.get_symbols():
if sym.is_referenced() and sym.is_global() and sym.get_name() not in known:
undefined.add(sym.get_name())
for child in table.get_children():
visit(child)

visit(top)
return undefined


def _decorator_attr(node: ast.expr) -> str:
target = node.func if isinstance(node, ast.Call) else node
return target.attr if isinstance(target, ast.Attribute) else ""


def test_example_references_only_defined_names():
assert _undefined_globals(_example_source()) == set()


def test_example_is_accepted_by_app_discovery(tmp_path):
sample = tmp_path / "main.py"
sample.write_text(_example_source(), encoding="utf-8")

apps = discover_apps(sample)

assert len(apps) == 1


def test_sync_entrypoint_does_not_spawn_tasks():
"""A spawned task keeps its pod until awaited, which sync code cannot do."""
tree = ast.parse(_example_source())
functions = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
tasks = {
fn.name
for fn in functions
if any(_decorator_attr(d) == "task" for d in fn.decorator_list)
}
entrypoints = [
fn
for fn in functions
if any(_decorator_attr(d) == "local_entrypoint" for d in fn.decorator_list)
]

spawned = {
call.func.value.id
for fn in entrypoints
for call in ast.walk(fn)
if isinstance(call, ast.Call)
and isinstance(call.func, ast.Attribute)
and call.func.attr == "spawn"
and isinstance(call.func.value, ast.Name)
}

assert tasks, "expected the example to define at least one task"
assert spawned & tasks == set()
Loading