Skip to content

ezpz.cli.flags⚓︎

Shared argparse builders for ezpz CLI entrypoints.

add_profiling_args(parser) ⚓︎

Add the shared profiling flag-set used across ezpz.examples.*.

This is the single source of truth for the profiler CLI surface. ezpz.examples.test was the only example with profiling support; these flags were inlined in :func:build_test_parser. Extracting them here lets every example (fsdp_tp, fsdp, vit, diffusion) expose an identical surface by calling this once, instead of copy-pasting ~80 lines that would inevitably drift.

Pair with :func:ezpz.profile.profiling_context_from_args, which consumes the resulting namespace and returns the context manager.

The flag spellings are kept exactly as they were in test so existing muscle-memory / docs / scripts carry over unchanged:

  • --pyinstrument-profiler — statistical (pyinstrument) profiler.
  • -p / --profile (dest pytorch_profiler) — torch profiler.
  • --rank-zero-only — only profile rank 0 (one trace, not N).
  • --pytorch-profiler-{wait,warmup,active,repeat} — schedule.
  • --profile-memory / --record-shapes / --with-stack / --with-flops / --with-modules — torch profiler detail toggles (BooleanOptionalAction → each has a --no-* form).
  • --acc-events — accumulate events across schedule cycles.
Source code in src/ezpz/cli/flags.py
def add_profiling_args(
    parser: argparse.ArgumentParser,
) -> argparse.ArgumentParser:
    """Add the shared profiling flag-set used across ``ezpz.examples.*``.

    This is the single source of truth for the profiler CLI surface.
    ``ezpz.examples.test`` was the only example with profiling support;
    these flags were inlined in :func:`build_test_parser`. Extracting
    them here lets every example (`fsdp_tp`, `fsdp`, `vit`, `diffusion`)
    expose an identical surface by calling this once, instead of
    copy-pasting ~80 lines that would inevitably drift.

    Pair with :func:`ezpz.profile.profiling_context_from_args`, which
    consumes the resulting namespace and returns the context manager.

    The flag spellings are kept exactly as they were in ``test`` so
    existing muscle-memory / docs / scripts carry over unchanged:

    - ``--pyinstrument-profiler`` — statistical (pyinstrument) profiler.
    - ``-p`` / ``--profile`` (dest ``pytorch_profiler``) — torch profiler.
    - ``--rank-zero-only`` — only profile rank 0 (one trace, not N).
    - ``--pytorch-profiler-{wait,warmup,active,repeat}`` — schedule.
    - ``--profile-memory`` / ``--record-shapes`` / ``--with-stack`` /
      ``--with-flops`` / ``--with-modules`` — torch profiler detail
      toggles (``BooleanOptionalAction`` → each has a ``--no-*`` form).
    - ``--acc-events`` — accumulate events across schedule cycles.
    """
    parser.add_argument(
        "--pyinstrument-profiler",
        action="store_true",
        help="Profile the training loop",
    )
    parser.add_argument(
        "-p",
        "--profile",
        default=False,
        dest="pytorch_profiler",
        required=False,
        action="store_true",
        help="Use PyTorch profiler",
    )
    parser.add_argument(
        "--rank-zero-only",
        action="store_true",
        help="Run profiler only on rank 0",
    )
    parser.add_argument(
        "--pytorch-profiler-wait",
        type=int,
        default=1,
        help="Wait time before starting the PyTorch profiler",
    )
    parser.add_argument(
        "--pytorch-profiler-warmup",
        type=int,
        default=2,
        help="Warmup iterations for the PyTorch profiler",
    )
    parser.add_argument(
        "--pytorch-profiler-active",
        type=int,
        default=3,
        help="Active iterations for the PyTorch profiler",
    )
    parser.add_argument(
        "--pytorch-profiler-repeat",
        type=int,
        default=5,
        help="Repeat iterations for the PyTorch profiler",
    )
    # The next five flags default to True. They were declared as
    # `action="store_true"`, which made them unreachable as False —
    # passing the flag and not passing it both produced True.
    # BooleanOptionalAction generates the matching --no-* form so users
    # can actually disable each one (e.g. `--no-with-stack`).
    parser.add_argument(
        "--profile-memory",
        default=True,
        action=argparse.BooleanOptionalAction,
        help="Profile memory usage",
    )
    parser.add_argument(
        "--record-shapes",
        default=True,
        action=argparse.BooleanOptionalAction,
        help="Record shapes in the profiler",
    )
    parser.add_argument(
        "--with-stack",
        default=True,
        action=argparse.BooleanOptionalAction,
        help="Include stack traces in the profiler",
    )
    parser.add_argument(
        "--with-flops",
        default=True,
        action=argparse.BooleanOptionalAction,
        help="Include FLOPs in the profiler",
    )
    parser.add_argument(
        "--with-modules",
        default=True,
        action=argparse.BooleanOptionalAction,
        help="Include module information in the profiler",
    )
    parser.add_argument(
        "--acc-events",
        default=False,
        action="store_true",
        help="Accumulate events in the profiler",
    )
    return parser

build_doctor_parser(*, prog=None) ⚓︎

Build the CLI argument parser for ezpz doctor.

Source code in src/ezpz/cli/flags.py
def build_doctor_parser(*, prog: str | None = None) -> argparse.ArgumentParser:
    """Build the CLI argument parser for ``ezpz doctor``."""
    parser = argparse.ArgumentParser(
        prog=prog,
        formatter_class=DefaultsFormatter,
        description="Inspect the current environment for ezpz launch readiness.",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Emit machine-readable JSON instead of human-friendly text.",
    )
    return parser

build_generate_parser(*, prog=None) ⚓︎

Build the CLI argument parser for ezpz generate.

Source code in src/ezpz/cli/flags.py
def build_generate_parser(
    *, prog: str | None = None
) -> argparse.ArgumentParser:
    """Build the CLI argument parser for ``ezpz generate``."""
    parser = argparse.ArgumentParser(
        prog=prog,
        formatter_class=DefaultsFormatter,
        description="Generate text using a model.",
    )
    parser.add_argument(
        "--model_name",
        type=str,
        default="meta-llama/Llama-3.2-1B",
        help="Name of the model to use.",
    )
    parser.add_argument(
        "--dtype",
        type=str,
        default="bfloat16",
        choices=["float16", "bfloat16", "float32"],
        help="Data type to use for the model.",
    )
    return parser

build_launch_parser(*, prog=None, include_command=True) ⚓︎

Build the CLI argument parser for ezpz launch.

Source code in src/ezpz/cli/flags.py
def build_launch_parser(
    *, prog: str | None = None, include_command: bool = True
) -> argparse.ArgumentParser:
    """Build the CLI argument parser for ``ezpz launch``."""
    parser = argparse.ArgumentParser(
        prog=prog,
        description="Launch a command on the current PBS/SLURM job.\n"
        "\n"
        "Additional `<launcher flags>` can be passed through directly\n"
        "to the launcher by including '--' as a separator before\n"
        "the command.\n"
        "\n"
        "Examples:\n"
        "\n"
        "    ezpz launch <launcher flags> -- <command> <args>\n"
        "\n"
        "    ezpz launch -n 8 -ppn 4 --verbose --tag-output -- python3 -m ezpz.examples.fsdp_tp\n"
        "\n"
        "    ezpz launch --nproc 8 -x EZPZ_LOG_LEVEL=DEBUG -- python3 my_script.py --my-arg val\n"
        if include_command
        else "Launch a command on the current PBS/SLURM job.",
        # ),
        # description="\n".join(
        #     [
        #         "Launch a command on the current PBS/SLURM job.",
        #         "",
        #         "Additional `<launcher flags>` can be passed through directly",
        #         "to the launcher by including '--' as a separator before ",
        #         "the command.",
        #         "Examples:",
        #         "\t$ # ezpz launch <launcher flags> -- <command> <args>"
        #         "\t$ ezpz launch --nproc 8 -x EZPZ_LOG_LEVEL=DEBUG -- python3 my_script.py --my-arg val",
        #     ]
        # )
        formatter_class=RawDescAndDefaultsFormatter,
    )
    parser.add_argument(
        "--print-source",
        action="store_true",
        help="Print the location of the launch CLI source and exit.",
    )
    parser.add_argument(
        "--filter",
        type=str,
        nargs="+",
        help="Deprecated: output filtering has been removed. This flag is ignored.",
    )
    parser.add_argument(
        "-n",
        "-np",
        "--n",
        "--np",
        "--nproc",
        "--world_size",
        "--nprocs",
        type=int,
        dest="nproc",
        default=-1,
        help="Number of processes.",
    )
    parser.add_argument(
        "-ppn",
        "--ppn",
        "--nproc_per_node",
        type=int,
        default=-1,
        dest="nproc_per_node",
        help="Processes per node.",
    )
    parser.add_argument(
        "-nh",
        "--nh",
        "--nhost",
        "--nnode",
        "--nnodes",
        "--nhosts",
        "--nhosts",
        type=int,
        default=-1,
        dest="nhosts",
        help="Number of nodes to use.",
    )
    parser.add_argument(
        "--hostfile",
        type=str,
        default=None,
        dest="hostfile",
        help="Hostfile to use for launching.",
    )
    parser.add_argument(
        "--cpu-bind",
        type=str,
        default=None,
        dest="cpu_bind",
        help=(
            "CPU binding value to pass to the launcher. "
            "Takes precedence over CPU_BIND when both are specified."
        ),
    )
    parser.add_argument(
        "--timeout",
        type=_non_negative_int,
        default=None,
        dest="idle_timeout_s",
        help=(
            "Idle-output watchdog timeout in seconds. If the launched "
            "process produces no output (on stdout OR stderr — they "
            "are merged) for this many seconds, send SIGTERM (then "
            "SIGKILL after a 10s grace period) and exit with code 124. "
            "NOT a total walltime — the process can run indefinitely "
            "as long as it keeps emitting output on either stream. "
            "Off by default, BUT defaults to 1800 (30 min) when "
            "--auto-retry is set (matches FAILOVER_IDLE_TIMEOUT in "
            "src/ezpz/bin/failover.sh — silent xccl hangs otherwise "
            "burn the full PBS walltime). Pass 0 explicitly to disable "
            "even under --auto-retry. Useful for catching collective "
            "hangs (e.g. xccl silent deadlock on XPU)."
        ),
    )
    parser.add_argument(
        "--retries",
        type=_non_negative_int,
        default=0,
        dest="retries",
        help=(
            "Re-execute the launched command up to N times on non-zero "
            "exit (including a watchdog kill, exit 124). Applies "
            "exponential backoff between attempts (5s, 10s, 20s, ..., "
            "capped at 60s). Default: 0 (no retry)."
        ),
    )
    parser.add_argument(
        "--auto-retry",
        action="store_true",
        dest="auto_retry",
        help=(
            "Run with automatic bad-node failover. On every non-zero "
            "exit (including watchdog kill, exit 124, and walltime-"
            "racing crashes that surface as 143), scrape the log for "
            "known bad-node signatures (Aurora PALS shepherd-9, gloo "
            "peer-closed) and either swap the named hosts for spares "
            "or rotate one spare blindly. Loops until success, a real "
            "walltime hit, spare exhaustion, two consecutive attempts "
            "with zero training progress, or SIGINT. "
            "Requires --nproc to be set explicitly. "
            "Mutually exclusive with --retries."
        ),
    )
    parser.add_argument(
        "--spare-nodes",
        type=_spare_nodes_value,
        default=None,
        dest="spare_nodes",
        help=(
            "Number of spare nodes to reserve for --auto-retry "
            "swap-ins. Pass 'auto' (the default when --auto-retry is "
            "set) to derive from total_pbs_nodes - ceil($nproc / $ppn) "
            "(ranks → hosts, since --nproc counts ranks). Pass an "
            "integer for an explicit count. Ignored when --auto-retry "
            "is not set."
        ),
    )
    parser.add_argument(
        "--max-failover-retries",
        type=_non_negative_int,
        default=None,
        dest="max_failover_retries",
        help=(
            "Upper bound on --auto-retry attempts. Default: unbounded "
            "— termination is governed by the matrix in "
            "docs/cli/launch/index.md (success, walltime, exhaustion, "
            "stuck-pre-training, or SIGINT)."
        ),
    )
    if include_command:
        parser.add_argument(
            "command",
            nargs=argparse.REMAINDER,
            help="Command (and arguments) to execute. Use '--' to separate options when needed.",
        )
    return parser

build_test_parser(*, prog=None) ⚓︎

Build the CLI argument parser for ezpz test (ezpz.examples.test).

Source code in src/ezpz/cli/flags.py
def build_test_parser(*, prog: str | None = None) -> argparse.ArgumentParser:
    """Build the CLI argument parser for ``ezpz test`` (ezpz.examples.test)."""
    parser = argparse.ArgumentParser(
        prog=prog,
        formatter_class=DefaultsFormatter,
        description=(
            """
            ezpz test: A simple PyTorch distributed smoke test

            Trains a simple MLP on MNIST dataset using DDP.

            NOTE: `ezpz test` is a lightweight wrapper around:

            ```bash
            ezpz launch python3 -m ezpz.examples.test
            ```
            """
        ),
    )
    parser.add_argument(
        "--warmup",
        type=int,
        default=5,
        help="Warmup iterations",
    )
    parser.add_argument(
        "--tp",
        type=int,
        default=1,
        help="Tensor parallel size",
    )
    parser.add_argument(
        "--pp",
        type=int,
        default=1,
        help="Pipeline length",
    )
    parser.add_argument(
        "--deepspeed_config",
        type=str,
        default="deepspeed_config.json",
        help="Deepspeed config file",
    )
    parser.add_argument(
        "--cp",
        type=int,
        default=1,
        help="Context parallel size",
    )
    parser.add_argument(
        "--backend",
        required=False,
        type=str,
        default="DDP",
        help="Backend (DDP, DeepSpeed, etc.)",
    )
    # Shared profiler flags (--profile / --pyinstrument-profiler / etc.)
    # live in add_profiling_args so every ezpz.examples.* module exposes
    # an identical surface. --save-datasets below is test-specific.
    add_profiling_args(parser)
    parser.add_argument(
        "--save-datasets",
        action="store_true",
        default=False,
        help="Save datasets",
    )
    parser.add_argument(
        "--train-iters",
        "--train_iters",
        type=int,
        default=200,
        help="Number of training iterations",
    )
    parser.add_argument(
        "--model",
        type=str,
        default=None,
        # Choices kept in sync with `ezpz.examples.test.MODEL_PRESETS`
        # and `MODEL_ALIASES`. If you add a size to test.py, add it
        # here too (this parser is in cli/flags.py, separate module
        # from test.py — couldn't import MODEL_PRESETS without
        # creating a circular dep).
        choices=[
            "debug",
            "s", "small",
            "m", "medium",
            "l", "large",
            "xl", "xlarge", "extra-large",
            "xxl", "xxlarge", "extra-extra-large",
            "xxxl", "xxxlarge", "extra-extra-extra-large",
        ],
        help=(
            "Model size preset for the smoke test. The size ladder is "
            "`debug → s → m → l → xl → xxl → xxxl` targeting roughly "
            "100M/250M/500M/1B/5B/10B params respectively. Long-form "
            "aliases (`small`/`medium`/`large`/`xlarge`/`extra-large`/...) "
            "map onto the same ladder. `debug` is the laptop-runnable "
            "smoke-test preset (~110K params)."
        ),
    )
    parser.add_argument(
        "--log-freq",
        "--log_freq",
        type=int,
        default=1,
        help="Logging frequency",
    )
    parser.add_argument(
        "--print-freq",
        "--print_freq",
        type=int,
        default=10,
        help="Printing frequency",
    )
    parser.add_argument(
        "--batch-size",
        type=int,
        default=128,
        help="Batch size",
    )
    parser.add_argument(
        "--input-size",
        type=int,
        default=28 * 28,
        help="Input size",
    )
    parser.add_argument(
        "--output-size",
        type=int,
        default=10,
        help="Output size",
    )
    parser.add_argument(
        "--layer-sizes",
        help="Comma-separated list of layer sizes",
        type=lambda s: [int(item) for item in s.split(",")],
        default=[512, 256, 128],
    )
    parser.add_argument(
        "--dtype",
        type=str,
        default="bf16",
        help="Data type (fp16, float16, bfloat16, bf16, float32, etc.)",
    )
    parser.add_argument(
        "--dataset",
        type=str,
        default="mnist",
        help="Dataset to use for training (e.g., mnist).",
    )
    parser.add_argument(
        "--dataset-root",
        type=str,
        default=None,
        help="Directory to cache dataset downloads.",
    )
    parser.add_argument(
        "--num-workers",
        type=int,
        default=0,
        help="Number of dataloader workers to use.",
    )
    parser.add_argument(
        "--no-distributed-history",
        action="store_true",
        help="Disable distributed history aggregation",
    )
    parser.add_argument(
        "--compile",
        action="store_true",
        help="Wrap the model with torch.compile after FSDP/DDP wrap.",
    )
    parser.add_argument(
        "--compile-mode",
        type=str,
        default="default",
        choices=["default", "reduce-overhead", "max-autotune"],
        help=(
            "torch.compile mode (only used when --compile is set). "
            "`default` is safest. `reduce-overhead` enables cudagraphs "
            "for small models / large batches. `max-autotune` does "
            "extensive kernel search - slow startup, fastest steady state."
        ),
    )
    return parser