623 lines
21 KiB
Python
623 lines
21 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""
|
|
Configuration for distributed storage manager
|
|
"""
|
|
|
|
# Standard
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Literal, cast
|
|
import argparse
|
|
import os
|
|
|
|
# First Party
|
|
from lmcache.logging import init_logger
|
|
from lmcache.v1.distributed.l2_adapters.config import (
|
|
L2AdapterConfigBase,
|
|
L2AdaptersConfig,
|
|
add_l2_adapters_args,
|
|
get_type_name_for_config,
|
|
parse_args_to_l2_adapters_config,
|
|
)
|
|
from lmcache.v1.platform import current_device_spec
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
_HYBRID_L1_SINGLE_REGION_L2_ADAPTERS = {
|
|
"nixl_store",
|
|
"nixl_store_dynamic",
|
|
}
|
|
|
|
|
|
def _requires_single_l1_memory_region(
|
|
adapter_config: L2AdapterConfigBase,
|
|
) -> str | None:
|
|
type_name = get_type_name_for_config(adapter_config)
|
|
if type_name in _HYBRID_L1_SINGLE_REGION_L2_ADAPTERS:
|
|
return type_name
|
|
if (
|
|
type_name == "mooncake_store"
|
|
and getattr(adapter_config, "setup_config", {}).get("protocol") == "rdma"
|
|
):
|
|
return type_name
|
|
return None
|
|
|
|
|
|
def _infer_l1_devdax_overflow_from_dax_adapter(
|
|
memory_config: "L1MemoryManagerConfig",
|
|
l2_adapter_config: L2AdaptersConfig,
|
|
) -> None:
|
|
if not memory_config.devdax_path or memory_config.devdax_size_in_bytes:
|
|
return
|
|
|
|
l1_devdax_path = memory_config.devdax_path
|
|
remaining_adapters: list[L2AdapterConfigBase] = []
|
|
matched_dax_device: Any | None = None
|
|
for adapter_config in l2_adapter_config.adapters:
|
|
if get_type_name_for_config(adapter_config) != "dax":
|
|
remaining_adapters.append(adapter_config)
|
|
continue
|
|
|
|
dax_adapter = cast(Any, adapter_config)
|
|
devices = list(getattr(dax_adapter, "devices", []))
|
|
matched_devices = [
|
|
device
|
|
for device in devices
|
|
if getattr(device, "device_path", None) == l1_devdax_path
|
|
]
|
|
if not matched_devices:
|
|
remaining_adapters.append(adapter_config)
|
|
continue
|
|
if matched_dax_device is not None or len(matched_devices) > 1:
|
|
raise ValueError(
|
|
"Only one DAX device can match l1-devdax-path for hybrid L1"
|
|
)
|
|
|
|
matched_dax_device = matched_devices[0]
|
|
remaining_devices = [
|
|
device
|
|
for device in devices
|
|
if getattr(device, "device_path", None) != l1_devdax_path
|
|
]
|
|
if remaining_devices:
|
|
remaining_dax_adapter = type(dax_adapter)(
|
|
devices=remaining_devices,
|
|
hotplug_enabled=dax_adapter.hotplug_enabled,
|
|
slot_bytes=dax_adapter.slot_bytes,
|
|
num_store_workers=dax_adapter.num_store_workers,
|
|
num_lookup_workers=dax_adapter.num_lookup_workers,
|
|
num_load_workers=dax_adapter.num_load_workers,
|
|
)
|
|
remaining_dax_adapter.eviction_config = dax_adapter.eviction_config
|
|
remaining_dax_adapter.persist_config = dax_adapter.persist_config
|
|
remaining_dax_adapter.serde_config = dax_adapter.serde_config
|
|
remaining_adapters.append(remaining_dax_adapter)
|
|
|
|
if matched_dax_device is None:
|
|
return
|
|
max_dax_size_gb = matched_dax_device.max_dax_size_gb
|
|
memory_config.devdax_size_in_bytes = int(max_dax_size_gb * (1 << 30))
|
|
l2_adapter_config.adapters = remaining_adapters
|
|
|
|
|
|
@dataclass
|
|
class L1MemoryManagerConfig:
|
|
"""
|
|
The configuration for L1 memory manager.
|
|
"""
|
|
|
|
size_in_bytes: int
|
|
""" The size of L1 memory in bytes. """
|
|
|
|
use_lazy: bool
|
|
""" Whether to use lazy initialization for L1 memory. """
|
|
|
|
init_size_in_bytes: int = field(default=20 << 30)
|
|
""" The initial size when using lazy allocation. Default is 20GB. """
|
|
|
|
align_bytes: int = field(default=0x1000)
|
|
""" The alignment size in bytes. Default is 4KB. """
|
|
|
|
shm_name: str = field(default_factory=lambda: f"lmcache_l1_pool_{os.getpid()}")
|
|
""" POSIX shared-memory segment name for L1 pool. Empty disables SHM. """
|
|
|
|
devdax_path: str | None = None
|
|
""" Optional Device-DAX path to use as the L1 backing arena. """
|
|
|
|
devdax_size_in_bytes: int = 0
|
|
""" Optional Device-DAX overflow size for hybrid DRAM + DAX L1. """
|
|
|
|
def __post_init__(self):
|
|
self.init_size_in_bytes = min(self.init_size_in_bytes, self.size_in_bytes)
|
|
|
|
if self.devdax_path is not None:
|
|
self.devdax_path = self.devdax_path.strip()
|
|
|
|
if self.devdax_size_in_bytes < 0:
|
|
raise ValueError("devdax_size_in_bytes must be >= 0")
|
|
if self.devdax_size_in_bytes and not self.devdax_path:
|
|
raise ValueError("devdax_size_in_bytes requires devdax_path")
|
|
|
|
if self.devdax_path and self.use_lazy:
|
|
raise ValueError(
|
|
"l1-devdax-path requires lazy allocation to be disabled. "
|
|
"Please set --no-l1-use-lazy."
|
|
)
|
|
if self.devdax_path and self.shm_name:
|
|
raise ValueError(
|
|
'l1-devdax-path requires SHM to be disabled. Please set --shm-name "".'
|
|
)
|
|
|
|
# LazyMemoryAllocator requires pinned memory support.
|
|
# Auto-disable on platforms that don't support it to avoid a RuntimeError.
|
|
if self.use_lazy and not current_device_spec.is_pin_supported:
|
|
logger.warning(
|
|
"LazyMemoryAllocator requires memory pinning which is not "
|
|
"supported on the current backend. Disabling l1-use-lazy."
|
|
)
|
|
self.use_lazy = False
|
|
|
|
|
|
@dataclass
|
|
class GdsL1Config:
|
|
"""Configuration for the GDS slab-file L1 tier.
|
|
|
|
When present on :class:`L1ManagerConfig`, the L1 medium becomes an NVMe
|
|
slab file accessed via GPUDirect Storage DMA (cuFile on NVIDIA, hipFile on
|
|
AMD ROCm) instead of pinned DRAM (mutually exclusive with the pinned-DRAM
|
|
tier in ``memory_config``). Carries the slab location, capacity, and DMA
|
|
mode.
|
|
"""
|
|
|
|
file_location: str
|
|
"""Directory for the slab file (one shared slab per process, used by all
|
|
GPU instances)."""
|
|
|
|
size_in_bytes: int
|
|
"""Slab capacity in bytes (from ``--l1-size-gb``). Sizes both the
|
|
preallocated slab file and the GDS tier's address space."""
|
|
|
|
use_direct_io: bool = True
|
|
"""Open the slab with ``O_DIRECT`` (required for the GDS DMA fast path)."""
|
|
|
|
align_bytes: int = 4096
|
|
"""Allocation alignment; cuFile/hipFile and O_DIRECT require 4 KiB."""
|
|
|
|
|
|
@dataclass
|
|
class L1ManagerConfig:
|
|
"""
|
|
Special config for the L1 Object/Key manager
|
|
"""
|
|
|
|
memory_config: L1MemoryManagerConfig
|
|
""" The memory manager configuration for L1 cache. """
|
|
|
|
gds_l1_config: "GdsL1Config | None" = None
|
|
""" Optional GDS L1 tier. When set, the GDS slab is the L1 medium
|
|
(mutually exclusive with the pinned-DRAM tier in ``memory_config``). """
|
|
|
|
write_ttl_seconds: int = field(default=600)
|
|
""" Time to live for each object's write lock. Default is 600s (10 minutes). """
|
|
|
|
read_ttl_seconds: int = field(default=300)
|
|
""" Time to live for each object's read lock. Default is 300s (5 minutes). """
|
|
|
|
|
|
@dataclass
|
|
class EvictionConfig:
|
|
"""
|
|
The configuration for eviction policies (L1 and optionally L2).
|
|
"""
|
|
|
|
eviction_policy: Literal["LRU", "IsolatedLRU", "noop"]
|
|
""" The eviction policy to use. """
|
|
|
|
trigger_watermark: float = field(default=0.8)
|
|
""" The memory usage watermark to trigger eviction (0.0 to 1.0). """
|
|
|
|
eviction_ratio: float = field(default=0.2)
|
|
""" The fraction of *allocated* memory to evict when triggered (0.0 to 1.0). """
|
|
|
|
|
|
@dataclass
|
|
class StorageManagerConfig:
|
|
"""
|
|
The configuration for the distributed storage manager.
|
|
"""
|
|
|
|
l1_manager_config: L1ManagerConfig
|
|
""" The configuration for the L1 manager. """
|
|
|
|
eviction_config: EvictionConfig
|
|
""" The configuration for eviction policies. """
|
|
|
|
l2_adapter_config: L2AdaptersConfig = field(
|
|
default_factory=lambda: L2AdaptersConfig([])
|
|
)
|
|
""" The configuration for L2 adapters. """
|
|
|
|
store_policy: str = "default"
|
|
""" The L2 store policy name. """
|
|
|
|
prefetch_policy: str = "default"
|
|
""" The L2 prefetch policy name. """
|
|
|
|
prefetch_max_in_flight: int = 8
|
|
""" Maximum number of concurrent prefetch requests. """
|
|
|
|
periodic_notifier_interval_ms: int = 5
|
|
""" Interval (ms) for the periodic event notifier heartbeat. """
|
|
|
|
def __post_init__(self) -> None:
|
|
normalize_storage_manager_config(self)
|
|
validate_storage_manager_config(self)
|
|
|
|
|
|
def normalize_storage_manager_config(config: StorageManagerConfig) -> None:
|
|
"""Normalize storage manager configuration.
|
|
|
|
This consumes a matching DAX adapter as hybrid L1 Device-DAX overflow
|
|
capacity.
|
|
|
|
Args:
|
|
config: Storage manager configuration to normalize in place.
|
|
|
|
Returns:
|
|
None.
|
|
|
|
Raises:
|
|
ValueError: If more than one DAX device matches ``l1-devdax-path``.
|
|
"""
|
|
memory_config = config.l1_manager_config.memory_config
|
|
_infer_l1_devdax_overflow_from_dax_adapter(memory_config, config.l2_adapter_config)
|
|
|
|
|
|
def validate_storage_manager_config(config: StorageManagerConfig) -> None:
|
|
"""Validate storage manager configuration.
|
|
|
|
This rejects L2 adapters that require a single contiguous L1 memory
|
|
descriptor when hybrid L1 Device-DAX overflow is enabled.
|
|
|
|
Args:
|
|
config: Storage manager configuration to validate.
|
|
|
|
Returns:
|
|
None.
|
|
|
|
Raises:
|
|
ValueError: If mutually exclusive L1 tiers are both configured, or
|
|
hybrid L1 is paired with incompatible L2 adapters.
|
|
"""
|
|
if (
|
|
config.l1_manager_config.gds_l1_config is not None
|
|
and config.l1_manager_config.memory_config.devdax_path
|
|
):
|
|
raise ValueError("gds-l1-path cannot be used with l1-devdax-path")
|
|
|
|
memory_config = config.l1_manager_config.memory_config
|
|
if not (memory_config.devdax_path and memory_config.devdax_size_in_bytes):
|
|
return
|
|
|
|
incompatible_adapters = [
|
|
adapter_name
|
|
for adapter_config in config.l2_adapter_config.adapters
|
|
if (adapter_name := _requires_single_l1_memory_region(adapter_config))
|
|
is not None
|
|
]
|
|
if incompatible_adapters:
|
|
raise ValueError(
|
|
"Hybrid DRAM + Device-DAX L1 cannot be used with L2 adapters "
|
|
"that register a single L1 memory region: "
|
|
f"{', '.join(incompatible_adapters)}"
|
|
)
|
|
|
|
|
|
def l1_exposes_single_memory_region(config: StorageManagerConfig) -> bool:
|
|
"""Whether L1 is a single memory region a transfer channel can register.
|
|
|
|
Args:
|
|
config: Storage manager configuration to inspect.
|
|
|
|
Returns:
|
|
``True`` if L1 is a single registerable memory region, ``False`` for
|
|
GDS L1 or Device-DAX L1.
|
|
"""
|
|
l1_config = config.l1_manager_config
|
|
if l1_config.gds_l1_config is not None:
|
|
return False
|
|
if l1_config.memory_config.devdax_path:
|
|
return False
|
|
return True
|
|
|
|
|
|
def add_storage_manager_args(
|
|
parser: argparse.ArgumentParser,
|
|
) -> argparse.ArgumentParser:
|
|
"""
|
|
Add storage manager configuration arguments to an existing parser.
|
|
|
|
This function allows other modules to integrate storage manager arguments
|
|
into their own argument parsers. Arguments are organized into groups to
|
|
avoid naming conflicts with other modules.
|
|
|
|
Args:
|
|
parser: The argument parser to add arguments to.
|
|
|
|
Returns:
|
|
argparse.ArgumentParser: The same parser with storage manager
|
|
arguments added.
|
|
|
|
Example:
|
|
>>> # In another module that needs its own arguments
|
|
>>> parser = argparse.ArgumentParser(description="My Application")
|
|
>>> parser.add_argument("--my-arg", type=str)
|
|
>>> add_storage_manager_args(parser)
|
|
>>> args = parser.parse_args()
|
|
>>> config = parse_args_to_config(args)
|
|
"""
|
|
# L1 Memory Manager Config
|
|
memory_group = parser.add_argument_group(
|
|
"L1 Memory Manager", "Configuration for L1 memory manager"
|
|
)
|
|
memory_group.add_argument(
|
|
"--l1-size-gb",
|
|
type=float,
|
|
required=True,
|
|
help="The size of L1 memory in GB.",
|
|
)
|
|
memory_group.add_argument(
|
|
"--l1-use-lazy",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help="Whether to use lazy loading for L1 memory. (Default is True)",
|
|
)
|
|
memory_group.add_argument(
|
|
"--l1-init-size-gb",
|
|
type=int,
|
|
default=20,
|
|
help="The initial size (GB) when using lazy allocation. Default is 20.",
|
|
)
|
|
memory_group.add_argument(
|
|
"--l1-align-bytes",
|
|
type=int,
|
|
default=4096,
|
|
help="The alignment size in bytes. Default is 4KB (4096 bytes).",
|
|
)
|
|
memory_group.add_argument(
|
|
"--l1-devdax-path",
|
|
type=str,
|
|
default=None,
|
|
help=(
|
|
"Optional /dev/dax device or mmap-able file to use as the L1 "
|
|
"backing arena. When set, L1 lazy allocation and SHM transfer "
|
|
"advertising must be disabled because the L1 bytes live in the DAX "
|
|
'map. Set --no-l1-use-lazy and --shm-name "". '
|
|
"If a DAX L2 adapter with the same device_path is registered, "
|
|
"that adapter's max_dax_size_gb is used as L1 overflow size."
|
|
),
|
|
)
|
|
|
|
# GDS L1 tier (optional, opt-in via --gds-l1-path)
|
|
gds_group = parser.add_argument_group(
|
|
"GDS L1 tier",
|
|
"Configuration for the GDS slab-file L1 tier. Setting --gds-l1-path "
|
|
"makes the L1 medium an NVMe slab accessed via GPUDirect Storage DMA "
|
|
"(cuFile on NVIDIA, hipFile on AMD ROCm) instead of pinned DRAM; "
|
|
"--l1-size-gb then sizes the slab. Disable byte-array L2 adapters when "
|
|
"this is on.",
|
|
)
|
|
gds_group.add_argument(
|
|
"--gds-l1-path",
|
|
type=str,
|
|
default=None,
|
|
help="NVMe directory path for the GDS L1 slab. Setting this enables GDS L1.",
|
|
)
|
|
gds_group.add_argument(
|
|
"--gds-l1-use-direct-io",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help="Open the slab file with O_DIRECT (required for the GDS DMA fast "
|
|
"path on ext4). Default True.",
|
|
)
|
|
# L1 Manager Config (TTL settings)
|
|
ttl_group = parser.add_argument_group(
|
|
"L1 Manager TTL", "TTL configuration for L1 manager locks"
|
|
)
|
|
ttl_group.add_argument(
|
|
"--l1-write-ttl-seconds",
|
|
type=int,
|
|
default=600,
|
|
help="Time to live for each object's write lock. Default is 600s.",
|
|
)
|
|
ttl_group.add_argument(
|
|
"--l1-read-ttl-seconds",
|
|
type=int,
|
|
default=300,
|
|
help="Time to live for each object's read lock. Default is 300s.",
|
|
)
|
|
|
|
# Eviction Config
|
|
eviction_group = parser.add_argument_group(
|
|
"Eviction Policy", "Configuration for eviction policies"
|
|
)
|
|
eviction_group.add_argument(
|
|
"--eviction-policy",
|
|
type=str,
|
|
choices=["LRU", "IsolatedLRU", "noop"],
|
|
required=True,
|
|
help="The eviction policy to use ('LRU', 'IsolatedLRU', or 'noop'). "
|
|
"'IsolatedLRU' maintains one LRU list per cache_salt and requires "
|
|
"quotas keyed by cache_salt to be configured via the HTTP API.",
|
|
)
|
|
eviction_group.add_argument(
|
|
"--eviction-trigger-watermark",
|
|
type=float,
|
|
default=0.8,
|
|
help="The memory usage watermark to trigger eviction (0.0 to 1.0). "
|
|
"Default is 0.8.",
|
|
)
|
|
eviction_group.add_argument(
|
|
"--eviction-ratio",
|
|
type=float,
|
|
default=0.2,
|
|
help="The fraction of memory to evict when triggered (0.0 to 1.0). "
|
|
"Default is 0.2.",
|
|
)
|
|
|
|
# L2 Policies
|
|
# Import here to break circular dependency:
|
|
# config.py <-> storage_controllers (via eviction_controller)
|
|
# Safe because config.py is fully initialized by the time this
|
|
# function is called.
|
|
# First Party
|
|
from lmcache.v1.distributed.storage_controllers.prefetch_policy import (
|
|
get_registered_prefetch_policies,
|
|
)
|
|
from lmcache.v1.distributed.storage_controllers.store_policy import (
|
|
get_registered_store_policies,
|
|
)
|
|
import lmcache.v1.distributed.storage_controllers # noqa: F401
|
|
|
|
policy_group = parser.add_argument_group(
|
|
"L2 Policies", "Store and prefetch policy selection for L2 adapters"
|
|
)
|
|
policy_group.add_argument(
|
|
"--l2-store-policy",
|
|
type=str,
|
|
choices=get_registered_store_policies(),
|
|
default="default",
|
|
help="L2 store policy. Determines which adapters receive each key "
|
|
"and whether keys are deleted from L1 after L2 store. "
|
|
"Default is 'default' (store all keys to all adapters, keep L1).",
|
|
)
|
|
policy_group.add_argument(
|
|
"--l2-prefetch-policy",
|
|
type=str,
|
|
choices=get_registered_prefetch_policies(),
|
|
default="default",
|
|
help="L2 prefetch policy. Determines which adapter loads each key "
|
|
"when multiple adapters have it. "
|
|
"Default is 'default' (pick the first adapter by index).",
|
|
)
|
|
policy_group.add_argument(
|
|
"--l2-prefetch-max-in-flight",
|
|
type=int,
|
|
default=8,
|
|
help="Maximum number of concurrent prefetch requests. Default is 8.",
|
|
)
|
|
policy_group.add_argument(
|
|
"--periodic-notifier-interval-ms",
|
|
type=int,
|
|
default=5,
|
|
help="Interval in ms for the periodic event notifier heartbeat. Default is 5.",
|
|
)
|
|
|
|
# Adapter config
|
|
add_l2_adapters_args(parser)
|
|
return parser
|
|
|
|
|
|
def get_arg_parser() -> argparse.ArgumentParser:
|
|
"""
|
|
Get a standalone argument parser for storage manager configuration.
|
|
|
|
This creates a new parser with only storage manager arguments.
|
|
For integrating with other modules' parsers, use add_storage_manager_args()
|
|
instead.
|
|
|
|
Returns:
|
|
argparse.ArgumentParser: The argument parser with all storage manager
|
|
configuration options.
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Distributed Storage Manager Configuration"
|
|
)
|
|
return add_storage_manager_args(parser)
|
|
|
|
|
|
def parse_args_to_config(
|
|
args: argparse.Namespace,
|
|
) -> StorageManagerConfig:
|
|
"""
|
|
Convert parsed command line arguments to a StorageManagerConfig.
|
|
|
|
Args:
|
|
args: Parsed arguments from the argument parser.
|
|
|
|
Returns:
|
|
StorageManagerConfig: The configuration object.
|
|
"""
|
|
shm_name = getattr(args, "shm_name", None)
|
|
if shm_name is None:
|
|
memory_config = L1MemoryManagerConfig(
|
|
size_in_bytes=int(args.l1_size_gb * (1 << 30)),
|
|
use_lazy=args.l1_use_lazy,
|
|
init_size_in_bytes=int(args.l1_init_size_gb * (1 << 30)),
|
|
align_bytes=args.l1_align_bytes,
|
|
devdax_path=args.l1_devdax_path,
|
|
)
|
|
else:
|
|
memory_config = L1MemoryManagerConfig(
|
|
size_in_bytes=int(args.l1_size_gb * (1 << 30)),
|
|
use_lazy=args.l1_use_lazy,
|
|
init_size_in_bytes=int(args.l1_init_size_gb * (1 << 30)),
|
|
align_bytes=args.l1_align_bytes,
|
|
shm_name=shm_name,
|
|
devdax_path=args.l1_devdax_path,
|
|
)
|
|
|
|
gds_l1_config: GdsL1Config | None = None
|
|
if getattr(args, "gds_l1_path", None):
|
|
# --l1-size-gb is the single L1 size flag; under GDS it sizes the slab.
|
|
gds_l1_config = GdsL1Config(
|
|
file_location=args.gds_l1_path,
|
|
size_in_bytes=int(args.l1_size_gb * (1 << 30)),
|
|
use_direct_io=args.gds_l1_use_direct_io,
|
|
)
|
|
|
|
l1_manager_config = L1ManagerConfig(
|
|
memory_config=memory_config,
|
|
gds_l1_config=gds_l1_config,
|
|
write_ttl_seconds=args.l1_write_ttl_seconds,
|
|
read_ttl_seconds=args.l1_read_ttl_seconds,
|
|
)
|
|
|
|
eviction_config = EvictionConfig(
|
|
eviction_policy=args.eviction_policy,
|
|
trigger_watermark=args.eviction_trigger_watermark,
|
|
eviction_ratio=args.eviction_ratio,
|
|
)
|
|
|
|
l2_adapter_config = parse_args_to_l2_adapters_config(args)
|
|
|
|
config = StorageManagerConfig(
|
|
l1_manager_config=l1_manager_config,
|
|
eviction_config=eviction_config,
|
|
l2_adapter_config=l2_adapter_config,
|
|
store_policy=args.l2_store_policy,
|
|
prefetch_policy=args.l2_prefetch_policy,
|
|
prefetch_max_in_flight=args.l2_prefetch_max_in_flight,
|
|
periodic_notifier_interval_ms=args.periodic_notifier_interval_ms,
|
|
)
|
|
return config
|
|
|
|
|
|
def parse_args(args: list[str] | None = None) -> StorageManagerConfig:
|
|
"""
|
|
Parse command line arguments and return a StorageManagerConfig.
|
|
|
|
This is a convenience function that combines get_arg_parser() and
|
|
parse_args_to_config().
|
|
|
|
Args:
|
|
args: Optional list of arguments to parse. If None, uses sys.argv.
|
|
|
|
Returns:
|
|
StorageManagerConfig: The configuration object.
|
|
"""
|
|
parser = get_arg_parser()
|
|
parsed_args = parser.parse_args(args)
|
|
return parse_args_to_config(parsed_args)
|