Skip to content

Commit d717605

Browse files
mdboomrwgk
andauthored
Fix nvbug6084457: Make NVLINK_MAX_LINKS version-dependent (#2192)
* Improve NVML_NVLINK_MAX_LINKS dynamic handling * Fix bug * Fix bug * Vendor the Deprecated library * Fix error message * Cleanup * Document NVLink API lifecycle changes 1. Review finding: the 1.1.0 release notes did not mention the new Device.get_nvlink_count and Device.get_nvlinks APIs, the changed Device.get_nvlink validation behavior, or the NvlinkInfo.max_links deprecation.\n\n2. Suggested fix implemented in this commit: add release-note entries for device-specific NVLink enumeration, the stricter get_nvlink ValueError behavior, and the deprecated NvlinkInfo.max_links replacement path. * Update AGENTS.md instructions * Apply suggestion from @mdboom * Update AGENTS.md as suggested in the PR --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent 5e9911a commit d717605

13 files changed

Lines changed: 523 additions & 24 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ repos:
3232
language: python
3333
additional_dependencies:
3434
- https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl
35-
exclude: '(.*pixi\.lock)|(\.git_archival\.txt)|(.*\.patch$)'
35+
exclude: '(.*pixi\.lock)|(\.git_archival\.txt)|(.*\.patch$)|(^cuda_core/cuda/core/_vendored/)'
3636
args: ["--fix"]
3737

3838
- id: no-markdown-in-docs-source
@@ -111,6 +111,7 @@ repos:
111111
alias: mypy-cuda-core
112112
name: mypy-cuda-core
113113
files: ^cuda_core/cuda/.*\.(py|pyi)$
114+
exclude: ^cuda_core/cuda/core/_vendored/
114115
pass_filenames: false
115116
args: [--config-file=cuda_core/pyproject.toml, cuda_core/cuda/core]
116117
additional_dependencies:

cuda_core/AGENTS.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,104 @@ so that they are documented but don't appear in the main index.
145145
### API stability
146146

147147
Reviews should point out where existing public APIs are broken.
148+
149+
### API lifecycle and deprecations
150+
151+
`cuda.core` follows SemVer (see `docs/source/support.rst`):
152+
153+
- **New APIs** may be added at any time (`x.Y.0`). They MUST have a
154+
`@versionadded` decorator, unless the docstring formatting requires it to be
155+
manually-specified.
156+
- **Breaking removals** only happen in **major releases** (`X.0.0`).
157+
- Per the support policy, a deprecation notice must be present for **at least
158+
one minor release** before the API is actually removed. The deprecation notice
159+
should use the `@deprecated` decorator, unless
160+
- Changes should be notated in the code and also in the release notes in the
161+
"Deprecated APIs" section.
162+
163+
**Annotating a new API** — Use the `versionadded` decorator from the vendored
164+
`cuda.core._vendored.deprecated.sphinx` module:
165+
166+
```python
167+
168+
from cuda.core._vendored.deprecated.sphinx import versionadded
169+
170+
@versionadded(version="1.2.0")
171+
def new_feature(...):
172+
"""Short description.
173+
"""
174+
```
175+
176+
Alternatively, if the vagaries of how we implement functions in Cython does not
177+
allow this, you can add the reST `versionadded` directive directly:
178+
179+
```python
180+
def new_feature(...):
181+
"""Short description.
182+
183+
.. versionadded:: 1.2.0
184+
"""
185+
```
186+
187+
**Annotating a changed API** — Use the `versionchanged` decorator from the
188+
vendored `cuda.core._vendored.deprecated.sphinx` module:
189+
190+
```python
191+
192+
from cuda.core._vendored.deprecated.sphinx import versionchanged
193+
194+
@versionchanged(version="1.2.0", reason="The old version was broken because...")
195+
def new_feature(...):
196+
"""Short description.
197+
"""
198+
```
199+
200+
Alternatively, if the vagaries of how we implement functions in Cython does not
201+
allow this, you can add the reST `versionchanged` directive directly:
202+
203+
```python
204+
def new_feature(...):
205+
"""Short description.
206+
207+
.. versionchanged:: 1.2.0
208+
The old version was broken because...
209+
"""
210+
```
211+
212+
**Deprecating an existing API** — use the `@deprecated` decorator from the
213+
vendored `cuda.core._vendored.deprecated.sphinx` module and add a
214+
`.. deprecated::` directive in the docstring. The decorator emits a
215+
`DeprecationWarning` at call time; the docstring directive surfaces it in the
216+
generated docs.
217+
218+
```python
219+
from cuda.core._vendored.deprecated.sphinx import deprecated
220+
221+
@deprecated(version="1.2.0", reason="Use `new_feature` instead.")`
222+
def old_feature(...):
223+
"""Short description.
224+
"""
225+
```
226+
227+
Rules to follow when deprecating:
228+
229+
- The `version=` argument must be the **first** in which the
230+
deprecation appears, not the release in which removal is planned.
231+
- The `reason=` string must name the replacement (if one exists) so users
232+
know what to migrate to.
233+
- Keep the old implementation fully functional — do not change its behavior,
234+
only add the decorator.
235+
- The deprecated API must remain in the codebase for **at least one full minor
236+
release cycle** before it can be removed in a subsequent major release.
237+
238+
**Removing a deprecated API** — removals land in a **major release**. Before
239+
removing, verify that the deprecation has been present since at least the
240+
previous minor release. Remove the decorator, the implementation, and any
241+
`__all__` entry; update `api.rst` and the release notes accordingly.
242+
243+
## Vendored code
244+
245+
The `cuda/core/_vendored` directory contains vendored code from third-party
246+
sources. It should not be modified, except to update the dependency or under
247+
exceptional circumstances, in which case any modifications should be clearly
248+
marked.

cuda_core/cuda/core/_vendored/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
from cuda.core._vendored.deprecated.classic import deprecated
6+
from cuda.core._vendored.deprecated.params import deprecated_params
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
import functools
6+
import inspect
7+
import warnings
8+
9+
# stacklevel=2 points past the wrapper to the actual call site
10+
_routine_stacklevel = 2
11+
_class_stacklevel = 2
12+
13+
string_types = (bytes, str)
14+
15+
16+
class ClassicAdapter:
17+
"""
18+
Classic adapter -- *for advanced usage only*
19+
20+
This adapter is used to get the deprecation message according to the wrapped
21+
object type: class, function, standard method, static method, or class method.
22+
23+
This is the base class of the :class:`~deprecated.sphinx.SphinxAdapter` class
24+
which is used to update the wrapped object docstring.
25+
"""
26+
27+
def __init__(self, reason="", version="", action=None, category=DeprecationWarning, extra_stacklevel=0):
28+
self.reason = reason or ""
29+
self.version = version or ""
30+
self.action = action
31+
self.category = category
32+
self.extra_stacklevel = extra_stacklevel
33+
34+
def get_deprecated_msg(self, wrapped, instance):
35+
if instance is None:
36+
if inspect.isclass(wrapped):
37+
fmt = "Call to deprecated class {name}."
38+
else:
39+
fmt = "Call to deprecated function (or staticmethod) {name}."
40+
else:
41+
if inspect.isclass(instance):
42+
fmt = "Call to deprecated class method {name}."
43+
else:
44+
fmt = "Call to deprecated method {name}."
45+
if self.reason:
46+
fmt += " ({reason})"
47+
if self.version:
48+
fmt += " -- Deprecated since version {version}."
49+
return fmt.format(name=wrapped.__name__, reason=self.reason or "", version=self.version or "")
50+
51+
def __call__(self, wrapped):
52+
if inspect.isclass(wrapped):
53+
old_new1 = wrapped.__new__
54+
55+
def wrapped_cls(cls, *args, **kwargs):
56+
msg = self.get_deprecated_msg(wrapped, None)
57+
stacklevel = _class_stacklevel + self.extra_stacklevel
58+
if self.action:
59+
with warnings.catch_warnings():
60+
warnings.simplefilter(self.action, self.category)
61+
warnings.warn(msg, category=self.category, stacklevel=stacklevel)
62+
else:
63+
warnings.warn(msg, category=self.category, stacklevel=stacklevel)
64+
if old_new1 is object.__new__:
65+
return old_new1(cls)
66+
return old_new1(cls, *args, **kwargs)
67+
68+
wrapped.__new__ = staticmethod(wrapped_cls)
69+
return wrapped
70+
71+
elif inspect.isroutine(wrapped):
72+
adapter = self
73+
74+
@functools.wraps(wrapped)
75+
def wrapper(*args, **kwargs):
76+
msg = adapter.get_deprecated_msg(wrapped, None)
77+
stacklevel = _routine_stacklevel + adapter.extra_stacklevel
78+
if adapter.action:
79+
with warnings.catch_warnings():
80+
warnings.simplefilter(adapter.action, adapter.category)
81+
warnings.warn(msg, category=adapter.category, stacklevel=stacklevel)
82+
else:
83+
warnings.warn(msg, category=adapter.category, stacklevel=stacklevel)
84+
return wrapped(*args, **kwargs)
85+
86+
return wrapper
87+
88+
else:
89+
raise TypeError(repr(type(wrapped)))
90+
91+
92+
def deprecated(*args, **kwargs):
93+
"""
94+
Decorator which can be used to mark functions as deprecated.
95+
96+
It will result in a warning being emitted when the function is used.
97+
"""
98+
if args and isinstance(args[0], string_types):
99+
kwargs["reason"] = args[0]
100+
args = args[1:]
101+
102+
if args and not callable(args[0]):
103+
raise TypeError(repr(type(args[0])))
104+
105+
if args:
106+
adapter_cls = kwargs.pop("adapter_cls", ClassicAdapter)
107+
adapter = adapter_cls(**kwargs)
108+
wrapped = args[0]
109+
return adapter(wrapped)
110+
111+
return functools.partial(deprecated, **kwargs)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
import collections
6+
import functools
7+
import inspect
8+
import warnings
9+
10+
11+
class DeprecatedParams:
12+
"""
13+
Decorator for functions where one or more parameters are deprecated.
14+
"""
15+
16+
def __init__(self, param, reason="", category=DeprecationWarning):
17+
self.messages = {}
18+
self.category = category
19+
self.populate_messages(param, reason=reason)
20+
21+
def populate_messages(self, param, reason=""):
22+
if isinstance(param, dict):
23+
self.messages.update(param)
24+
elif isinstance(param, str):
25+
fmt = "'{param}' parameter is deprecated"
26+
reason = reason or fmt.format(param=param)
27+
self.messages[param] = reason
28+
else:
29+
raise TypeError(param)
30+
31+
def check_params(self, signature, *args, **kwargs):
32+
binding = signature.bind(*args, **kwargs)
33+
bound = collections.OrderedDict(binding.arguments, **binding.kwargs)
34+
return [param for param in bound if param in self.messages]
35+
36+
def warn_messages(self, messages):
37+
for message in messages:
38+
warnings.warn(message, category=self.category, stacklevel=3)
39+
40+
def __call__(self, f):
41+
signature = inspect.signature(f)
42+
43+
@functools.wraps(f)
44+
def wrapper(*args, **kwargs):
45+
invalid_params = self.check_params(signature, *args, **kwargs)
46+
self.warn_messages([self.messages[param] for param in invalid_params])
47+
return f(*args, **kwargs)
48+
49+
return wrapper
50+
51+
52+
deprecated_params = DeprecatedParams

0 commit comments

Comments
 (0)