- name
- tensor-bindings-gpu
- description
- Exchange GPU simulation state as caller-owned CUDA tensors (DLPack, GPU-to-GPU with no CPU staging) through tensor bindings. The tensor-binding CODE API is deprecated in ovphysx 0.6 in favor of the session read/write API (Python PhysX.read / PhysX.write, C ovphysx_read / ovphysx_write); use ovphysx-session-write for control inputs and state, and the ovphysx-output-read skill (ovstage-native identity-preserving reads and write-back) to read output back. Tensor bindings are kept only for maintaining existing caller-owned bulk-CUDA binding code during the deprecation period.
- license
- Apache-2.0
- compatibility
- ovphysx >=0.6.0 GPU mode with CUDA device memory; C examples require the SDK headers, libraries, CUDA runtime, and an NVIDIA GPU; Python examples require PyTorch with CUDA.
- allowed-tools
- Read Shell
- metadata
- {"version":"0.1.8","author":"NVIDIA Omniverse Physics","tags":"ovphysx, physics, tensor-bindings, gpu, cuda"}
# Tensor Bindings: GPU Read and Write
> **Tensor-binding code API deprecated (ovphysx 0.6).** The tensor-binding CODE API is deprecated in
> favor of the session read/write API — Python `PhysX.read` / `PhysX.write`, C `ovphysx_read` /
> `ovphysx_write`. Prefer the session API where it fits (`ovphysx-output-read` covers ovstage-native
> identity-preserving reads and write-back). For writing control inputs and state, use the
> `ovphysx-session-write` skill; tensor bindings are kept only for maintaining existing
> caller-owned bulk-CUDA binding code.
GPU tensor bindings use GPU-mode PhysX with DLPack CUDA tensors.
DirectGPU (`/physics/suppressReadback`) is a separate setting: keep it enabled for fastest tensor-pipeline workloads and disabled for workflows that need contact modification.
ovstage populates the authored scene, while binding path resolution also includes
runtime-only clone paths. The tensor data path does not go through ovstage.
## When to Use
Use this skill when a caller needs GPU-to-GPU tensor exchange through CUDA device pointers, PyTorch CUDA tensors, or DLPack without CPU staging.
## Instructions
1. Read the full C or Python sample before adapting this pattern because CUDA memory lifetime, DLPack shape storage, and device ordinal handling matter.
2. Create the binding, query its native device with `binding.native_device` or
`ovphysx_get_tensor_binding_native_device()`, require `kDLCUDA` for a
no-staging GPU path, allocate on the returned process-visible ordinal, and
keep shape storage valid until the synchronous read or write returns.
3. Use Shell to compile and run the full sample or a local integration test after adapting the scene path and tensor type.
## C sample (CUDA)
The GPU tensor bindings sample uses `cuda_runtime.h` for device memory allocation and wraps those device pointers in DLPack `DLTensor` structs for:
- `ovphysx_read_tensor_binding()`
- `ovphysx_write_tensor_binding()`
Full sample:
- `samples/c_samples/tensor_bindings_gpu_c/main.c` (SDK)
- Source checkout: `tests/c_samples/tensor_bindings_gpu_c/main.c`
Minimal DLPack wrapper pattern:
```c
#include <ovphysx/ovphysx.h>
#include <ovphysx/dlpack/dlpack.h>
#include <stddef.h>
#include <stdint.h>
static DLTensor make_cuda_tensor_f32_2d(
// CUDA device memory, for example from cudaMalloc(); caller frees it.
void* device_buffer,
int64_t rows,
int64_t columns,
// Caller-owned storage; tensor.shape points here until the tensor is consumed.
int64_t shape_storage[2],
// Native CUDA ordinal returned for this binding.
int32_t cuda_device_id)
{
shape_storage[0] = rows;
shape_storage[1] = columns;
DLTensor tensor = {
.data = device_buffer,
.device = { kDLCUDA, cuda_device_id },
.ndim = 2,
.dtype = { kDLFloat, 32, 1 },
.shape = shape_storage,
.strides = NULL,
.byte_offset = 0
};
return tensor;
}
static int read_and_write_gpu_tensor(
ovphysx_instance_handle_t handle,
ovphysx_tensor_binding_handle_t binding,
// CUDA device memory, for example from cudaMalloc(); caller frees it.
void* device_buffer,
int64_t count,
int64_t components,
// Native CUDA ordinal returned for this binding.
int32_t cuda_device_id)
{
int64_t shape_storage[2];
DLTensor tensor = make_cuda_tensor_f32_2d(
device_buffer,
count,
components,
shape_storage,
cuda_device_id);
// Check every result; GPU I/O failures surface through the return status.
if (ovphysx_read_tensor_binding(handle, binding, &tensor).status != OVPHYSX_API_SUCCESS) {
return 0;
}
// NULL means no index tensor: apply a full, unindexed write.
if (ovphysx_write_tensor_binding(handle, binding, &tensor, NULL).status != OVPHYSX_API_SUCCESS) {
return 0;
}
return 1;
}
```
After creating each binding, call `ovphysx_get_tensor_binding_native_device()`
and require `kDLCUDA` before using this GPU-only pattern. Allocate and wrap the
buffer on the returned `device_id`; the binding query, not configured
`active_cuda_gpus`, is authoritative for empty or multi-scene device selection.
The full C sample shows the query before allocation.
This helper wraps an existing CUDA allocation; it does not allocate memory.
`device_buffer`, `shape_storage`, and optional `strides` storage must stay valid until the synchronous read or write call returns.
The snippet uses `strides = NULL` for C-contiguous tensors, matching the ovphysx C samples.
For Python, install `physx-usd-schemas` and `newton-usd-schemas` alongside
`ovphysx`. Register both PhysX and Newton schemas before the first population
call so authored `newton:*` values are preserved.
Import the PhysX schema recipe from
`ovphysx.samples.python_samples.schema_support`; for native samples, set
`PHYSX_USD_SCHEMAS_ROOT` to the extracted wheel. See
[Physics Schemas](../../docs/physics_schemas.md) for setup.
## Python with PyTorch
```python
from pathlib import Path
import ovphysx
from ovphysx.samples.python_samples.schema_support import physx_schema_paths
import torch
from ovphysx import ConfigKey, DLDeviceType, PhysX, PhysXConfig
from ovphysx.types import TensorType
import ovstage
cuda_device = 0
# DirectGPU (`/physics/suppressReadback`) still uses the legacy PhysXConfig
# carbonite path; GPU ordinal is per-instance after create.
physx = PhysX(
config=PhysXConfig(
carbonite_overrides={"/physics/suppressReadback": True},
),
)
physx.set_config(ConfigKey.ACTIVE_CUDA_GPUS, str(cuda_device))
# Register external PhysX and Newton schemas before the first population.
ovstage.population.register_usd_schemas(
[*[str(p) for p in physx_schema_paths()], str(ovphysx.newton_schema_root())]
)
stage = ovstage.Stage("ovphysx-gpu-tensors")
usd_path = Path(ovphysx.__file__).resolve().parent / "samples" / "data" / "basic_simulation.usda"
ovstage.population.open_usd(stage, str(usd_path), ordinal=1, domains=ovstage.PopulationDomain.ALL)
# attach_ovstage() reads at a sealed ordinal.
stage.advance_write_floor(ordinal=1).wait()
physx.wait_task(physx.attach_ovstage(stage, read_ordinal=1))
# /World/envs/env* is a clone pattern: env1..env3 exist only after clone + wait.
# clone-environments owns the full clone contract (wait_all, CPU isolation).
targets = ["/World/envs/env1", "/World/envs/env2", "/World/envs/env3"]
anchor_transforms = [
(4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
(8.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
(12.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
]
physx.clone("/World/envs/env0", targets, anchor_transforms=anchor_transforms)
physx.wait_all()
binding = physx.create_tensor_binding(
pattern="/World/envs/env*/table",
tensor_type=TensorType.RIGID_BODY_POSE,
raise_if_empty=True,
)
native_device = binding.native_device
if native_device.device_type.value != DLDeviceType.kDLCUDA:
raise RuntimeError(f"Expected a native CUDA binding, got {native_device}")
cuda_device = native_device.device_id
# Write poses from a PyTorch CUDA tensor via DLPack (e.g. an RL env reset).
# The pose layout is [px, py, pz, qx, qy, qz, qw]; qw = 1 is identity rotation.
reset_poses = torch.zeros(
binding.shape, dtype=torch.float32, device=f"cuda:{cuda_device}"
)
reset_poses[..., 6] = 1.0
binding.write(reset_poses)
# step_sync steps and waits in one call
physx.step_sync(1.0 / 60.0)
# Read simulated poses back into a CUDA tensor (no CPU staging)
output = torch.zeros(
binding.shape, dtype=torch.float32, device=f"cuda:{cuda_device}"
)
binding.read(output)
binding.destroy()
physx.wait_task(physx.detach_ovstage())
stage.destroy()
physx.destroy()
```
Use `ALL` when populating USD so nested world transforms are resolved by the
rendering domain. `PHYSICS` alone seeds local world matrices in OVStage 0.3;
see [Population domains](../../docs/ovstage_integration.md#population-domains).
## Important notes
- GPU dynamics are enabled by default (`physxScene:enableGPUDynamics` defaults to `true`); set it to `false` to opt into CPU dynamics (PhysX reads this). To prevent any GPU usage in ovphysx, call `PhysX.set_cpu_mode(True)` before creating any `PhysX` instance. Process-wide CPU-only mode rejects CUDA DLPack tensors before accessing CUDA.
- DLPack interop works with PyTorch and other objects implementing the `__dlpack__` protocol.
- `binding.native_device` is the authoritative no-staging placement. A CPU
result means this GPU-only pattern does not apply to that binding.
- Masked writes are supported on GPU via `ovphysx_write_tensor_binding_masked()` /
`binding.write(tensor, mask=mask)` for bindings whose storage follows the
simulation device. CPU-only property bindings require host-resident source and
mask tensors.
- Create bindings once outside simulation loops and reuse them; binding creation allocates native TensorAPI resources.
- Pass `raise_if_empty=True` when a zero-count binding is a configuration error
(wrong pattern, missing clone). The default `False` accepts empty bindings;
check `binding.count` before reading. See `docs/tutorials/tensor_bindings.md`.
## References
- Docs: `docs/tutorials/tensor_bindings.md` (tensor bindings tutorial, shared with the CPU skill); `docs/developer_guide.md` for GPU/DirectGPU specifics
- C sample: `samples/c_samples/tensor_bindings_gpu_c/main.c` (SDK; source: `tests/c_samples/tensor_bindings_gpu_c/main.c`)
GitHub에서 보기