From 52cd12c0683bae6270c9b8d5dc521b3975708706 Mon Sep 17 00:00:00 2001 From: Romeo Kienzler Date: Mon, 13 Apr 2026 13:35:49 +0200 Subject: [PATCH 1/2] improve gpu test code Signed-off-by: Romeo Kienzler --- .../components/util/gpu_performance_test.py | 137 +++++++++++------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/src/claimed/components/util/gpu_performance_test.py b/src/claimed/components/util/gpu_performance_test.py index d7e40adc..2af0a811 100644 --- a/src/claimed/components/util/gpu_performance_test.py +++ b/src/claimed/components/util/gpu_performance_test.py @@ -205,83 +205,108 @@ def benchmark_gpu(matrix_size, iterations, device): # Main # ===================== -def main(): - parser = argparse.ArgumentParser() - - parser.add_argument("--mode", choices=["cpu", "single_gpu", "ddp"], required=True) - parser.add_argument("--batch_size", type=int, default=256) - parser.add_argument("--num_workers", type=int, default=4) - parser.add_argument("--dataset_size", type=int, default=100000) - parser.add_argument("--steps", type=int, default=100) - parser.add_argument("--input_dim", type=int, default=1024) - parser.add_argument("--hidden_dim", type=int, default=2048) - parser.add_argument("--num_classes", type=int, default=10) - parser.add_argument("--depth", type=int, default=3) - parser.add_argument("--materialize_dir", type=str, default=None) - parser.add_argument("--cleanup", action="store_true") - parser.add_argument("--matrix_size", type=int, default=2048) - parser.add_argument("--iterations", type=int, default=50) - - args = parser.parse_args() - - if args.mode == "cpu": - print("CPU GFLOPS:", benchmark_cpu(args.matrix_size, args.iterations)) +def run( + mode: str = 'single_gpu', + batch_size: int = 256, + num_workers: int = 4, + dataset_size: int = 100000, + steps: int = 100, + input_dim: int = 1024, + hidden_dim: int = 2048, + num_classes: int = 10, + depth: int = 3, + materialize_dir: str = None, + cleanup: bool = False, + matrix_size: int = 2048, + iterations: int = 50, +) -> None: + """ + Run the PyTorch HPC benchmark. + + mode: benchmark mode: cpu | single_gpu | ddp + batch_size: dataloader batch size + num_workers: dataloader worker processes + dataset_size: total number of synthetic samples + steps: number of batches per benchmark phase + input_dim: input feature dimension of the MLP + hidden_dim: hidden layer width of the MLP + num_classes: number of output classes + depth: number of hidden layers + materialize_dir: directory to cache synthetic dataset on disk (None = lazy) + cleanup: remove materialize_dir after the benchmark + matrix_size: square matrix edge length for compute benchmarks + iterations: number of matrix-multiply iterations for compute benchmarks + """ + if mode == 'cpu': + print('CPU GFLOPS:', benchmark_cpu(matrix_size, iterations)) return - if args.mode == "single_gpu": - device = torch.device("cuda:0") - elif args.mode == "ddp": + if mode == 'single_gpu': + device = torch.device('cuda:0') + elif mode == 'ddp': local_rank = setup_ddp() - device = torch.device(f"cuda:{local_rank}") + device = torch.device(f'cuda:{local_rank}') + else: + raise ValueError(f"Unknown mode '{mode}'. Choose from: cpu | single_gpu | ddp") dataset = SyntheticDataset( - args.dataset_size, - args.input_dim, - args.num_classes, - materialize_dir=args.materialize_dir + dataset_size, + input_dim, + num_classes, + materialize_dir=materialize_dir, ) loader = DataLoader( dataset, - batch_size=args.batch_size, - num_workers=args.num_workers, + batch_size=batch_size, + num_workers=num_workers, pin_memory=True, - shuffle=True + shuffle=True, ) - model = SimpleMLP( - args.input_dim, - args.hidden_dim, - args.num_classes, - args.depth - ).to(device) + model = SimpleMLP(input_dim, hidden_dim, num_classes, depth).to(device) - if args.mode == "ddp": + if mode == 'ddp': model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[device.index]) - print("\n--- DataLoader throughput ---") - dl_tp = benchmark_dataloader(loader, device, args.steps) - print(f"Samples/sec: {dl_tp:.2f}") + print('\n--- DataLoader throughput ---') + print(f'Samples/sec: {benchmark_dataloader(loader, device, steps):.2f}') - print("\n--- Training throughput ---") - train_tp = benchmark_training(model, loader, device, args.steps) - print(f"Samples/sec: {train_tp:.2f}") + print('\n--- Training throughput ---') + print(f'Samples/sec: {benchmark_training(model, loader, device, steps):.2f}') - print("\n--- Inference throughput ---") - infer_tp = benchmark_inference(model, loader, device, args.steps) - print(f"Samples/sec: {infer_tp:.2f}") + print('\n--- Inference throughput ---') + print(f'Samples/sec: {benchmark_inference(model, loader, device, steps):.2f}') - print("\n--- GPU compute ---") - gpu_gflops = benchmark_gpu(args.matrix_size, args.iterations, device) - print(f"GFLOPS: {gpu_gflops:.2f}") + print('\n--- GPU compute ---') + print(f'GFLOPS: {benchmark_gpu(matrix_size, iterations, device):.2f}') - if args.cleanup and args.materialize_dir: - shutil.rmtree(args.materialize_dir, ignore_errors=True) - print("Materialized dataset removed.") + if cleanup and materialize_dir: + shutil.rmtree(materialize_dir, ignore_errors=True) + print('Materialized dataset removed.') - if args.mode == "ddp": + if mode == 'ddp': cleanup_ddp() -if __name__ == "__main__": +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--mode', choices=['cpu', 'single_gpu', 'ddp'], required=True) + parser.add_argument('--batch_size', type=int, default=256) + parser.add_argument('--num_workers', type=int, default=4) + parser.add_argument('--dataset_size', type=int, default=100000) + parser.add_argument('--steps', type=int, default=100) + parser.add_argument('--input_dim', type=int, default=1024) + parser.add_argument('--hidden_dim', type=int, default=2048) + parser.add_argument('--num_classes', type=int, default=10) + parser.add_argument('--depth', type=int, default=3) + parser.add_argument('--materialize_dir', type=str, default=None) + parser.add_argument('--cleanup', action='store_true') + parser.add_argument('--matrix_size', type=int, default=2048) + parser.add_argument('--iterations', type=int, default=50) + args = parser.parse_args() + run(**vars(args)) + + +if __name__ == '__main__': main() From 330c4940182cf9bced881b8fb69640f7dfc1e168 Mon Sep 17 00:00:00 2001 From: Romeo Kienzler Date: Mon, 13 Apr 2026 13:51:45 +0200 Subject: [PATCH 2/2] add docs and build system Signed-off-by: Romeo Kienzler --- .github/workflows/docs.yml | 45 +++++++++ .gitignore | 1 + docs/assets/logo.png | Bin 0 -> 49629 bytes docs/c3/create-gridwrapper.md | 50 ++++++++++ docs/c3/create-operator.md | 38 ++++++++ docs/c3/index.md | 61 ++++++++++++ docs/c3/operator-utils.md | 39 ++++++++ docs/cli.md | 135 ++++++++++++++++++++++++++ docs/components/util/cosutils.md | 62 ++++++++++++ docs/components/util/gpu-benchmark.md | 67 +++++++++++++ docs/contributing.md | 58 +++++++++++ docs/getting-started.md | 125 ++++++++++++++++++++++++ docs/index.md | 60 ++++++++++++ docs/mlx/cos-backend.md | 7 ++ docs/mlx/index.md | 53 ++++++++++ docs/mlx/s3-kv-store.md | 7 ++ mkdocs.yml | 80 +++++++++++++++ 17 files changed, 888 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/assets/logo.png create mode 100644 docs/c3/create-gridwrapper.md create mode 100644 docs/c3/create-operator.md create mode 100644 docs/c3/index.md create mode 100644 docs/c3/operator-utils.md create mode 100644 docs/cli.md create mode 100644 docs/components/util/cosutils.md create mode 100644 docs/components/util/gpu-benchmark.md create mode 100644 docs/contributing.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/mlx/cos-backend.md create mode 100644 docs/mlx/index.md create mode 100644 docs/mlx/s3-kv-store.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..f3012aa6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +name: Publish Docs to GitHub Pages + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'src/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history needed for git-committers plugin + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + + - name: Install doc dependencies + run: | + pip install \ + mkdocs-material \ + mkdocstrings[python] \ + mkdocs-git-revision-date-localized-plugin \ + griffe + + - name: Install claimed package (for mkdocstrings introspection) + run: pip install -e . + + - name: Build & deploy docs + run: mkdocs gh-deploy --force --clean --verbose diff --git a/.gitignore b/.gitignore index 41f4e616..da33be62 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist *.bak *.swp .DS_Store +site/ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2fb11298f57552fdd4259be783f879a8ef2748 GIT binary patch literal 49629 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 RHKjPs=ibuKN+hA*{RQqc-IV|U literal 0 HcmV?d00001 diff --git a/docs/c3/create-gridwrapper.md b/docs/c3/create-gridwrapper.md new file mode 100644 index 00000000..da9d49d4 --- /dev/null +++ b/docs/c3/create-gridwrapper.md @@ -0,0 +1,50 @@ +# create_gridwrapper + +Wraps an existing component to run in parallel over a collection of inputs +using one of several storage backends. + +## CLI + +```bash +c3_create_gridwrapper [options] +``` + +| Option | Type | Default | Description | +|---|---|---|---| +| `source_file` | path | *required* | `.ipynb` or `.py` component to wrap | +| `--backend` | str | `local` | Storage backend (see table below) | +| `--component-inputs` | str | `''` | Comma-separated parameter names that vary per grid cell | +| `--component-dependencies` | str | `''` | Pip dependencies to inject | +| `--repository` | str | | Container registry namespace | +| `--log-level` | str | `WARNING` | Python logging level | + +## Backends + +| Key | Description | +|---|---| +| `local` | Local filesystem, simple parallelism | +| `cos` | IBM COS – iterate over objects in a bucket prefix | +| `s3kv` | MLX S3 key-value store backend | +| `simple_grid_wrapper` | Source-only, minimal overhead | +| `folder_grid_wrapper` | Separate source and target folder | +| `legacy_cos_grid_wrapper` | Older COS format | + +## Python API + +::: claimed.c3.create_gridwrapper + options: + members: + - wrap_component + - create_gridwrapper + +## Example + +```bash +# Wrap a training script to process every CSV in a COS bucket in parallel +c3_create_gridwrapper train_model.py \ + --backend cos \ + --component-inputs input_file \ + --repository docker.io/myuser +``` + +This emits `gw_train_model.py` which, when containerised, launches one worker per input file. diff --git a/docs/c3/create-operator.md b/docs/c3/create-operator.md new file mode 100644 index 00000000..06b20f75 --- /dev/null +++ b/docs/c3/create-operator.md @@ -0,0 +1,38 @@ +# create_operator + +Builds a Docker image and generates KFP, CWL, and Kubernetes descriptors +from a Jupyter notebook, Python script, or R script. + +## CLI + +```bash +c3_create_operator [options] +``` + +| Option | Type | Default | Description | +|---|---|---|---| +| `source_file` | path | *required* | `.ipynb`, `.py`, or `.R` file | +| `--repository` | str | *required* | Container registry namespace, e.g. `docker.io/myuser` | +| `--version` | str | auto | Image tag; auto-detected from `image_version` variable in source | +| `--additional-files` | list | `[]` | Extra files to `ADD` into the image | +| `--dockerfile` | path | auto | Custom Dockerfile template | +| `--log-level` | str | `WARNING` | Python logging level | + +## Python API + +::: claimed.c3.create_operator + options: + members: + - create_operator + - create_dockerfile + +## Output Files + +After a successful run you will find: + +| File | Description | +|---|---| +| `.dockerfile` | Generated Dockerfile | +| `.yaml` | KubeFlow Pipelines component spec | +| `.job.yaml` | Kubernetes Job spec | +| `.cwl` | CWL component descriptor | diff --git a/docs/c3/index.md b/docs/c3/index.md new file mode 100644 index 00000000..7eb7273a --- /dev/null +++ b/docs/c3/index.md @@ -0,0 +1,61 @@ +# C3 – CLAIMED Component Compiler + +C3 automates the transformation of arbitrary code assets into fully portable, executable AI components. + +--- + +## What C3 does + +``` + ┌──────────────────────┐ + │ .ipynb / .py / .R │ ← your code + └──────────┬───────────┘ + │ c3_create_operator + ▼ + ┌──────────────────────────────────────────┐ + │ Dockerfile (build + push) │ + │ KubeFlow component YAML │ + │ Kubernetes Job YAML │ + │ CWL component descriptor │ + └──────────────────────────────────────────┘ +``` + +C3 reads **parameter declarations** from the top of your source file: + +```python +import os + +# description of my_param +my_param = os.environ.get('my_param', 'default_value') +``` + +Each `os.environ.get(...)` line is parsed into a typed, documented parameter +that appears in the generated YAML descriptors and KFP UI. + +--- + +## Modules + +| Module | CLI entry-point | Purpose | +|---|---|---| +| [`create_operator`](create-operator.md) | `c3_create_operator` | Build container images and component descriptors | +| [`create_gridwrapper`](create-gridwrapper.md) | `c3_create_gridwrapper` | Wrap a component for parallel grid execution | +| [`create_containerless_operator`](create-operator.md) | `c3_create_containerless_operator` | Containerless variant (runs in-process) | +| [`operator_utils`](operator-utils.md) | – | Shared helpers (connection strings, logging) | +| `parser` | – | Source-file parameter parser | +| `notebook` | – | Jupyter notebook handler | +| `pythonscript` | – | Python script handler | +| `rscript` | – | R script handler | + +--- + +## Grid Compute Backends + +| Backend key | Description | +|---|---| +| `local` | Plain local filesystem | +| `cos` / `cos_grid_wrapper` | IBM Cloud Object Storage | +| `s3kv` | S3-backed key-value store (MLX) | +| `simple_grid_wrapper` | Minimal wrapper – source folder only | +| `folder_grid_wrapper` | Source **and** target folder variant | +| `legacy_cos_grid_wrapper` | Older COS format, kept for backwards compatibility | diff --git a/docs/c3/operator-utils.md b/docs/c3/operator-utils.md new file mode 100644 index 00000000..d9213acd --- /dev/null +++ b/docs/c3/operator-utils.md @@ -0,0 +1,39 @@ +# operator_utils + +Shared utility helpers used across C3 and the component library. + +## Python API + +::: claimed.c3.operator_utils + +## Connection String Format + +Many CLAIMED components accept a `cos_connection` parameter in the following URI format: + +``` +[cos|s3]://access_key_id:secret_access_key@endpoint_host/bucket/path +``` + +**Examples:** + +``` +s3://AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI@s3.us-east-1.amazonaws.com/my-bucket/data/ +cos://mykey:mysecret@s3.eu-de.cloud-object-storage.appdomain.cloud/my-bucket/models/ +``` + +### `explode_connection_string(cs)` + +Parses the URI into its components: + +```python +from claimed.c3.operator_utils import explode_connection_string + +access_key_id, secret_access_key, endpoint, path = explode_connection_string( + 's3://KEY:SECRET@s3.eu-de.cloud-object-storage.appdomain.cloud/my-bucket/prefix' +) +# endpoint → 'https://s3.eu-de.cloud-object-storage.appdomain.cloud' +# path → 'my-bucket/prefix' +``` + +If the string does not start with `cos://` or `s3://`, the input is returned as-is in the `path` field +(useful when passing a plain local path or a Kubernetes secret reference). diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..382ba3cb --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,135 @@ +# CLI Reference + +The `claimed` command is the single entry-point for the CLAIMED framework. + +--- + +## Synopsis + +``` +claimed [options] +``` + +--- + +## Subcommands + +### `claimed run` + +Directly invoke the `run()` function of any CLAIMED Python module. + +``` +claimed run [--param-name value ...] [--help] +``` + +**Arguments** + +| Argument | Description | +|---|---| +| `module.path` | Fully-qualified Python module containing a `run()` function (e.g. `claimed.components.util.cosutils`) | +| `--` | Any parameter accepted by `run()`. Hyphens are converted to underscores. | +| `--help` | Print the function signature, docstring, and parameter list, then exit. | + +**Type coercion** + +String values from the command line are automatically cast to the type declared in the function signature +(annotation or default-value type). +For example, `--recursive true` is cast to `bool` if the parameter is annotated as `bool`. + +**Examples** + +```bash +# List objects in a COS bucket +claimed run claimed.components.util.cosutils \ + --cos-connection s3://KEY:SECRET@endpoint/bucket \ + --operation ls \ + --local-path . + +# Download a file +claimed run claimed.components.util.cosutils \ + --cos-connection s3://KEY:SECRET@endpoint/bucket/file.zip \ + --operation get \ + --local-path . + +# Show help for any module +claimed run claimed.components.util.cosutils --help + +# CPU benchmark +claimed run claimed.components.util.gpu_performance_test \ + --mode cpu \ + --matrix-size 4096 \ + --iterations 100 +``` + +--- + +### `claimed create operator` + +Generate a container image + KFP/CWL/Kubernetes descriptors from a script or notebook. + +``` +claimed create operator [options] +``` + +| Option | Description | +|---|---| +| `--repository` | Container registry namespace, e.g. `docker.io/myuser` | +| `--version` | Image tag (default: auto-detected from script) | +| `--additional-files` | Space-separated list of extra files to bundle | + +Example: + +```bash +claimed create operator my_script.py --repository docker.io/myuser +``` + +--- + +### `claimed create gridwrapper` + +Wrap a component so it executes in parallel over a collection of inputs. + +``` +claimed create gridwrapper [options] +``` + +| Option | Description | +|---|---| +| `--backend` | Storage backend: `local` \| `cos` \| `s3kv` \| `simple_grid_wrapper` \| `folder_grid_wrapper` | +| `--component-inputs` | Comma-separated parameter names that vary across grid cells | +| `--repository` | Container registry namespace | + +Example: + +```bash +claimed create gridwrapper my_script.py \ + --backend cos \ + --component-inputs input_file \ + --repository docker.io/myuser +``` + +--- + +### `claimed --component` *(legacy)* + +Run a component image via Docker. + +``` +claimed --component [--param-name value ...] +``` + +| Option | Description | +|---|---| +| `--component` | Docker image reference, e.g. `docker.io/claimed/my-op:latest` | +| `--` | Environment variable to pass into the container | + +Set `CLAIMED_DATA_PATH` to mount a local directory as `/opt/app-root/src/data` inside the container. + +--- + +## Environment Variables + +| Variable | Effect | +|---|---| +| `CLAIMED_DATA_PATH` | Local path mounted as `/opt/app-root/src/data` when using `--component` | +| `CLAIMED_CONTAINERLESS_OPERATOR_PATH` | Root path for containerless operator resolution | diff --git a/docs/components/util/cosutils.md b/docs/components/util/cosutils.md new file mode 100644 index 00000000..07bc8502 --- /dev/null +++ b/docs/components/util/cosutils.md @@ -0,0 +1,62 @@ +# cosutils + +COS/S3 utility component providing common object-storage operations. + +## CLI + +```bash +claimed run claimed.components.util.cosutils --help +``` + +```bash +claimed run claimed.components.util.cosutils \ + --cos-connection s3://KEY:SECRET@endpoint/bucket/path \ + --operation \ + --local-path \ + [--recursive true] \ + [--log-level DEBUG] +``` + +## Operations + +| `--operation` | Description | +|---|---| +| `ls` | List objects at the path | +| `find` | Recursively find all objects | +| `mkdir` | Create a bucket/prefix | +| `get` | Download object(s) to `local_path` | +| `put` | Upload `local_path` to the COS path | +| `rm` | Delete object(s) | +| `glob` | Return all paths matching a glob pattern | +| `sync_to_cos` | Upload only changed local files to COS | +| `sync_to_local` | Download only changed COS objects to local | + +## Examples + +```bash +# List a bucket +claimed run claimed.components.util.cosutils \ + --cos-connection "s3://KEY:SECRET@s3.eu-de.cloud-object-storage.appdomain.cloud/my-bucket" \ + --operation ls \ + --local-path . + +# Download a single file +claimed run claimed.components.util.cosutils \ + --cos-connection "s3://KEY:SECRET@s3.eu-de.cloud-object-storage.appdomain.cloud/my-bucket/model.zip" \ + --operation get \ + --local-path . + +# Upload an entire directory +claimed run claimed.components.util.cosutils \ + --cos-connection "s3://KEY:SECRET@s3.eu-de.cloud-object-storage.appdomain.cloud/my-bucket/output/" \ + --operation put \ + --local-path ./results \ + --recursive true +``` + +## Python API + +::: claimed.components.util.cosutils + options: + members: + - run diff --git a/docs/components/util/gpu-benchmark.md b/docs/components/util/gpu-benchmark.md new file mode 100644 index 00000000..2000e506 --- /dev/null +++ b/docs/components/util/gpu-benchmark.md @@ -0,0 +1,67 @@ +# gpu_performance_test + +PyTorch HPC benchmark component covering CPU, single-GPU, and multi-node distributed (DDP) workloads. + +## CLI + +```bash +claimed run claimed.components.util.gpu_performance_test --help +``` + +```bash +# CPU matrix-multiply benchmark +claimed run claimed.components.util.gpu_performance_test \ + --mode cpu \ + --matrix-size 4096 \ + --iterations 100 + +# Single GPU full benchmark +claimed run claimed.components.util.gpu_performance_test \ + --mode single_gpu \ + --steps 50 + +# Multi-node DDP (via torchrun) +torchrun --nnodes=2 --nproc_per_node=4 \ + -m claimed.components.util.gpu_performance_test \ + --mode ddp +``` + +## Benchmark Phases + +| Phase | Metric | Description | +|---|---|---| +| DataLoader throughput | samples/sec | Measures IO / preprocessing pipeline speed | +| Training throughput | samples/sec | Forward + backward + optimiser step | +| Inference throughput | samples/sec | Forward pass only, `torch.no_grad()` | +| GPU compute | GFLOPS | Dense matrix-multiply (`torch.mm`) | +| CPU compute | GFLOPS | Same on CPU tensors | + +## Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `mode` | str | `single_gpu` | `cpu` \| `single_gpu` \| `ddp` | +| `batch_size` | int | 256 | DataLoader batch size | +| `num_workers` | int | 4 | DataLoader worker processes | +| `dataset_size` | int | 100 000 | Total synthetic samples | +| `steps` | int | 100 | Batches per benchmark phase | +| `input_dim` | int | 1 024 | MLP input feature dimension | +| `hidden_dim` | int | 2 048 | MLP hidden layer width | +| `num_classes` | int | 10 | Output classes | +| `depth` | int | 3 | Number of hidden layers | +| `materialize_dir` | str | `None` | Cache synthetic data on disk | +| `cleanup` | bool | `False` | Delete `materialize_dir` after benchmark | +| `matrix_size` | int | 2 048 | Square matrix edge for compute test | +| `iterations` | int | 50 | Matrix-multiply iterations | + +## Python API + +::: claimed.components.util.gpu_performance_test + options: + members: + - run + - benchmark_cpu + - benchmark_gpu + - benchmark_training + - benchmark_inference + - benchmark_dataloader diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..5f9c5982 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,58 @@ +# Contributing + +Thank you for your interest in contributing to CLAIMED! + +## Code of Conduct + +Please read and follow our [Code of Conduct](https://github.com/claimed-framework/claimed/blob/main/CODE_OF_CONDUCT.md). + +## Development Setup + +```bash +git clone https://github.com/claimed-framework/claimed.git +cd claimed +pip install -e ".[dev]" +pip install -r test_requirements.txt +``` + +## Running Tests + +```bash +pytest tests/ +``` + +## Adding a New Component + +1. Create a directory under `src/claimed/components//` +2. Add an empty `__init__.py` +3. Create `.py` with: + - Module-level `os.environ.get(...)` parameter declarations + - A `run(...)` function with type annotations and a docstring + - A `main()` entry-point that calls `run()` + - `if __name__ == "__main__": main()` +4. Add a documentation page under `docs/components//.md` +5. Register the page in `mkdocs.yml` under `nav` + +## Improving Documentation + +The docs live in `docs/` and are built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/). + +Local preview: + +```bash +pip install mkdocs-material mkdocstrings[python] +mkdocs serve +``` + +Then open . + +## Submitting a Pull Request + +1. Fork the repository +2. Create a branch: `git checkout -b feat/my-feature` +3. Commit your changes +4. Push: `git push origin feat/my-feature` +5. Open a Pull Request against `main` + +Please follow the [contribution process](https://github.com/claimed-framework/claimed/blob/main/contribution_process.md) +and the [release process](https://github.com/claimed-framework/claimed/blob/main/release_process.md) docs. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..dcabca23 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,125 @@ +# Getting Started + +## Prerequisites + +| Requirement | Version | +|---|---| +| Python | ≥ 3.7 | +| Docker / Podman | any recent version (for building images) | +| pip | ≥ 22 | + +--- + +## Installation + +```bash +pip install claimed +``` + +To install directly from the repository: + +```bash +git clone https://github.com/claimed-framework/claimed.git +cd claimed +pip install -e . +``` + +--- + +## Your First Component + +### 1. Write a Python script (or notebook) + +CLAIMED reads **parameter declarations** from the top of your script – one variable per line, with an optional comment describing it: + +```python title="my_operator.py" +import os + +# input CSV file path +input_file = os.environ.get('input_file', 'data.csv') + +# number of rows to process +num_rows = int(os.environ.get('num_rows', 100)) + +# --- your logic below --- +import pandas as pd +df = pd.read_csv(input_file, nrows=num_rows) +print(df.head()) +``` + +### 2. Build a container image + +```bash +c3_create_operator my_operator.py --repository myregistry/myuser +``` + +C3 will: + +1. Parse the parameter declarations +2. Generate a `Dockerfile` +3. Build and push the image +4. Write a KubeFlow Pipelines component YAML and a Kubernetes Job YAML + +### 3. Run the component + +```bash +# locally via Docker +claimed --component myregistry/myuser/my-operator \ + --input-file data.csv \ + --num-rows 50 + +# or directly as a Python function +claimed run my_operator --input-file data.csv --num-rows 50 +``` + +--- + +## Grid Wrappers + +A **grid wrapper** parallelises a component over a set of inputs: + +```bash +c3_create_gridwrapper my_operator.py \ + --backend cos \ + --component-inputs "input_file" \ + --repository myregistry/myuser +``` + +See the [C3 overview](c3/index.md) for full details. + +--- + +## Using the Component Library + +Every module under `claimed.components` exposes a `run()` function: + +```python +from claimed.components.util.cosutils import run as cos + +cos( + cos_connection='s3://KEY:SECRET@endpoint/bucket/path', + operation='ls', + local_path='.', +) +``` + +Or from the CLI: + +```bash +claimed run claimed.components.util.cosutils \ + --cos-connection s3://KEY:SECRET@endpoint/bucket/path \ + --operation ls \ + --local-path . +``` + +--- + +## Next Steps + +| Topic | Link | +|---|---| +| Full CLI reference | [CLI Reference](cli.md) | +| C3 internals | [C3 – Component Compiler](c3/index.md) | +| MLX asset backend | [MLX Backend](mlx/index.md) | +| COS/S3 utilities | [cosutils](components/util/cosutils.md) | +| GPU benchmarking | [gpu_performance_test](components/util/gpu-benchmark.md) | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..4aee00a2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,60 @@ +# CLAIMED Framework + +[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/6718/badge)](https://bestpractices.coreinfrastructure.org/projects/6718) +[![PyPI](https://img.shields.io/pypi/v/claimed)](https://pypi.org/project/claimed/) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/claimed-framework/claimed/blob/main/LICENSE) + +**CLAIMED** is a framework for building, packaging, and executing portable AI components at scale. + +--- + +## What is CLAIMED? + +CLAIMED has three interlocking layers: + +| Layer | Package / module | Purpose | +|---|---|---| +| **C3** – Component Compiler | `claimed.c3` | Turns notebooks, Python scripts, and R scripts into fully containerised, executable AI components | +| **MLX** – ML eXchange backend | `claimed.mlx` | Tracks datasets, models, jobs and other assets; powers the grid-compute backend | +| **Component Library** | `claimed.components.*` | Ready-to-use components for COS/S3 I/O, benchmarking, NLP, training, and more | + +--- + +## Key Features + +- **Zero-boilerplate packaging** – point C3 at any `.ipynb`, `.py`, or `.R` file and get a Docker image plus KFP/CWL/Kubernetes descriptors +- **Grid parallelisation** – distribute work across heterogeneous clusters with a single `claimed run` call +- **MLX asset tracking** – full provenance for every dataset, model, and job +- **CLI-first** – every component is callable as `claimed run --param value` +- **KubeFlow Pipelines & Kubernetes** – first-class output formats + +--- + +## Quick Install + +```bash +pip install claimed +``` + +--- + +## Quick Example + +```bash +# List files in a COS/S3 bucket +claimed run claimed.components.util.cosutils \ + --cos-connection s3://KEY:SECRET@endpoint/bucket \ + --operation ls \ + --local-path . + +# Show all parameters for any module +claimed run claimed.components.util.cosutils --help +``` + +--- + +## Video Introduction + + diff --git a/docs/mlx/cos-backend.md b/docs/mlx/cos-backend.md new file mode 100644 index 00000000..142fad47 --- /dev/null +++ b/docs/mlx/cos-backend.md @@ -0,0 +1,7 @@ +# cos_backend + +Low-level S3/COS file operations powering the MLX backend. + +## Python API + +::: claimed.mlx.cos_backend diff --git a/docs/mlx/index.md b/docs/mlx/index.md new file mode 100644 index 00000000..1208acc6 --- /dev/null +++ b/docs/mlx/index.md @@ -0,0 +1,53 @@ +# MLX Backend + +The **Machine Learning eXchange (MLX)** backend is responsible for tracking and managing all assets +used and produced by the CLAIMED framework. + +--- + +## What MLX tracks + +| Asset type | Description | +|---|---| +| **Datasets** | Input/output data files stored in S3/COS | +| **Models** | Trained model artefacts | +| **Jobs** | Execution records and logs | +| **Pipeline runs** | End-to-end provenance graphs | + +--- + +## Architecture + +``` + claimed.c3 grid wrappers + │ + ▼ + claimed.mlx.s3_kv_store ← key-value abstraction over S3/COS + │ + ▼ + claimed.mlx.cos_backend ← low-level S3/COS operations (s3fs) + │ + ▼ + S3 / IBM COS +``` + +--- + +## Modules + +| Module | Description | +|---|---| +| [`cos_backend`](cos-backend.md) | Low-level S3/COS file operations | +| [`s3_kv_store`](s3-kv-store.md) | Key-value store abstraction used by grid wrappers | + +--- + +## Configuration + +The MLX backend is configured through connection strings in the standard CLAIMED format: + +``` +s3://access_key_id:secret_access_key@endpoint_host/bucket/prefix +``` + +See [operator_utils](../c3/operator-utils.md) for the full connection string specification. diff --git a/docs/mlx/s3-kv-store.md b/docs/mlx/s3-kv-store.md new file mode 100644 index 00000000..85675c4f --- /dev/null +++ b/docs/mlx/s3-kv-store.md @@ -0,0 +1,7 @@ +# s3_kv_store + +Key-value store abstraction over S3/COS used by C3 grid wrappers to coordinate parallel work. + +## Python API + +::: claimed.mlx.s3_kv_store diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..87521e23 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,80 @@ +site_name: CLAIMED Framework +site_url: https://claimed-framework.github.io/claimed/ +site_description: >- + The CLAIMED framework – C3 Component Compiler, MLX backend, + and a library of ready-to-use AI components. +site_author: The CLAIMED authors +repo_name: claimed-framework/claimed +repo_url: https://github.com/claimed-framework/claimed +edit_uri: edit/main/docs/ + +theme: + name: material + logo: assets/logo.png + favicon: assets/logo.png + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.highlight + - content.code.copy + - content.action.edit + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_root_heading: true + heading_level: 2 + merge_init_into_class: true + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - attr_list + - md_in_html + - toc: + permalink: true + +nav: + - Home: index.md + - Getting Started: getting-started.md + - CLI Reference: cli.md + - C3 – Component Compiler: + - Overview: c3/index.md + - create_operator: c3/create-operator.md + - create_gridwrapper: c3/create-gridwrapper.md + - operator_utils: c3/operator-utils.md + - MLX Backend: + - Overview: mlx/index.md + - cos_backend: mlx/cos-backend.md + - s3_kv_store: mlx/s3-kv-store.md + - Components: + - util/cosutils: components/util/cosutils.md + - util/gpu_performance_test: components/util/gpu-benchmark.md + - Contributing: contributing.md