Skip to content

cuda.core: fix a few test issues - #2714

Merged
juenglin merged 18 commits into
NVIDIA:mainfrom
juenglin:graphics-tests-followup
Aug 28, 2026
Merged

cuda.core: fix a few test issues#2714
juenglin merged 18 commits into
NVIDIA:mainfrom
juenglin:graphics-tests-followup

Conversation

@juenglin

@juenglin juenglin commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-up to #2701 addressing review feedback and the test-guidance cleanup noted in cleanup-importorskip-dead-code.md.

  • Remove test_close_while_mapped_passes_stream_override (xfail from cuda.core: Fix graphics tests #2701); the stream-forwarding behavior of Buffer.close() is covered elsewhere, and the test asserted on internal call dispatch via patch.object on an immutable Cython type.
  • Fix a partial-resource leak in the GL setup helpers (_setup_gl_buffer/_setup_gl_texture in cuda_core and cuda_bindings): a failure after the window opens now closes the window before re-raising.
  • Remove dead-code pytest.importorskip calls for declared dependencies: Cython/setuptools in test_build_hooks.py and cuda.pathfinder in test_device_launch.py, replaced with top-level imports so a missing install fails collection.
  • Narrow the GL setup except Exception to pyglet's "GL unavailable" exceptions (matched by name to avoid pyglet's import-time shadow-window side effect), so a bug in our own setup code re-raises instead of being hidden as a skip.
  • Narrow _compile_heat_kernels/_compile_bisect_kernels in test_graph_definition_integration.py from bare except Exception to a narrow match on NVRTC's cudaGraphConditionalHandle is undefined diagnostic, so a real compile error fails instead of skipping.

Checklist

  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@copy-pr-bot

copy-pr-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module labels Aug 27, 2026
Comment thread cuda_core/tests/test_graphics.py
@juenglin juenglin added P1 Medium priority - Should do test Improvements or additions to tests labels Aug 27, 2026
@juenglin juenglin added this to the cuda.core 1.2.0 milestone Aug 27, 2026
@juenglin
juenglin force-pushed the graphics-tests-followup branch 2 times, most recently from 2b149cf to 92eb2ee Compare August 27, 2026 22:02
@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test 6854ccb

@juenglin
juenglin requested review from Andy-Jost and rwgk August 27, 2026 22:16
@juenglin
juenglin marked this pull request as ready for review August 27, 2026 22:17
@juenglin juenglin self-assigned this Aug 27, 2026
@juenglin
juenglin force-pushed the graphics-tests-followup branch from 6854ccb to 9a28ab6 Compare August 27, 2026 23:00
@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test 9a28ab6

@github-actions

This comment has been minimized.

@juenglin
juenglin force-pushed the graphics-tests-followup branch from 9a28ab6 to f05c176 Compare August 27, 2026 23:31
@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test f05c176

@rwgk

rwgk commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

PR 2714 initial feedback

codex gpt-5.6-sol ultra, with very minor edits

Overall, this is a useful follow-up to PR 2701: removing dead importorskip calls, deleting the permanently-xfailed implementation-detail test, and narrowing broad exception handling are all good directions. I do have several requested changes, primarily around where the new availability classifiers draw their boundaries.

Findings

High: GLException still hides real setup regressions

cuda_core/tests/test_graphics.py:65 and cuda_bindings/tests/test_graphics_apis.py:29 classify every pyglet GLException as "GL unavailable." The surrounding catches include the actual glGen*, glBind*, glBufferData, and glTexImage2D calls.

Pyglet's default GL error checking raises GLException after any GL call that reports an error, including GL_INVALID_ENUM, GL_INVALID_VALUE, and GL_INVALID_OPERATION. Consequently, an incorrect constant, argument, or context-state regression in our test setup is converted into a skip, which conflicts with this PR's stated goal of surfacing bugs in our own setup code.

Please restrict "GL unavailable" classification to context/window creation. Once a context exists, errors from buffer or texture allocation should propagate and fail the test. If GLException("No GL context; create a Window first") must be supported as an availability case, match that exact condition rather than every GLException.

Reference: https://github.com/pyglet/pyglet/blob/v2.1.14/pyglet/gl/lib.py

High: the nvfatbin availability probe misses the actual loader failures

cuda_core/tests/test_module.py:49 catches only nvfatbin.nvFatbinError around nvfatbin.version(). Loading is lazy, however:

  • An absent libnvfatbin raises cuda.pathfinder.DynamicLibNotFoundError.
  • An absent nvFatbinVersion symbol raises cuda.bindings._internal.utils.FunctionNotFoundError.
  • Neither is an nvfatbin.nvFatbinError.

Because _is_nvfatbin_available() is evaluated while constructing the marker at cuda_core/tests/test_module.py:54, either normal unavailability case aborts collection of the entire module instead of skipping the two nvfatbin-dependent tests.

Please catch the exact library/symbol-unavailable exceptions and allow genuine nvfatbin API-status failures to propagate. The newly added example in cuda_core/tests/AGENTS.md:145-153 repeats the same incorrect catch and should be fixed at the same time.

High: the NVRTC version probe can also abort collection

cuda_core/tests/test_program.py:57 has two analogous problems:

  • Missing libnvrtc or nvrtcVersion raises DynamicLibNotFoundError or FunctionNotFoundError, neither of which is a CUDAError.
  • If the suppressed import at lines 41-42 fails, nvrtc remains unbound; line 54 then raises NameError, not AttributeError.

The PCH marker is evaluated at module import time, so these paths abort collection instead of reporting PCH/NVRTC as unavailable.

Please replace the suppressed import with an explicit sentinel established under a narrow import catch, then catch the exact dynamic-library and missing-symbol exceptions around nvrtcVersion(). A CUDAError returned by a successfully loaded version API should be considered separately rather than conflated with "library unavailable."

Medium: supports_ipc_mempool still turns all driver failures into skips

cuda_core/tests/helpers/__init__.py:52 now catches CUDAError rather than Exception, but it still maps every failure from cuInit or cuDeviceGetAttribute to False. Callers interpret False as an unsupported capability and skip.

Unsupported handle types are normally represented by a successful attribute query whose bitmask lacks the POSIX-FD bit. Errors such as an invalid device, invalid attribute, deinitialized driver, or another driver regression should not silently become "unsupported." Please let query failures propagate, or classify only a specifically documented availability result by inspecting the raw CUresult.

Medium: genuine Linux GL-library unavailability is not recognized

The new predicate only recognizes pyglet-namespaced exception classes and the Windows opengl32 built-in exceptions. On Linux, pyglet raises a built-in ImportError('Library "GL" not found.') or ImportError('Library "EGL" not found.') when those system libraries cannot be loaded.

Those are genuine "GL unavailable" setup outcomes, but they now fail the tests. The existing ctypes.util.find_library("EGL") preflight covers only the simplest headless case; it does not cover a visible-display path without libGL or a library that is discoverable but cannot actually be loaded.

Please recognize the exact pyglet GL/EGL loader messages, or perform a reliable load preflight. Do not catch arbitrary ImportError.

Reference: https://github.com/pyglet/pyglet/blob/v2.1.14/pyglet/lib.py

Low: partial GL resource cleanup remains incomplete

The new cleanup begins too late:

  • In both implementations, win.switch_to() can fail after the window has been constructed but before the protected cleanup region can access it.
  • If glGenBuffers or glGenTextures succeeds and a later setup call fails, the generated GL object is not deleted.
  • In headless/shared-context mode there is no window close to release that object; even in the windowed path, pyglet's shadow/shared context can keep it alive.

Please initialize the window and GL object handles before one encompassing protected region, delete any generated object before closing the window, and make _open_gl_window() close a constructed window if switch_to() fails.

Relevant locations are cuda_core/tests/test_graphics.py:90, cuda_core/tests/test_graphics.py:105, cuda_core/tests/test_graphics.py:128, and cuda_bindings/tests/test_graphics_apis.py:54.

Low: conditional-handle diagnostic matching is duplicated

cuda_core/tests/graph/test_graph_definition_integration.py:124 adds another conditional-handle diagnostic/skip implementation even though cuda_core/tests/helpers/graph_kernels.py:79 already implements the same policy.

The copies already differ:

  • One catches CUDAError while the other catches NVRTCError.
  • One accepts two diagnostic spellings while the other accepts one.

Please extract a shared NVRTCError classifier/skip helper into helpers/graph_kernels.py and reuse it. If that is intentionally deferred, add reciprocal keep-in-sync comments.

Requested GL helper consolidation

Please consolidate _GL_UNAVAILABLE_EXC_NAMES and _is_gl_unavailable rather than keeping two copies.

The natural home is a new dependency-free module:

cuda_python_test_helpers/cuda_python_test_helpers/graphics.py

Both cuda_bindings and cuda_core test environments already depend on cuda-python-test-helpers, and both test conftest.py files contain source-tree fallback wiring for it. This avoids coupling the bindings tests to cuda_core/tests/helpers and does not introduce a package dependency cycle.

The shared helper should not import pyglet. It can classify by exception module/name and tightly matched built-in loader errors, preserving the requirement that pyglet.gl and pyglet.window not be imported until after headless mode is configured.

A suggested shape is:

# cuda_python_test_helpers/graphics.py

_GL_CONTEXT_UNAVAILABLE_EXC_NAMES = frozenset(
    {
        "NoSuchDisplayException",
        "NoSuchConfigException",
        "NoSuchScreenModeException",
        "WindowException",
        "ContextException",
    }
)

_PYGLET_GL_LIBRARY_IMPORT_ERRORS = frozenset(
    {
        'Library "GL" not found.',
        'Library "EGL" not found.',
    }
)


def is_gl_context_unavailable(exc: BaseException) -> bool:
    exc_type = type(exc)
    if (
        exc_type.__module__.startswith("pyglet")
        and exc_type.__name__ in _GL_CONTEXT_UNAVAILABLE_EXC_NAMES
    ):
        return True

    if (
        isinstance(exc, (FileNotFoundError, AttributeError))
        and "opengl32" in str(exc).lower()
    ):
        return True

    return (
        isinstance(exc, ImportError)
        and str(exc) in _PYGLET_GL_LIBRARY_IMPORT_ERRORS
    )

This intentionally omits the broad GLException entry. If the exact "No GL context" GLException is observed during context establishment and needs to skip, add a narrow message check for that case only.

The callers should also stage setup so the predicate is used only around context/window establishment:

try:
    win = _open_gl_window()
except Exception as exc:
    if is_gl_context_unavailable(exc):
        pytest.skip(f"Could not create GL context: {type(exc).__name__}: {exc}")
    raise

# GL object allocation is outside the unavailable-context catch.
# GLException from these calls must fail the test.
buf_id = _allocate_gl_buffer(...)

Please add focused, environment-independent tests for the shared predicate:

  • Accept representative pyglet context/window exceptions.
  • Accept the exact Windows opengl32 failures.
  • Accept the exact Linux GL/EGL loader failures.
  • Reject unrelated TypeError, AttributeError, ImportError, and FileNotFoundError.
  • Reject an allocation-time GLException such as an invalid-enum error.

The example at cuda_core/tests/AGENTS.md:106 should then reference this shared helper rather than showing exception names that do not match the implementation. If consolidation is declined, the fallback is reciprocal comments naming the exact other file, for example:

# Keep in sync with cuda_bindings/tests/test_graphics_apis.py.

and:

# Keep in sync with cuda_core/tests/test_graphics.py.

Other reviewed changes

The following changes looked good:

  • Replacing the declared-dependency importorskip calls for Cython, setuptools, and cuda-pathfinder.
  • Removing the permanently-xfailed stream-dispatch implementation-detail test.
  • Narrowing the conditional-kernel compile skip to specific diagnostics, subject to sharing the classifier noted above.
  • The remaining graph, build-hook, module, and program test edits outside the findings above.

Validation

  • Reviewed all nine files in the seven-commit PR diff from merge base db94059.
  • git diff --check passes.
  • The current GitHub Actions build, GPU-test, docs, API, security, and pre-commit matrix is green.
  • I did not run pytest locally because this checkout has no repository-standard TestVenv.

Per PR NVIDIA#2714 review (Ralf K): GLException is pyglet's generic GL-error
class, raised after any GL call that reports an error
(GL_INVALID_ENUM, etc.). Including it in the "GL unavailable" set hid
real bugs in our GL allocation code as skips.

Drop GLException from _GL_UNAVAILABLE_EXC_NAMES and restructure
the helpers so the skip catch wraps only context/window creation
(_open_gl_window). GL object allocation (_allocate_gl_buffer /
_allocate_gl_texture) runs outside the catch, so a GLException
from allocation propagates and fails the test.
Per PR NVIDIA#2714 review (Ralf K): on Linux without libGL or libEGL,
pyglet raises ImportError('Library "GL" not found.') /
ImportError('Library "EGL" not found.') from pyglet/lib.py.
These are genuine "GL unavailable" setup outcomes but the
predicate only recognized pyglet exceptions and the Windows
opengl32 built-in exceptions, so the tests failed on such
Linux runners.

Recognize the exact pyglet GL/EGL loader messages so a genuine
GL setup is skipped. A different ImportError from our own code
does not match.
Per PR NVIDIA#2714 review (Ralf K): the GL availability predicate
was duplicated in cuda_core/tests/test_graphics.py and
cuda_bindings/tests/test_graphics_apis.py. Both test
environments already depend on cuda-python-test-helpers.

Move the predicate to cuda_python_test_helpers/graphics.py
as is_gl_context_unavailable and import it from both test files.
The shared helper does not import pyglet (importing pyglet.gl
/ pyglet.window triggers the shadow-window side effect) and classifies
by exception module/name and tightly matched built-in loader
errors.

Add focused tests for the predicate: accepts the pyglet context/window
exceptions, the Windows opengl32 failures, and the Linux
GL/EGL loader ImportErrors; rejects unrelated TypeError/AttributeError/
ImportError/FileNotFoundError and an allocation-time GLException.
Per PR NVIDIA#2714 review (Ralf K): _is_nvfatbin_available
caught only nvfatbin.nvFatbinError around nvfatbin.version(), but
loading is lazy. An absent libnvfatbin raises
cuda.pathfinder.DynamicLibNotFoundError; an absent
nvFatbinVersion symbol raises
cuda.bindings._internal.utils.FunctionNotFoundError. Neither is an
nvFatbinError, so normal unavailability aborted collection
of the whole module instead of skipping the two nvfatbin-dependent
tests.

Catch ImportError, DynamicLibNotFoundError, FunctionNotFoundError as
"not available" and let nvFatbinError (a genuine
API-status failure from a successfully loaded library) propagate.

Update the AGENTS.md capability-probe example to match.
Per PR NVIDIA#2714 review (Ralf K): the suppressed
import of nvrtc plus the (AttributeError, CUDAError)
catch around nvrtcVersion() had two bugs.

- Missing libnvrtc raises cuda.pathfinder.DynamicLibNotFoundError;
  missing nvrtcVersion raises cuda.bindings._internal.utils.FunctionNotFoundError.
  Neither is a CUDAError.
- If the suppressed import fails, nvrtc stays unbound and
  nvrtc.nvrtcVersion() raises NameError, not AttributeError.

The PCH marker is evaluated at module import time, so these aborted
collection instead of reporting PCH/NVRTC as unavailable.

Replace the suppressed import with an explicit sentinel under a
narrow ImportError catch, then catch the exact dynamic-library and
missing-symbol exceptions around nvrtcVersion(). A CUDAError
from a successfully loaded version API propagates as a real bug.
Per PR NVIDIA#2714 review (Ralf K): supports_ipc_mempool caught
CUDAError broadly and returned False, so any cuInit or
cuDeviceGetAttribute failure (invalid device, deinitialized
driver) was silently treated as "IPC unsupported" and
callers skipped.

Unsupported handle types are normally represented by a successful
query whose bitmask lacks the POSIX-FD bit. Inspect the raw
CUresult from cuDeviceGetAttribute and treat only CUDA_ERROR_NOT_SUPPORTED as "unsupported"; let other driver errors propagate via handle_return so a real bug is not hidden as a skip.
Per PR NVIDIA#2714 review (Ralf K): the GL setup cleanup
began too late.

- _open_gl_window: win.switch_to() could fail after the window was
  constructed but before the protected cleanup region could access it.
- _allocate_gl_buffer / _allocate_gl_texture: if glGen* succeeded and a
  later setup call failed, the generated GL object was not deleted.

- In headless mode there is no window close to release the object.

Wrap switch_to() in _open_gl_window so it closes a constructed window on
failure. Wrap the GL allocation calls after glGen* in _allocate_gl_buffer
/ _allocate_gl_texture so they delete the generated object on failure. Initialize buf_id / tex_id to None in the context managers so the finally cleanup does not NameError when allocation raises before returning a handle. Use contextlib.suppress for the best-effort cleanup blocks.
Per PR NVIDIA#2714 review (Ralf K): the conditional-handle
diagnostic skip was duplicated between
test_graph_definition_integration.py and
helpers/graph_kernels.py,
and the two copies already differed (one caught
CUDAError and accepted two spellings; the other caught NVRTCError
and accepted one).

Extract skip_if_nvrtc_lacks_conditional_handle into helpers/graph_kernels.py
(with the two narrow diagnostic phrases) and reuse it from both
compile_conditional_kernels and _compile_heat_kernels /
_compile_bisect_kernels. A genuine compile error re-raises so a real bug is not hidden as a skip.
Apply the guidance updates suggested during PR NVIDIA#2714 review:

- "Skip only real setup failures": note the example
  exception names are illustrative, add GLException to an
  explicit "do not include" list, and list the platform-specific
  "library not loadable" manifestations
  (Linux ImportError for GL/EGL, Windows
  FileNotFoundError/AttributeError for opengl32).
- "Capability probes": add an example of a probe that
  inspects a raw CUresult for a documented
  availability code (the supports_ipc_mempool fix).
- New section "Clean up partial setup on failure": if
  setup allocates a resource and a later step fails, clean up
  the partial resource before re-raising.
- "Shared test support": search tests/helpers and
  cuda_python_test_helpers for prior art before adding a
  new helper; consolidate duplicates across packages
  into cuda_python_test_helpers.
@juenglin
juenglin force-pushed the graphics-tests-followup branch from f05c176 to b608b64 Compare August 28, 2026 18:23
@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test b608b64

@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test 7028efd

@rwgk

rwgk commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The below is from codex again. I'm not sure myself about the "Add mocked tests" suggestion in the last "Low" finding. All the other ones I'd try to resolve, within reason.


Re Prior Feedback

The requested _GL_UNAVAILABLE_EXC_NAMES / predicate consolidation is complete in cuda_python_test_helpers/cuda_python_test_helpers/graphics.py. The broad GLException handling, Linux GL/EGL loader handling, partial-resource cleanup, nvfatbin/NVRTC loader handling, and conditional-handle helper duplication are otherwise resolved. The IPC finding is only partially resolved.

Findings

  • Medium — IPC probe still masks driver failures. cuda_core/tests/helpers/__init__.py:53 treats CUDA_ERROR_NOT_SUPPORTED as capability absence and caches False; cuda_core/tests/AGENTS.md:197 teaches the same pattern. cuDeviceGetAttribute does not document that result and may surface errors from earlier asynchronous work, while unsupported handle types are represented by a successful bitmask without the POSIX-FD bit. Remove the special case and use mask = handle_return(driver.cuDeviceGetAttribute(...)). Add success/zero-mask/error-propagation tests. CUDA Driver API

  • Medium — The new shared-helper tests never run. cuda_python_test_helpers/tests/test_graphics.py:34 and :53 are absent from pytest.ini:10, root pixi.toml delegates only to the three product suites, and ci/tools/run-tests:16 accepts only pathfinder/bindings/core. Add a dedicated test-helpers task and lightweight CPU CI invocation. Adding the path to pytest.ini alone would fix bare root pytest, but not pixi run test or CI.

  • Medium — One genuine headless-EGL unavailability case remains unclassified. cuda_python_test_helpers/cuda_python_test_helpers/graphics.py:47 rejects Pyglet’s MissingFunctionException. Pyglet 2.1.14’s headless display calls eglQueryDevicesEXT and eglGetPlatformDisplayEXT; when libEGL exists but those entry points do not, Pyglet raises that exception and these tests fail instead of skipping. Match the exact exception type and required EGL function names, with positive and negative tests. Pyglet headless implementation, missing-function implementation

  • Low — The NVRTC sentinel hides a broken hard dependency. cuda_core/tests/test_program.py:46 catches ImportError, although the same cuda_utils module was already successfully imported at line 18 and itself imports nvrtc. Missing libnvrtc is handled lazily at lines 62–67. Import nvrtc directly alongside CUDAError/handle_return and retain only the loader/symbol catches around nvrtcVersion().

  • Low — New tests lack provenance markers. Add the appropriate human_authored, human_reviewed, or agent_authored(model=...) marker immediately above cuda_python_test_helpers/tests/test_graphics.py:34 and :53.

  • Low — Probe fixes have no focused regression coverage. Add mocked tests for the nvfatbin and NVRTC loader/symbol cases and for IPC success, zero-mask, and unexpected driver-result propagation. These rare collection/environment paths are unlikely to be exercised naturally by CI.

- Finding 1 (IPC probe): supports_ipc_mempool inspected the raw
  CUresult and special-cased CUDA_ERROR_NOT_SUPPORTED. Ralf says
  unsupported handle types are represented by a successful bitmask without the
  POSIX-FD bit, so the special case is unnecessary. Remove it; let
  handle_return raise on any non-success and rely on the bitmask check.
  Update AGENTS.md example to match.

- Finding 3 (headless EGL): is_gl_context_unavailable rejected MissingFunctionException
  (pyglet raises it when libEGL exists but eglQueryDevicesEXT /
  eglGetPlatformDisplayEXT entry points do not). Add it to the recognized set.

- Finding 4 (NVRTC sentinel): test_program caught ImportError for
  nvrtc but cuda_utils already imports nvrtc at module level, so the ImportError
  catch was dead code. Import nvrtc directly alongside CUDAError/handle_return and
  keep only the loader/symbol catches around nvrtcVersion().

- Finding 5 (provenance): move the is_gl_context_unavailable tests from
  cuda_python_test_helpers/tests/test_graphics.py to
 cuda_core/tests/test_helpers.py with
 human_reviewed markers, so
  CI actually runs them. Drop the source file.
@juenglin

Copy link
Copy Markdown
Contributor Author

/ok to test 3d36ecb

@juenglin

Copy link
Copy Markdown
Contributor Author

The below is from codex again. I'm not sure myself about the "Add mocked tests" suggestion in the last "Low" finding. All the other ones I'd try to resolve, within reason.
...

Thanks, I ignored the suggestions for the mocked tests.

@rwgk rwgk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, there is just one accidental empty line I think.

Comment thread cuda_core/tests/AGENTS.md
`from helpers.memory import create_managed_memory_resource_or_skip`.
- Search `tests/helpers/` and `cuda_python_test_helpers/` for prior art
before adding a new helper; consolidate duplicates across

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like an accidental empty line?

@juenglin
juenglin enabled auto-merge (squash) August 28, 2026 22:43
@juenglin
juenglin merged commit 996b0a0 into NVIDIA:main Aug 28, 2026
109 checks passed
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

Copy link
Copy Markdown
Doc Preview CI
Preview removed because the pull request was closed or merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module P1 Medium priority - Should do test Improvements or additions to tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants