vLLM Explained

engine/arg_utils.py: EngineArgs, the flags and create_engine_config

AdvancedCode mapabout 10 min read + 3 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/engine/arg_utils.py is the bridge from the command line to the engine. It makes the engine flags from the config classes, holds the parsed values in EngineArgs, and builds the VllmConfig in create_engine_config().

Filevllm/engine/arg_utils.py, 3,266 lines of Python. Two dataclasses, one argparse action and 17 module-level functions.
Main classesEngineArgs (vllm/engine/arg_utils.py:482-483), AsyncEngineArgs (vllm/engine/arg_utils.py:3230-3235).
Runs inThe process that parses the flags and builds the config: the API server process of vllm serve (main thread), or the process of the offline LLM class. The engine core and the workers do not use it.
Called bymake_arg_parser (flags), build_async_engine_client (from_cli_args and create_engine_config), run_multi_api_server, run_headless, the gRPC and render launchers, AsyncLLM.from_engine_args, LLMEngine.
CallsThe constructors of the config classes, current_platform (hooks, device name and memory), load_general_plugins, FlexibleArgumentParser.
Not in this fileThe argv rewrite (--config, -O, dotted keys: vllm/utils/argparse_utils.py), the front-end flags (FrontendArgs in vllm/entrypoints/common/cli_args.py), the late defaults (VllmConfig.__post_init__, config/vllm.py: VllmConfig, __post_init__ and compute_hash).

The page How configuration works explains the ideas. This page reads the file section by section.

Where it sits#

The diagram on How configuration works shows the file in panels 1 and 2: the parser fills EngineArgs, and create_engine_config builds the sub-configs and VllmConfig.

Who calls it#

What it calls#

Key classes and functions#

A map of the file, in line order:

LinesSectionWhat it does
vllm/engine/arg_utils.py:1-153ImportsEvery config class; current_platform at module level.
vllm/engine/arg_utils.py:161-282Type helpersparse_type, optional_type, union_dict_and_str, get_type_hints: turn a type hint into an argparse type.
vllm/engine/arg_utils.py:285-321NEEDS_HELP, docs URL, JSON numbersRead help text only for --help; accept 80m inside JSON.
vllm/engine/arg_utils.py:324-453_compute_kwargs, get_kwargsargparse settings for each field of a config class.
vllm/engine/arg_utils.py:456-479DeprecationsMessages and an action for three deprecated flags.
vllm/engine/arg_utils.py:482-871EngineArgs fieldsOne field per flag.
vllm/engine/arg_utils.py:873-949EngineArgs.__post_init__Dicts to config objects, quantization config, general plugins, offline paths.
vllm/engine/arg_utils.py:951-1959add_cli_argsAbout 1,000 lines: one argument group per config class.
vllm/engine/arg_utils.py:1961-1970from_cli_argsNamespace to dataclass.
vllm/engine/arg_utils.py:1972-2232create_*_config helpersModel, load, speculative, diffusion, watermark, structured outputs, observability, logging.
vllm/engine/arg_utils.py:2234-2905create_engine_configBuilds every sub-config and VllmConfig.
vllm/engine/arg_utils.py:2907-2924_check_feature_supportedPipeline parallelism needs a supported executor.
vllm/engine/arg_utils.py:2926-3026get_batch_defaultsToken budget and request limit per usage context and GPU.
vllm/engine/arg_utils.py:3028-3096_set_default_chunked_prefill_and_prefix_caching_argsNone becomes the model's support.
vllm/engine/arg_utils.py:3098-3136Reasoning config, multimodal minimumSmall helpers.
vllm/engine/arg_utils.py:3138-3227_set_default_max_num_seqs_and_batched_tokens_argsApplies the batch defaults and the caps.
vllm/engine/arg_utils.py:3230-3258AsyncEngineArgsAdds --enable-log-requests; plugins and the platform hook.

Walk through the main methods#

_compute_kwargs: a config field becomes argparse settings#

@functools.lru_cache(maxsize=30)
def _compute_kwargs(cls: ConfigType) -> dict[str, dict[str, Any]]:
    # Save time only getting attr docs if we're generating help text
    cls_docs = get_attr_docs(cls) if NEEDS_HELP else {}
    kwargs = {}
    for field in fields(cls):
        # Get the set of possible types for the field
        type_hints: set[TypeHint] = get_type_hints(field.type)
        # Subclasses of dict (e.g. MultiModalDummyOptions) are CLI dicts
        type_hints = {dict if is_dict_subclass(th) else th for th in type_hints}

        # If the field is a dataclass, we can use the model_validate_json
        generator = (th for th in type_hints if is_dataclass(th))
        dataclass_cls = next(generator, None)

        # Get the default value of the field
        if field.default is not MISSING:
            default = field.default
            # Handle pydantic.Field defaults
            if isinstance(default, FieldInfo):
                if default.default_factory is None:
                    default = default.default
                else:
                    # VllmConfig's Fields have default_factory set to config classes.
                    # These could emit logs on init, which would be confusing.
                    with suppress_logging():
                        default = default.default_factory()  # type: ignore[call-arg]
        elif field.default_factory is not MISSING:
            default = field.default_factory()

        # Get the help text for the field
        name = field.name
        help = cls_docs.get(name, "").strip()
        # Escape % for argparse
        help = help.replace("%", "%%")

        # Initialise the kwargs dictionary for the field
        kwargs[name] = {"default": default, "help": help}
  1. Line 324. lru_cache(maxsize=30): each config class is read once per process. get_kwargs returns a deep copy, so a caller can change it (vllm/engine/arg_utils.py:443-453).
  2. Line 327. The docstrings are read with ast only when NEEDS_HELP is true, that is with --help in argv or mkdocs loaded (vllm/engine/arg_utils.py:285-288).
  3. Lines 336-337. A field whose type is a dataclass is found here. It gets a JSON parser later (lines 367-378).
  4. Lines 340-352. The default: a plain value, a pydantic Field default, or the result of a default factory. Factories of VllmConfig fields run with logging off.

The rest of the function picks the argparse type from the type hint. The order of the checks matters: dataclass, bool | str | None, bool, Literal, tuple, list, set, int, float, dict, str (vllm/engine/arg_utils.py:367-427). Five int fields accept 1k or 2G; max_model_len also accepts auto (lines 397-409). A type hint with None wraps the type in optional_type, so the text None is accepted (lines 434-439, vllm/engine/arg_utils.py:173-179).

EngineArgs: the fields#

@dataclass
class EngineArgs:
    """Arguments for vLLM engine."""

    model: str = ModelConfig.model
    # Public compatibility argument. Canonical runtime state lives in
    # AuxOutputConfig.
    enable_return_routed_experts: bool = False
    return_sampling_mask: bool = ModelConfig.return_sampling_mask
    model_weights: str = ModelConfig.model_weights
    served_model_name: str | list[str] | None = ModelConfig.served_model_name
    tokenizer: str | None = ModelConfig.tokenizer
    hf_config_path: str | None = ModelConfig.hf_config_path
    runner: RunnerOption = ModelConfig.runner
    convert: ConvertOption = ModelConfig.convert
    skip_tokenizer_init: bool = ModelConfig.skip_tokenizer_init
    enable_prompt_embeds: bool = ModelConfig.enable_prompt_embeds
    tokenizer_mode: TokenizerMode | str = ModelConfig.tokenizer_mode
    trust_remote_code: bool = ModelConfig.trust_remote_code
    allowed_local_media_path: str = ModelConfig.allowed_local_media_path
    allowed_media_domains: list[str] | None = ModelConfig.allowed_media_domains
    download_dir: str | None = LoadConfig.download_dir

Most fields copy the default of a config field, for example model = ModelConfig.model (line 486). A field with a default factory uses get_field(VllmConfig, "compilation_config"), which copies the factory (vllm/engine/arg_utils.py:765, vllm/config/utils.py:85-114). The fields that create_engine_config decides are None: block_size, enable_prefix_caching, max_num_batched_tokens, max_num_seqs and enable_chunked_prefill (vllm/engine/arg_utils.py:591-592, vllm/engine/arg_utils.py:610, vllm/engine/arg_utils.py:616, vllm/engine/arg_utils.py:707).

EngineArgs.__post_init__#

    def __post_init__(self):
        # support `EngineArgs(compilation_config={...})`
        # without having to manually construct a
        # CompilationConfig object
        if isinstance(self.compilation_config, dict):
            self.compilation_config = CompilationConfig(**self.compilation_config)

The method repeats this pattern for the attention, auxiliary output, Engram, Mamba, kernel, encoder cache, EPLB, weight transfer, fault tolerance, IR priority and watchdog configs (vllm/engine/arg_utils.py:879-913). A dict passed for --fault-tolerance-config also turns fault tolerance on, with a warning (lines 899-908). Then it resolves the quantization config, loads the general plugins, and with HF_HUB_OFFLINE replaces the model name by a local path (lines 915-949).

add_cli_args: about 1,000 lines of flags#

        # Model arguments
        model_kwargs = get_kwargs(ModelConfig)
        aux_output_kwargs = get_kwargs(AuxOutputConfig)
        model_group = parser.add_argument_group(
            title="ModelConfig",
            description=ModelConfig.__doc__,
        )
        if not ("serve" in sys.argv[1:] and "--help" in sys.argv[1:]):
            model_group.add_argument("--model", **model_kwargs["model"])
        model_group.add_argument("--runner", **model_kwargs["runner"])
        model_group.add_argument("--convert", **model_kwargs["convert"])
        model_group.add_argument("--tokenizer", **model_kwargs["tokenizer"])
        model_group.add_argument("--tokenizer-mode", **model_kwargs["tokenizer_mode"])
  1. Line 955. The settings of all ModelConfig fields.
  2. Lines 957-960. One argument group, with the class name as title. --help=ModelConfig shows only this group.
  3. Lines 961-962. --model is hidden from vllm serve --help, because serve takes the model as a positional argument.

The groups, in order: ModelConfig, LoadConfig, AttentionConfig, MambaConfig, StructuredOutputsConfig, ParallelConfig, CacheConfig, OffloadConfig, MultiModalConfig, LoRAConfig, LoggingConfig, ObservabilityConfig, SchedulerConfig, CompilationConfig, KernelConfig and VllmConfig (vllm/engine/arg_utils.py:957, vllm/engine/arg_utils.py:1071, vllm/engine/arg_utils.py:1153, vllm/engine/arg_utils.py:1374, vllm/engine/arg_utils.py:1726, vllm/engine/arg_utils.py:1848). Seven flags without a group come last (lines 1908-1958). The full list is generated on vllm serve flags.

The VllmConfig group takes whole sub-configs as JSON. Three of them are parsed with json.loads only, so their pydantic check runs later, in create_engine_config:

        # We construct SpeculativeConfig using fields from other configs in
        # create_engine_config. So we set the type to a JSON string here to
        # delay the Pydantic validation that comes with SpeculativeConfig.
        vllm_kwargs["speculative_config"]["type"] = optional_type(json.loads)
        vllm_group.add_argument(
            "--speculative-config", "-sc", **vllm_kwargs["speculative_config"]
        )

from_cli_args#

    @classmethod
    def from_cli_args(cls, args: argparse.Namespace):
        # Get the list of attributes of this dataclass.
        attrs = [attr.name for attr in dataclasses.fields(cls)]

        # Set the attributes from the parsed arguments.
        engine_args = cls(
            **{attr: getattr(args, attr) for attr in attrs if hasattr(args, attr)}
        )
        return engine_args

Only names that are both a dataclass field and a Namespace attribute are copied. Front-end flags such as --port stay in the Namespace. AsyncEngineArgs is the class here, so enable_log_requests is copied too.

create_engine_config, part 1: platform and model#

        logging_config = self.create_logging_config()
        configure_logging_if_needed(logging_config)

        current_platform.pre_register_and_update()

        device_config = DeviceConfig(device=cast(Device, current_platform.device_type))

        current_platform.validate_environ(self.fail_on_environ_validation)

        # Check if the model is a speculator and override model/tokenizer/config
        # BEFORE creating ModelConfig, so the config is created with the target model
        # Skip speculator detection for cloud storage models (eg: S3, GCS) since
        # HuggingFace cannot load configs directly from S3 URLs. S3 models can still
        # use speculators with explicit --speculative-config.
        if not is_cloud_storage(self.model):
            (self.model, self.tokenizer, self.speculative_config) = (
                maybe_override_with_speculators(
                    model=self.model,
                    tokenizer=self.tokenizer,
                    revision=self.revision,
                    trust_remote_code=self.trust_remote_code,
                    vllm_speculative_config=self.speculative_config,
                    hf_token=self.hf_token,
                )
            )

        model_config = self.create_model_config()
        self.model = model_config.model
        self.model_weights = model_config.model_weights
        self.tokenizer = model_config.tokenizer

        self._check_feature_supported()
        self._set_default_chunked_prefill_and_prefix_caching_args(model_config)
  1. Line 2246. The platform hook, without a parser this time (The Platform interface).
  2. Line 2250. Unknown VLLM_* names give a warning, or ValueError with --fail-on-environ-validation.
  3. Lines 2257-2267. A speculators checkpoint can replace the model, the tokenizer and the speculative config before ModelConfig exists.
  4. Line 2269. create_model_config passes about 70 fields to ModelConfig (vllm/engine/arg_utils.py:1984-2054).
  5. Line 2275. The model decides chunked prefill and prefix caching (vllm/engine/arg_utils.py:3031-3035, vllm/engine/arg_utils.py:3062-3063).

create_engine_config, part 2: cache, parallel, scheduler#

Lines 2277-2321 build CacheConfig with the resolved KV cache dtype. Lines 2342-2557 read the Ray context and check the data parallel options. They raise a ValueError for each invalid mix (vllm/engine/arg_utils.py:2372-2378). Then ParallelConfig is made (vllm/engine/arg_utils.py:2558), followed by the speculative config and the batch defaults:

        speculative_config = self.create_speculative_config(
            target_model_config=model_config,
            target_parallel_config=parallel_config,
        )
        diffusion_config = self.create_diffusion_config()
        watermark_config = self.create_watermark_config()

        self._set_default_max_num_seqs_and_batched_tokens_args(
            usage_context,
            model_config,
            parallel_config,
        )

        assert self.max_num_batched_tokens is not None, (
            "max_num_batched_tokens must be set by this point"
        )
        assert self.max_num_seqs is not None, "max_num_seqs must be set by this point"
        assert self.enable_chunked_prefill is not None, (
            "enable_chunked_prefill must be set by this point"
        )
        assert model_config.max_model_len is not None, (
            "max_model_len must be set by this point"
        )

The asserts make sure that every None default is resolved before SchedulerConfig is made (lines 2637-2646, then 2647-2672). After that come LoRA, attention, Mamba, kernel, load, structured outputs, observability, compilation and offload (vllm/engine/arg_utils.py:2680-2850). Two shortcut flags conflict with their JSON form: --attention-backend with attention_config.backend, and --cudagraph-capture-sizes with compilation_config.cudagraph_capture_sizes (vllm/engine/arg_utils.py:2717-2722, vllm/engine/arg_utils.py:2821-2827). The function ends with VllmConfig(...) (vllm/engine/arg_utils.py:2870-2905).

get_batch_defaults and _set_default_max_num_seqs_and_batched_tokens_args#

        try:
            device_memory = current_platform.get_device_total_memory()
            device_name = current_platform.get_device_name().lower()
            # GB10 (DGX Spark, cc 12.1) has the memory but not the compute of
            # a large discrete GPU, keep it on the small tier.
            is_gb10 = current_platform.is_device_capability((12, 1))
        except Exception:
            # This is only used to set default_max_num_batched_tokens
            device_memory = 0
            device_name = ""
            is_gb10 = False

The memory and the name come from the platform. If the query fails, the memory is 0, so the small tier applies (lines 2951-2955). The tiers are in lines 2960-2993; TPU and CPU override them (lines 2995-3024). The caller then applies the values:

        if orig_max_num_batched_tokens is None:
            assert model_config.max_model_len is not None, (
                "max_model_len must be set by this point"
            )
            if not self.enable_chunked_prefill:
                # If max_model_len is too short, use the default for higher throughput.
                self.max_num_batched_tokens = max(
                    model_config.max_model_len,
                    self.max_num_batched_tokens,
                )

            # For multimodal prefix-LM models (e.g., Gemma 4) that disable
            # chunked MM input, a single multimodal item must fit in one batch.
            # Raise the floor to accommodate the largest per-item token count.
            if model_config.is_multimodal_model and model_config.is_mm_prefix_lm:
                result = self._get_min_mm_batched_tokens(model_config)
                if result is not None and result[0] > self.max_num_batched_tokens:
                    mm_min, modality = result
                    logger.info(
                        "Raising max_num_batched_tokens from %d to %d to "
                        "accommodate '%s' input for prefix-LM model %s.",
                        self.max_num_batched_tokens,
                        mm_min,
                        modality,
                        model_config.model,
                    )
                    self.max_num_batched_tokens = mm_min

            # When using default settings,
            # Ensure max_num_batched_tokens does not exceed model limit.
            # Some models (e.g., Whisper) have embeddings tied to max length.
            self.max_num_batched_tokens = min(
                self.max_num_seqs * model_config.max_model_len,
                self.max_num_batched_tokens,
            )
  1. Lines 3181-3186. Without chunked prefill, a full prompt must fit in one step, so the budget is at least max_model_len.
  2. Lines 3208-3211. The budget is at most max_num_seqs × max_model_len; the comment names Whisper.
  3. Lines 3219-3221 (after the excerpt). max_num_seqs is at most the budget (vllm/engine/arg_utils.py:3219-3221).

Each cap applies only to a value that you did not set (the orig_* checks at lines 3177 and 3219). The final values are logged at DEBUG level only (lines 3213-3227).

AsyncEngineArgs.add_cli_args#

    def add_cli_args(
        parser: FlexibleArgumentParser, async_args_only: bool = False
    ) -> FlexibleArgumentParser:
        # Initialize plugin to update the parser, for example, The plugin may
        # add a new kind of quantization method to --quantization argument or
        # a new device to --device argument.
        load_general_plugins()
        if not async_args_only:
            parser = EngineArgs.add_cli_args(parser)
        parser.add_argument(
            "--enable-log-requests",
            action=argparse.BooleanOptionalAction,
            default=AsyncEngineArgs.enable_log_requests,
            help="Enable logging request information, dependent on log level:\n"
            "- INFO: Request ID, parameters and LoRA request.\n"
            "- DEBUG: Prompt inputs (e.g: text, token IDs).\n"
            "You can set the minimum log level via `VLLM_LOGGING_LEVEL`.\n"
            "You can also set a separate log level for request JSON body "
            "via `VLLM_REQUEST_BODY_LOG_LEVEL` (default: DEBUG).",
        )
        current_platform.pre_register_and_update(parser)
        return parser

The general plugins load first, so that a plugin can add choices to a flag, for example a new quantization method (comment at lines 3240-3242). The platform hook runs last, with the finished parser. async_args_only=True skips the engine flags; no caller in vllm/ passes it (derived from a search).

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Data flow: types in and out

ValueTypeWhere
argparse settings of one classdict[str, dict[str, Any]]: field name → {"default", "help", "type", ...}vllm/engine/arg_utils.py:325
parsed flagsargparse.Namespacevllm/engine/arg_utils.py:1962
engine argumentsAsyncEngineArgs (a stdlib dataclass, not pydantic)vllm/engine/arg_utils.py:482, vllm/engine/arg_utils.py:3230
usage contextUsageContext enum, for example OPENAI_API_SERVERvllm/usage/usage_lib.py:111-117
batch defaultstuple[dict[UsageContext, int], dict[UsageContext, int]]vllm/engine/arg_utils.py:2927-2930
resultVllmConfigvllm/engine/arg_utils.py:2238

EngineArgs is a plain dataclasses.dataclass, so it does no type checks. The checks happen in the config classes that create_engine_config makes.

Deep dive

Worked example: three runs of the batch defaults

Model meta-llama/Llama-3.1-8B-Instruct with --max-model-len 8192, usage context OPENAI_API_SERVER, chunked prefill on unless stated. (Derived from vllm/engine/arg_utils.py:2960-2993 and vllm/engine/arg_utils.py:3153-3221.)

GPU and flagsTier valuesAfter the stepsResult
80 GB A100, no flags2048 tokens, 256 requeststokens = min(256 × 8192, 2048) = 2048; requests = min(256, 2048) = 2562048 / 256
80 GB A100, --no-enable-chunked-prefill2048, 256tokens = max(8192, 2048) = 8192; then min(256 × 8192, 8192) = 81928192 / 256
80 GB H100, --performance-mode throughput8192, 1024× 2 → 16384, 2048; min(2048 × 8192, 16384) = 16384; min(2048, 16384) = 204816384 / 2048

The H100 row assumes that the driver reports 70 GiB or more. A change of either value also changes the compile cache key, because SchedulerConfig.compute_hash includes both (vllm/config/scheduler.py:280-284).

Deep dive

Processes and threads

Everything in this file runs in the main thread of the process that builds the config. For vllm serve with one API server, that is the CLI process. With several API servers, the CLI process builds a config to launch the engines, and each API server process builds its own config again from the same Namespace (derived from vllm/entrypoints/cli/serve.py:307-313 and vllm/v1/utils.py:509-529). The engine core and the workers never import the parser code paths: they get the finished VllmConfig.

Deep dive

Error paths: what raises, what only warns

SituationResultWhere
JSON flag with an unknown key or a bad valueargparse.ArgumentTypeError; argparse exitsvllm/engine/arg_utils.py:369-374
A config field type that the parser does not knowValueError("Unsupported type ...") when the parser is builtvllm/engine/arg_utils.py:426-427
A deprecated flagwarning; the flag is ignored or forwardedvllm/engine/arg_utils.py:456-479
Pipeline parallelism with an unsupported executorNotImplementedErrorvllm/engine/arg_utils.py:2909-2924, vllm/engine/arg_utils.py:3261-3266
Two DP load-balancing modes, or bad --nnodesValueErrorvllm/engine/arg_utils.py:2372-2409
External DP load balancing with a non-MoE modelValueErrorvllm/engine/arg_utils.py:2445-2454
LoRA + speculative decoding with too small a token budgetValueErrorvllm/engine/arg_utils.py:2701-2713
A shortcut flag and its JSON form togetherValueError "mutually exclusive"vllm/engine/arg_utils.py:2717-2722, vllm/engine/arg_utils.py:2821-2836
Disabling chunked prefill for a model that supports itwarning onlyvllm/engine/arg_utils.py:3041-3050
Deep dive

Environment variables read in this file

NameRead atEffect
VLLM_ENABLE_V1_MULTIPROCESSINGvllm/engine/arg_utils.py:1973Only a warning about the global seed when it is off.
VLLM_BATCH_INVARIANTvllm/engine/arg_utils.py:2730-2739On XPU with no backend set: Triton attention.
HF_HUB_OFFLINE (through huggingface_hub)vllm/engine/arg_utils.py:926Replaces model and tokenizer names with local paths.

All other environment variables are read elsewhere (envs.py: environment variables as a dict of lambdas).

Check yourself#

  1. 1
    Where does the help text of a flag such as --max-model-len come from?
    easy
    Show answer

    From the docstring of the field ModelConfig.max_model_len. _compute_kwargs reads the docstrings with get_attr_docs, but only when --help is in argv (vllm/engine/arg_utils.py:327, vllm/engine/arg_utils.py:356).

  2. 2
    Why is --max-num-seqs None by default, when SchedulerConfig.max_num_seqs is 128?
    medium
    Show answer

    The flag replaces the field default with None (vllm/engine/arg_utils.py:1743-1749). None lets _set_default_max_num_seqs_and_batched_tokens_args choose a value from the usage context and the GPU (vllm/engine/arg_utils.py:3164-3168).

  3. 3
    You pass --attention-backend FLASHINFER -ac '{"backend": "FLASH_ATTN"}'. What happens?
    medium
    Show answer

    create_engine_config raises ValueError: "attention_backend and attention_config.backend are mutually exclusive" (vllm/engine/arg_utils.py:2717-2722).

  4. 4
    With --no-enable-chunked-prefill and --max-model-len 32768 on an 80 GB A100, what token budget does vllm serve use, and why?
    hard
    Show answer

    32768. The tier gives 2048 (vllm/engine/arg_utils.py:2986-2989). Without chunked prefill the budget becomes max(32768, 2048), then min(256 × 32768, 32768) (vllm/engine/arg_utils.py:3181-3211). The requests stay at min(256, 32768) = 256 (lines 3219-3221).