chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.core.utils import numba_utils, process_launcher
+275
View File
@@ -0,0 +1,275 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import numpy as np
import torch
from packaging.version import Version
from nemo.core.utils.optional_libs import CUDA_PYTHON_AVAILABLE, cuda_python_required
from nemo.utils.exceptions import NeMoBaseException
if CUDA_PYTHON_AVAILABLE:
from cuda.bindings import __version__ as cuda_python_version
from cuda.bindings import driver as cuda
from cuda.bindings import nvrtc
from cuda.bindings import runtime as cudart
__CUDA_PYTHON_MINIMUM_VERSION_CUDA_GRAPH_CONDITIONAL_NODES_SUPPORTED__ = (12, 6) # 12060
class NeMoCUDAPythonException(NeMoBaseException):
"""Exception caused by python-cuda in NeMo"""
pass
CUDA_GRAPH_COMPILE_ERROR_TYPES = (NeMoCUDAPythonException,)
if hasattr(torch, "AcceleratorError"):
CUDA_GRAPH_COMPILE_ERROR_TYPES += (torch.AcceleratorError,)
def check_cuda_python_cuda_graphs_conditional_nodes_supported():
"""Check if CUDA and CUDA-Python are available with CUDA Graphs with conditional nodes support"""
# for CPU-only environment we need to raise an exception, otherwise cuda-python library will fail
if not torch.cuda.is_available():
raise EnvironmentError("CUDA is not available")
try:
from cuda.bindings import driver as cuda
except ImportError:
raise ModuleNotFoundError("No `cuda-python` module. Please do `pip install cuda-python>=12.3`")
from cuda.bindings import __version__ as cuda_python_version
if Version(cuda_python_version) < Version("12.3.0"):
raise ImportError(f"Found cuda-python {cuda_python_version}, but at least version 12.3.0 is needed.")
error, driver_version = cuda.cuDriverGetVersion()
if error != cuda.CUresult.CUDA_SUCCESS:
raise ImportError(f"cuDriverGetVersion() returned {cuda.cuGetErrorString(error)}")
driver_version_major = driver_version // 1000
driver_version_minor = (driver_version % 1000) // 10
driver_version = (driver_version_major, driver_version_minor)
if driver_version < __CUDA_PYTHON_MINIMUM_VERSION_CUDA_GRAPH_CONDITIONAL_NODES_SUPPORTED__:
required_version = __CUDA_PYTHON_MINIMUM_VERSION_CUDA_GRAPH_CONDITIONAL_NODES_SUPPORTED__
raise ImportError(
f"""Driver supports cuda toolkit version \
{driver_version_major}.{driver_version_minor}, but the driver needs to support \
at least {required_version[0]},{required_version[1]}. Please update your cuda driver."""
)
def skip_cuda_python_test_if_cuda_graphs_conditional_nodes_not_supported():
"""
Helper method to skip pytest test case if cuda graph conditionals nodes are not supported.
"""
try:
check_cuda_python_cuda_graphs_conditional_nodes_supported()
except (ImportError, ModuleNotFoundError, EnvironmentError) as e:
import pytest
pytest.skip(
"Test using cuda graphs with conditional nodes is being skipped because "
f"cuda graphs with conditional nodes aren't supported. Error message: {e}"
)
@cuda_python_required
def assert_drv(err):
"""
Throws an exception if the return value of a cuda-python call is not success.
"""
if isinstance(err, cuda.CUresult):
if err != cuda.CUresult.CUDA_SUCCESS:
raise NeMoCUDAPythonException("Cuda Error: {}".format(err))
elif isinstance(err, nvrtc.nvrtcResult):
if err != nvrtc.nvrtcResult.NVRTC_SUCCESS:
raise NeMoCUDAPythonException("Nvrtc Error: {}".format(err))
elif isinstance(err, cudart.cudaError_t):
if err != cudart.cudaError_t.cudaSuccess:
raise NeMoCUDAPythonException("Cuda Runtime Error: {}".format(err))
else:
raise NeMoCUDAPythonException("Unknown error type: {}".format(err))
@cuda_python_required
def cu_call(f_call_out):
"""
Makes calls to cuda-python's functions inside cuda.cuda more python by throwing an exception
if they return a status which is not cudaSuccess
"""
error, *others = f_call_out
if error != cudart.cudaError_t.cudaSuccess:
raise NeMoCUDAPythonException(f"CUDA failure! {error}")
else:
return tuple(others)
@contextlib.contextmanager
@cuda_python_required
def with_conditional_node(while_loop_kernel, while_loop_args, while_loop_conditional_handle, device):
"""
Even though we add a conditional node only once, we need to
capture the kernel that calls cudaGraphSetConditional() both
before in the parent graph containing the while loop body graph
and after the rest of the while loop body graph (because we need
to decide both whether to enter the loop, and also whether to
execute the next iteration of the loop).
"""
# NB: depending on cuda-python version, cudaStreamGetCaptureInfo can return either 5 or 6 elements
capture_status, _, graph, *_ = cu_call(
cudart.cudaStreamGetCaptureInfo(torch.cuda.current_stream(device=device).cuda_stream)
)
assert capture_status == cudart.cudaStreamCaptureStatus.cudaStreamCaptureStatusActive
cuda.cuLaunchKernel(
while_loop_kernel,
1,
1,
1,
1,
1,
1,
0,
torch.cuda.current_stream(device=device).cuda_stream,
while_loop_args.ctypes.data,
0,
)
# NB: depending on cuda-python version, cudaStreamGetCaptureInfo can return either 5 or 6 elements
capture_status, _, graph, dependencies, *_ = cu_call(
cudart.cudaStreamGetCaptureInfo(torch.cuda.current_stream(device=device).cuda_stream)
)
assert capture_status == cudart.cudaStreamCaptureStatus.cudaStreamCaptureStatusActive
driver_params = cuda.CUgraphNodeParams()
driver_params.type = cuda.CUgraphNodeType.CU_GRAPH_NODE_TYPE_CONDITIONAL
driver_params.conditional.handle = while_loop_conditional_handle
driver_params.conditional.type = cuda.CUgraphConditionalNodeType.CU_GRAPH_COND_TYPE_WHILE
driver_params.conditional.size = 1
if Version(cuda_python_version) == Version("12.3.0"):
# Work around for https://github.com/NVIDIA/cuda-python/issues/55
# Originally, cuda-python version 12.3.0 failed to allocate phGraph_out
# on its own.
# This bug is fixed in cuda-python version 12.4.0. In fact, we can
# no longer write to phGraph_out in cuda-python 12.4.0, so we must
# condition on the version number.
driver_params.conditional.phGraph_out = [cuda.CUgraph()]
(ctx,) = cu_call(cuda.cuCtxGetCurrent())
driver_params.conditional.ctx = ctx
# Use driver API here because of bug in cuda-python runtime API: https://github.com/NVIDIA/cuda-python/issues/55
# TODO: Change call to this after fix goes in (and we bump minimum cuda-python version to 12.4.0):
# node, = cu_call(cudart.cudaGraphAddNode(graph, dependencies, len(dependencies), driver_params))
# CUDA 13 (cuda-python >= 13.0.0) adds an edgeData parameter to cuGraphAddNode and
# cudaStreamUpdateCaptureDependencies; CUDA 12 does not accept it.
_cuda13 = Version(cuda_python_version) >= Version("13.0.0")
if _cuda13:
(node,) = cu_call(cuda.cuGraphAddNode(graph, dependencies, None, len(dependencies), driver_params))
else:
(node,) = cu_call(cuda.cuGraphAddNode(graph, dependencies, len(dependencies), driver_params))
body_graph = driver_params.conditional.phGraph_out[0]
if _cuda13:
cu_call(
cudart.cudaStreamUpdateCaptureDependencies(
torch.cuda.current_stream(device=device).cuda_stream,
[node],
None,
1,
cudart.cudaStreamUpdateCaptureDependenciesFlags.cudaStreamSetCaptureDependencies,
)
)
else:
cu_call(
cudart.cudaStreamUpdateCaptureDependencies(
torch.cuda.current_stream(device=device).cuda_stream,
[node],
1,
cudart.cudaStreamUpdateCaptureDependenciesFlags.cudaStreamSetCaptureDependencies,
)
)
body_stream = torch.cuda.Stream(device)
previous_stream = torch.cuda.current_stream(device=device)
body_capture_active = False
cu_call(
cudart.cudaStreamBeginCaptureToGraph(
body_stream.cuda_stream,
body_graph,
None,
None,
0,
cudart.cudaStreamCaptureMode.cudaStreamCaptureModeThreadLocal,
)
)
body_capture_active = True
try:
torch.cuda.set_stream(body_stream)
yield body_stream, body_graph
cuda.cuLaunchKernel(
while_loop_kernel, 1, 1, 1, 1, 1, 1, 0, body_stream.cuda_stream, while_loop_args.ctypes.data, 0
)
end_capture_out = cudart.cudaStreamEndCapture(body_stream.cuda_stream)
body_capture_active = False
cu_call(end_capture_out)
finally:
if body_capture_active:
try:
end_capture_out = cudart.cudaStreamEndCapture(body_stream.cuda_stream)
body_capture_active = False
cu_call(end_capture_out)
except Exception:
pass
torch.cuda.set_stream(previous_stream)
@cuda_python_required
def run_nvrtc(kernel_string: str, kernel_name: bytes, program_name: bytes):
"""Run CUDA kernel using CUDA-Python"""
err, prog = nvrtc.nvrtcCreateProgram(str.encode(kernel_string), program_name, 0, [], [])
assert_drv(err)
# Compile program
# Not specifying --gpu-architecture will default us to a fairly low compute capability, which is a safe bet.
# Otherwise, there are ways to query the current device's compute capability.
# https://stackoverflow.com/questions/48283009/nvcc-get-device-compute-capability-in-runtime
opts = []
(err,) = nvrtc.nvrtcCompileProgram(prog, len(opts), opts)
assert_drv(err)
err, size = nvrtc.nvrtcGetProgramLogSize(prog)
assert_drv(err)
buf = b" " * size
(err,) = nvrtc.nvrtcGetProgramLog(prog, buf)
assert_drv(err)
# Get PTX from compilation
err, ptxSize = nvrtc.nvrtcGetPTXSize(prog)
assert_drv(err)
ptx = b" " * ptxSize
(err,) = nvrtc.nvrtcGetPTX(prog, ptx)
assert_drv(err)
ptx = np.char.array(ptx)
err, module = cuda.cuModuleLoadData(ptx.ctypes.data)
assert_drv(err)
err, kernel = cuda.cuModuleGetFunction(module, kernel_name)
assert_drv(err)
return kernel
+53
View File
@@ -0,0 +1,53 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Guard for importing optional NeMo dependency k2.
Contains checks for k2 availability and version.
Use `from nemo.core.utils.k2_guard import k2` to import k2 instead of direct import.
If there is an error, the module will raise an exception with a helpful message.
"""
import textwrap
from lightning.pytorch.utilities.imports import package_available
from packaging.version import Version
from nemo.core.utils.k2_utils import K2_INSTALLATION_MESSAGE
__K2_MINIMUM_MAJOR_VERSION = 1
__K2_MINIMUM_MINOR_VERSION = 14
__K2_MINIMUM_VERSION = Version(f"{__K2_MINIMUM_MAJOR_VERSION}.{__K2_MINIMUM_MINOR_VERSION}")
if not package_available("k2"):
raise ModuleNotFoundError("Module k2 is not available.\n" + K2_INSTALLATION_MESSAGE)
import k2 # noqa: E402
try:
__k2_version = Version(k2.__dev_version__)
except AttributeError:
raise ImportError("Module k2 is corrupted.\n" + K2_INSTALLATION_MESSAGE)
if __k2_version < __K2_MINIMUM_VERSION:
raise ImportError(
textwrap.dedent(
f"""
Minimum required k2 version: {__K2_MINIMUM_VERSION};
Installed k2 version: {__k2_version}
"""
)
)
+24
View File
@@ -0,0 +1,24 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
K2_INSTALLATION_MESSAGE = (
"Could not import `k2`.\n"
"Please install k2 in one of the following ways:\n"
"1) (recommended) Run `bash scripts/installers/install_k2.sh`\n"
"2) Use any approach from https://k2-fsa.github.io/k2/installation/index.html "
"if your your cuda and pytorch versions are supported.\n"
"It is advised to always install k2 using setup.sh only, "
"as different versions of k2 may not interact with the NeMo code as expected."
)
+82
View File
@@ -0,0 +1,82 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import defaultdict
from typing import Dict, List, Optional
import torch
from nemo.core.neural_types import AxisKind, NeuralType
def get_io_names(types: Optional[Dict[str, NeuralType]], disabled_names: List[str]) -> List[str]:
if types is None:
return []
names = list(types.keys())
for name in disabled_names:
if name in names:
names.remove(name)
return names
def extract_dynamic_axes(name: str, ntype: NeuralType):
"""
This method will extract BATCH and TIME dimension ids from each provided input/output name argument.
For example, if module/model accepts argument named "input_signal" with type corresponding to [Batch, Time, Dim]
shape, then the returned result should contain "input_signal" -> [0, 1] because Batch and Time are dynamic axes
as they can change from call to call during inference.
Args:
name: Name of input or output parameter
ntype: Corresponding Neural Type
Returns:
"""
def unpack_nested_neural_type(neural_type):
if type(neural_type) in (list, tuple):
return unpack_nested_neural_type(neural_type[0])
return neural_type
dynamic_axes = defaultdict(list)
if type(ntype) in (list, tuple):
ntype = unpack_nested_neural_type(ntype)
if ntype.axes:
for ind, axis in enumerate(ntype.axes):
if axis.kind in [AxisKind.Batch, AxisKind.Time, AxisKind.Width, AxisKind.Height]:
dynamic_axes[name].append(ind)
return dynamic_axes
def get_dynamic_axes(types, names, use_dynamo=False):
dynamic_axes = defaultdict(list)
if names is not None:
for name in names:
if name in types:
dynamic_axes.update(extract_dynamic_axes(name, types[name]))
if use_dynamo:
dynamic_shapes = {}
batch = torch.export.Dim("batch")
for name, dims in dynamic_axes.items():
ds = {}
for d in dims:
if d == 0:
ds[d] = batch
# this currently has issues: https://github.com/pytorch/pytorch/issues/126127
else:
ds[d] = torch.export.Dim(name + '__' + str(d))
dynamic_shapes[name] = ds
dynamic_axes = dynamic_shapes
return dynamic_axes
+195
View File
@@ -0,0 +1,195 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import logging as pylogger
import operator
import os
from typing import Tuple, Union
from nemo.utils import model_utils
# Prevent Numba CUDA logs from showing at info level
cuda_logger = pylogger.getLogger('numba.cuda.cudadrv.driver')
cuda_logger.setLevel(pylogger.ERROR) # only show error
__NUMBA_DEFAULT_MINIMUM_VERSION__ = "0.53.0"
__NUMBA_MINIMUM_VERSION__ = os.environ.get("NEMO_NUMBA_MINVER", __NUMBA_DEFAULT_MINIMUM_VERSION__)
__NUMBA_MINIMUM_VERSION_FP16_SUPPORTED__ = "0.57.0"
NUMBA_INSTALLATION_MESSAGE = (
"Could not import `numba`.\n"
"Please install numba in one of the following ways."
"1) If using conda, simply install it with conda using `conda install -c numba numba`\n"
"2) If using pip (not recommended), `pip install --upgrade numba`\n"
"followed by `export NUMBAPRO_LIBDEVICE='/usr/local/cuda/nvvm/libdevice/'` and \n"
"`export NUMBAPRO_NVVM='/usr/local/cuda/nvvm/lib64/libnvvm.so'`.\n"
"It is advised to always install numba using conda only, "
"as pip installations might interfere with other libraries such as llvmlite.\n"
"If pip install does not work, you can also try adding `--ignore-installed` to the pip command,\n"
"but this is not advised."
)
STRICT_NUMBA_COMPAT_CHECK = True
# Get environment key if available
if 'STRICT_NUMBA_COMPAT_CHECK' in os.environ:
check_str = os.environ.get('STRICT_NUMBA_COMPAT_CHECK')
check_bool = str(check_str).lower() in ("yes", "true", "t", "1")
STRICT_NUMBA_COMPAT_CHECK = check_bool
def is_numba_compat_strict() -> bool:
"""
Returns strictness level of numba cuda compatibility checks.
If value is true, numba cuda compatibility matrix must be satisfied.
If value is false, only cuda availability is checked, not compatibility.
Numba Cuda may still compile and run without issues in such a case, or it may fail.
"""
return STRICT_NUMBA_COMPAT_CHECK
def set_numba_compat_strictness(strict: bool):
"""
Sets the strictness level of numba cuda compatibility checks.
If value is true, numba cuda compatibility matrix must be satisfied.
If value is false, only cuda availability is checked, not compatibility.
Numba Cuda may still compile and run without issues in such a case, or it may fail.
Args:
strict: bool value, whether to enforce strict compatibility checks or relax them.
"""
global STRICT_NUMBA_COMPAT_CHECK
STRICT_NUMBA_COMPAT_CHECK = strict
@contextlib.contextmanager
def with_numba_compat_strictness(strict: bool):
"""Context manager for setting numba compatibility checks temporary"""
initial_strictness = is_numba_compat_strict()
set_numba_compat_strictness(strict=strict)
yield
set_numba_compat_strictness(strict=initial_strictness)
def numba_cpu_is_supported(min_version: str) -> bool:
"""
Tests if an appropriate version of numba is installed.
Args:
min_version: The minimum version of numba that is required.
Returns:
bool, whether numba CPU supported with this current installation or not.
"""
module_available, msg = model_utils.check_lib_version('numba', checked_version=min_version, operator=operator.ge)
# If numba is not installed
if module_available is None:
return False
else:
return True
def numba_cuda_is_supported(min_version: str) -> bool:
"""
Tests if an appropriate version of numba is installed, and if it is,
if cuda is supported properly within it.
Args:
min_version: The minimum version of numba that is required.
Returns:
bool, whether cuda is supported with this current installation or not.
"""
module_available = numba_cpu_is_supported(min_version)
# If numba is not installed
if module_available is None:
return False
# If numba version is installed and available
if module_available is True:
from numba import cuda
try:
cuda_available = cuda.is_available()
if cuda_available:
cuda_compatible = cuda.cudadrv.runtime.get_version()[0] in (12, 13)
else:
cuda_compatible = False
if is_numba_compat_strict():
return cuda_available and cuda_compatible
else:
return cuda_available
except Exception:
# dlopen(libcudart.dylib) might fail if CUDA was never installed in the first place.
return False
else:
return False
def is_numba_cuda_fp16_supported(return_reason: bool = False) -> Union[bool, Tuple[bool, str]]:
"""
Utility method that returns a bool, stating if FP16 is supported for numba cuda kernels or not.
Returns:
bool, whether Numba CUDA will support fp16 or not.
"""
reason = ""
use_nvidia_binding = os.environ.get('NUMBA_CUDA_USE_NVIDIA_BINDING', None)
if use_nvidia_binding is not None:
use_nvidia_binding = use_nvidia_binding.lower() == "1"
reason += "Env variable `NUMBA_CUDA_USE_NVIDIA_BINDING` is available and set to `1`. "
else:
use_nvidia_binding = False
reason += "Env variable `NUMBA_CUDA_USE_NVIDIA_BINDING` is not available or has not set to `1`."
numba_fp16_version_correct = model_utils.check_lib_version(
'numba', __NUMBA_MINIMUM_VERSION_FP16_SUPPORTED__, operator=operator.ge
)[0]
if numba_fp16_version_correct:
reason += "Numba CUDA FP16 is supported in installed numba version."
else:
reason += "Numba CUDA FP16 is not supported in installed numba version."
result = use_nvidia_binding and numba_fp16_version_correct
if return_reason:
return result, reason
else:
return result
def skip_numba_cuda_test_if_unsupported(min_version: str):
"""
Helper method to skip pytest test case if numba cuda is not supported.
Args:
min_version: The minimum version of numba that is required.
"""
numba_cuda_support = numba_cuda_is_supported(min_version)
if not numba_cuda_support:
import pytest
pytest.skip(f"Numba cuda test is being skipped. Minimum version required : {min_version}")
+137
View File
@@ -0,0 +1,137 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Module with guards for optional libraries, that cannot be listed in `requirements.txt`.
Provides helper constants and decorators to check if the library is available in the system.
"""
__all__ = [
# kenlm
"KENLM_AVAILABLE",
"kenlm_required",
# k2
"K2_AVAILABLE",
"k2_required",
# triton
"TRITON_AVAILABLE",
"triton_required",
# cuda-python
"CUDA_PYTHON_AVAILABLE",
"cuda_python_required",
# numba
"NUMBA_AVAILABLE",
"numba_required",
# numba-cuda
"NUMBA_CUDA_AVAILABLE",
"numba_cuda_required",
]
import importlib.util
from functools import wraps
from packaging.version import Version
from nemo.core.utils.k2_utils import K2_INSTALLATION_MESSAGE
from nemo.core.utils.numba_utils import __NUMBA_MINIMUM_VERSION__, numba_cpu_is_supported, numba_cuda_is_supported
def is_lib_available(name: str) -> bool:
"""
Checks if the library/package with `name` is available in the system
NB: try/catch with importlib.import_module(name) requires importing the library, which can be slow.
So, `find_spec` should be preferred
"""
return importlib.util.find_spec(name) is not None
KENLM_AVAILABLE = is_lib_available("kenlm")
KENLM_INSTALLATION_MESSAGE = "Try installing kenlm with `pip install kenlm`"
TRITON_AVAILABLE = is_lib_available("triton")
TRITON_INSTALLATION_MESSAGE = "Try installing triton with `pip install triton`"
NUMBA_AVAILABLE = numba_cpu_is_supported(__NUMBA_MINIMUM_VERSION__)
NUMBA_INSTALLATION_MESSAGE = (
"Numba is not found. Install with `pip install numba`. "
"For GPU support install with `pip install numba-cuda[cu12]` or `pip install numba-cuda[cu13]`"
)
NUMBA_CUDA_AVAILABLE = numba_cuda_is_supported(__NUMBA_MINIMUM_VERSION__)
NUMBA_CUDA_INSTALLATION_MESSAGE = (
"Numba with GPU support is not available. "
"For GPU support install with `pip install numba-cuda[cu12]` or `pip install numba-cuda[cu13]`"
)
try:
from nemo.core.utils.k2_guard import k2 as _ # noqa: F401
K2_AVAILABLE = True
except (ImportError, ModuleNotFoundError):
K2_AVAILABLE = False
try:
from cuda.bindings import __version__ as cuda_python_version
if Version(cuda_python_version) >= Version("12.6.0"):
CUDA_PYTHON_AVAILABLE = True
else:
CUDA_PYTHON_AVAILABLE = False
except (ImportError, ModuleNotFoundError):
CUDA_PYTHON_AVAILABLE = False
CUDA_PYTHON_INSTALLATION_MESSAGE = "Try installing cuda-python with `pip install cuda-python>=12.6.0`"
def identity_decorator(f):
"""Identity decorator for further using in conditional decorators"""
return f
def _lib_required(is_available: bool, name: str, message: str | None = None):
"""
Decorator factory. Returns identity decorator if lib `is_available`,
otherwise returns a decorator which returns a function that raises an error when called.
Such decorator can be used for conditional checks for optional libraries in functions and methods
with zero computational overhead.
"""
if is_available:
return identity_decorator
# return wrapper that will raise an error when the function is called
def function_stub_with_error_decorator(f):
"""Decorator that replaces the function and raises an error when called"""
@wraps(f)
def wrapper(*args, **kwargs):
error_msg = f"Module {name} required for the function {f.__name__} is not found."
if message:
error_msg += f" {message}"
raise ModuleNotFoundError(error_msg)
return wrapper
return function_stub_with_error_decorator
kenlm_required = _lib_required(is_available=KENLM_AVAILABLE, name="kenlm", message=KENLM_INSTALLATION_MESSAGE)
triton_required = _lib_required(is_available=TRITON_AVAILABLE, name="triton", message=TRITON_INSTALLATION_MESSAGE)
k2_required = _lib_required(is_available=K2_AVAILABLE, name="k2", message=K2_INSTALLATION_MESSAGE)
cuda_python_required = _lib_required(
is_available=CUDA_PYTHON_AVAILABLE, name="cuda_python", message=CUDA_PYTHON_INSTALLATION_MESSAGE
)
numba_required = _lib_required(is_available=NUMBA_AVAILABLE, name="numba", message=NUMBA_INSTALLATION_MESSAGE)
numba_cuda_required = _lib_required(
is_available=NUMBA_CUDA_AVAILABLE, name="numba-cuda", message=NUMBA_CUDA_INSTALLATION_MESSAGE
)
@@ -0,0 +1,15 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.core.utils.process_launcher.launcher import ProcessLauncher, ProcessLauncherConfig
@@ -0,0 +1,386 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import hashlib
import os
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
import torch
from hydra.core.config_store import ConfigStore
from hydra.core.hydra_config import HydraConfig
from hydra.core.plugins import Plugins
from hydra.core.singleton import Singleton
from hydra.core.utils import JobReturn, JobStatus, configure_log, filter_overrides, setup_globals
from hydra.plugins.launcher import Launcher
from hydra.types import HydraContext, TaskFunction
from omegaconf import DictConfig, OmegaConf, open_dict
from nemo.utils import logging
# monkey-patch hydra func
def is_in_toplevel_plugins_module(*args, **kwargs) -> bool:
"""Treat NeMo launcher plugins as top-level Hydra plugins."""
return True
# Monkey-patch Hydra
Plugins.instance().is_in_toplevel_plugins_module = is_in_toplevel_plugins_module
@dataclass
class ProcessLauncherConfig:
"""Configuration for the NeMo process launcher."""
_target_: str = "nemo.core.utils.process_launcher.launcher.ProcessLauncher"
num_gpus: int = -1
jobs_per_gpu: int = 1
def execute_job(
idx: int,
overrides: Sequence[str],
hydra_context: HydraContext,
config: DictConfig,
singleton_state: Dict[Any, Any],
gpu_idx: int,
):
"""
Creates a process that launches a "single" job that is identical in config + updated with sweep hyperparams.
Since a different process is being used, CUDA can work in non-ddp mode without issue.
Attempting ddp when using this script will not work as ddp cannot be used in shared contexts.
Args:
idx: Global index of the job.
overrides: List of str overrides that correspond to this job
hydra_context: Hydra Context used to load the sweep params into the global config
config: Global config that will be updated with sweep hyper parameters.
singleton_state: Hydra state.
gpu_idx: The GPU ID on which this process will be run.
Returns:
- The Process object that corresponds to this sweep
- The JobReturn object holding some metadata about this run
"""
# Required by Hydra (lookup other Hydra Launchers for details)
setup_globals()
Singleton.set_state(singleton_state)
# Update base config with overrides to create sweep config
sweep_config = hydra_context.config_loader.load_sweep_config(config, list(overrides))
with open_dict(sweep_config):
sweep_config.hydra.job.id = "{}_{}".format(sweep_config.hydra.job.name, idx)
sweep_config.hydra.job.num = idx
HydraConfig.instance().set_config(sweep_config)
# Setup a directory where the config will temporarily be stored.
script_path = os.path.join(os.getcwd(), sys.argv[0])
script_path = os.path.abspath(script_path)
hash_salt = "|".join([script_path, str(OmegaConf.to_yaml(config))]).encode('utf-8')
hash_val = hashlib.sha256(hash_salt).hexdigest()
config_dir = os.path.join(os.getcwd(), "hydra_cfg", str(hash_val))
if not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
task_cfg = copy.deepcopy(sweep_config)
# Remove hydra from sweep config
# This is done to prevent recursive call to multirun launcher in Hydra.
with open_dict(task_cfg):
task_cfg.pop('hydra', '')
# Save the current jobs config to directory
temp_config_name = f"config_{idx}.yaml"
temp_config = os.path.join(config_dir, temp_config_name)
OmegaConf.save(task_cfg, temp_config)
# Compute the overides as a dict
overrides = OmegaConf.to_container(config.hydra.overrides.task)
# Check and replace trainer.devices in config with gpu_idx
found_devices = False
gpu_override = f'trainer.devices=[{gpu_idx}]'
for oidx, val in enumerate(overrides):
if 'trainer.devices' in val:
overrides[oidx] = gpu_override
found_devices = True
if not found_devices:
overrides.append(gpu_override)
# Build launch command
# Note: We depend on PTL doing the right thing since this command has global visibility of all CUDA_VISIBLE_DEVICES
cmd = [
'python',
script_path,
"--config-path",
config_dir,
"--config-name",
temp_config_name,
*overrides,
]
# Launch the subprocess; pipe the stderr
# NOTE: If this hangs due to some reason after prolonged training, it means that the stderr pipe buffer
# has become full at the OS level and we need to explicitly empty it (either parallel thread or manually
# call proc.communicate(). It should not happen in general case as stderr is filled only in case retcode != 0
# If it does happen though, implement the code here
# https://stackoverflow.com/questions/39607172/python-subprocess-popen-poll-seems-to-hang-but-communicate-works
proc = subprocess.Popen(cmd, stderr=subprocess.PIPE)
# Setup data thread for stderr
std_error_buffer = []
# Trivial thread just reads lines from stdout into the list
drainerthread = threading.Thread(target=std_error_buffer.extend, args=(proc.stderr,))
drainerthread.daemon = True
drainerthread.start()
# Construct JobReturn object for Hydra
res = JobReturn()
res.cfg = task_cfg
res.overrides = overrides
res.hydra_cfg = config
res.working_dir = os.getcwd()
res.return_value = None
return proc, res, (std_error_buffer, drainerthread)
def launch(
launcher,
job_overrides: Sequence[Sequence[str]],
initial_job_idx: int,
) -> Sequence[JobReturn]:
"""
Args:
launcher: Reference to the Launched subclass
job_overrides: A List of List<String>, where each inner list is the arguments for one job run
initial_job_idx: Initial job idx in batch
Returns:
A list of JobReturn objects.
"""
# Needed for Hydra, lookup JoblibLauncher in Hydra
setup_globals()
assert launcher.config is not None
assert launcher.task_function is not None
assert launcher.hydra_context is not None
configure_log(launcher.config.hydra.hydra_logging, launcher.config.hydra.verbose)
sweep_dir = Path(str(launcher.config.hydra.sweep.dir))
sweep_dir.mkdir(parents=True, exist_ok=True)
# Extraact the runner's config (its actually a DictConfig, but type is used for autocomplete)
runner_cfg = launcher.runner # type: ProcessLauncherConfig
logging.info(
"ProcessLauncher({}) is launching {} jobs".format(
",".join([f"{k}={v}" for k, v in runner_cfg.items()]),
len(job_overrides),
)
)
logging.info("Launching jobs, sweep output dir : {}".format(sweep_dir))
for idx, overrides in enumerate(job_overrides):
logging.info("\t#{} : {}".format(idx, " ".join(filter_overrides(overrides))))
# Needed by Hydra
singleton_state = Singleton.get_state()
# Process the runner's config to build up the multiplex config
num_gpus = runner_cfg.get('num_gpus', -1)
jobs_per_gpu = runner_cfg.get('jobs_per_gpu', 1)
# Only GPUs are supported for now.
if num_gpus <= 0:
if torch.cuda.is_available():
num_gpus = torch.cuda.device_count()
else:
raise ValueError(f"{launcher.__class__.__name__} only supports GPU operations.")
# Setup arguments for multiplex runner
overrides = list(job_overrides)
num_overrides = len(overrides)
job_idx = 0
batch_size = num_gpus * jobs_per_gpu
gpu_idx = 0
ret = [] # List of returned JobResult
subprocess_list = [] # Buffer of subprocess
results = [] # Buffer of JobResult
# STD ERROR cache
std_error_buffers = [] # type: List[List[str]]
std_error_threads = [] # type: threading.Thread
# Run over all job combinations
while job_idx < num_overrides:
# Fill up subprocess buffer while its size is smaller than multiplex batch size
while len(subprocess_list) < batch_size:
# If we run out of jobs, stop trying to submit more jobs
if job_idx >= num_overrides:
break
# Submit a job as a new process
process, res, error_tup = execute_job(
initial_job_idx + job_idx,
overrides[job_idx],
launcher.hydra_context,
launcher.config,
singleton_state,
gpu_idx % num_gpus, # This will evenly distribute GPU load
)
# Store the subprocesses and JobResults
subprocess_list.append(process)
results.append(res)
# Manage stderror thread data
std_error_buffers.append(error_tup[0])
std_error_threads.append(error_tup[1])
job_idx += 1
gpu_idx += 1
# Poll for samples in batch to finish.
if len(subprocess_list) > 0:
finished_processes = [0] * len(subprocess_list)
# Check if all processes are completed or not
# This is busy waiting, this is actually quite necessary
# Turns out that when you do proc.communicate(), you block all other threads immediately.
# IE they may fill up their buffers entirely, and hang while they wait for the first thread
# who called communicate() to finish its work or crash.
# Effectively it entirely stops multiprocessing jobs or multiplexed runs.
# Must poll and busy wait to keep threads alive, along with drain the pipes with thread buffers.
while sum(finished_processes) < len(subprocess_list):
# Check all processes to make sure they have a retcode (doesnt matter yet if 0 or not)
for proc_idx, proc in enumerate(subprocess_list):
# poll() is cheaper op than communicate()
retcode = proc.poll()
if retcode is not None:
# Log that the process with some ID has finished
if finished_processes[proc_idx] == 0:
logging.info(f"Processed job : {len(ret) + proc_idx} :: Ret code = {retcode}")
finished_processes[proc_idx] = 1
# Join this thread and merge its stderror buffer
proc.wait()
std_error_threads[proc_idx].join()
error_data = std_error_buffers[proc_idx]
error_data = [
str(data, encoding='utf-8').encode('utf-8').decode('utf-8').encode('utf-8')
for data in error_data
]
std_error_buffers[proc_idx] = error_data
time.sleep(1.0)
# Process all the subprocess results
for proc_idx, (proc, res) in enumerate(zip(subprocess_list, results)):
# Wait until completion of process
output, error = proc.communicate()
# 0 is for successful run
if proc.returncode == 0:
res.status = JobStatus.COMPLETED
else:
# > 0 is for error, log the error.
# Note: For the sake of efficiency while we log the error and raise an exception,
# It will only raise the 1st wrong job in all the jobs.
# If multiple jobs fail, it will still try to execute every job first before
# raising the error for the first one.
# This is done so that even if some jobs fail (say OOM or something),
# other jobs can still run.
err_buffer = std_error_buffers[proc_idx]
if isinstance(err_buffer, (list, tuple)):
err_string = ""
for err_line in err_buffer:
err_string = (
err_string + f"{str(err_line, encoding='utf-8').encode('utf-8').decode('utf-8')}"
)
else:
err_string = err_buffer
error_msg = (
f"\nHyperparameter Arguments : {proc.args}\n"
f"Process Return code : {proc.returncode}\n"
f"Error Trace :\n"
f"{err_string}"
)
res.return_value = Exception(error_msg)
res.status = JobStatus.FAILED
logging.info(f"Finished executing job : {len(ret)}. Return Code = {proc.returncode}")
ret.append(res)
# Reset for next batch
subprocess_list.clear()
results.clear()
return ret
class ProcessLauncher(Launcher):
"""Hydra launcher that multiplexes jobs across local GPU processes."""
def __init__(self, **kwargs: Any) -> None:
"""Process Launcher
Based on the JoblibLauncher, but uses processes to scatter jobs in a multiplexed manner across
some number of GPUs on a single machine.
"""
self.config: Optional[DictConfig] = None
self.task_function: Optional[TaskFunction] = None
self.hydra_context: Optional[HydraContext] = None
self.runner = kwargs # type: ProcessLauncherConfig
def setup(
self,
*,
hydra_context: HydraContext,
task_function: TaskFunction,
config: DictConfig,
) -> None:
"""Store Hydra launch context and task function."""
self.config = config
self.task_function = task_function
self.hydra_context = hydra_context
def launch(self, job_overrides: Sequence[Sequence[str]], initial_job_idx: int) -> Sequence[JobReturn]:
"""Launch jobs with the configured process launcher."""
return launch(launcher=self, job_overrides=job_overrides, initial_job_idx=initial_job_idx)
ConfigStore.instance().store(
group="hydra/launcher",
name="nemo_launcher",
node=ProcessLauncherConfig,
provider="nemo_process_launcher",
)
Plugins.instance().register(ProcessLauncher)