System designTroubleshooting

Headless camera sensors need a valid graphics device id

Why a camera sensor returns handle -1 in a headless run, how to pin one GPU for a real graphics device, and when enable_tensors=False is the right call.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01What the error is telling you
  2. 02Cause 1: graphics_device_id=-1 removes rendering, not just the window
  3. 03Cause 2: two or more GPUs, and nobody pinned one
  4. 04Cause 3: enable_tensors=False hides the crash, not the cause
  5. 05Worked example: pinning one GPU on a two-GPU vision server
  6. 06The fix, in order
  7. 07Where this bites on real deployments
  8. 08Prevent it on the next build
  9. 09Questions

create_camera_sensor returns -1 in your headless run, and the next line dies with could not find camera with handle -1 in environment 0. The cause sits one call earlier and one idea deeper: headless turns off the on-screen window, not the need for a graphics device. Camera sensors render their images on a GPU, so a sim created with graphics_device_id=-1 cannot create sensors at all. This page is for anyone running a rendering or simulation perception stack on a rack-mounted vision controller or a GPU server. You will leave with both failure signatures decoded, a two-GPU pinning example with the arithmetic shown, the fix in order, and the decision rule for enable_tensors.

What the error is telling you

The log line reads [Error] [carb.gym.plugin] SetCameraLocation: Error: could not find camera with handle -1 in environment 0. The message names set_camera_location, but the blame sits one call earlier. create_camera_sensor returns an integer handle; -1 is its no-sensor value. The placement call then searches environment 0 for a camera with handle -1, finds nothing and raises the error. Debug the creation, not the placement: the camera never existed.

Two failure signatures show up in practice, and they need different first moves:

What you see What it means First move
Cam Handle: -1, then the SetCameraLocation error The sim has no graphics device, so no sensor was created Create the sim on a real GPU id
Segfault at create_sim on a multi-GPU machine Graphics and CUDA contexts landed on different GPUs Pin one GPU with CUDA_VISIBLE_DEVICES before launch
Segfault only with enable_tensors=True The GPU-tensor interop path has no graphics context Fix the device first; tensors off only if you drop GPU tensors

Both signatures share one root: the process tried to run a rendering workload without a usable graphics device, or with one it cannot address cleanly. The three causes below cover the ways that happens.

Cause 1: graphics_device_id=-1 removes rendering, not just the window

Passing -1 asks for compute without graphics. Physics, state reads and your own CPU code paths keep working, which is why the sim looks healthy right up to the sensor call. A camera sensor is different: it is a rendered view, produced by the graphics backend on a real device. With no graphics context the creation fails and hands back -1 quietly; the error only surfaces when you next touch the handle.

The fix on a single-GPU machine is one character: create the sim with graphics_device_id=0. Keep --headless if you want no window, because the two settings are independent. Headless decides what is displayed; the device id decides whether sensors can render at all.

Cause 2: two or more GPUs, and nobody pinned one

On a machine with two or more GPU cards, each subsystem picks its own device: the sim's graphics context lands on one card, the CUDA runtime that wraps your tensors lands on another, and create_sim segfaults at the join. The signature is hard to miss: the identical script runs on a single-GPU laptop and crashes on a dual-GPU workstation.

Pin the process to one card before it launches:

export CUDA_VISIBLE_DEVICES=0

The process now sees exactly one device, so graphics and tensors share the same silicon, and 0 becomes the only valid device id for create_sim.

Containers follow the same rule: pass the GPU through and make it visible first, as in the depth camera GPU visibility fix in Docker. Training jobs that segfault on multi-GPU servers hit the same join from the training side; the YOLO segmentation fault fixes start one step earlier in that stack.

Cause 3: enable_tensors=False hides the crash, not the cause

Setting enable_tensors=False in the camera properties makes the segfault disappear, because the sensor then skips the GPU-tensor interop path that has no graphics context to live on. Treat that as a diagnosis, not a fix: the run still has no working rendering device, and code that calls get_camera_image_gpu_tensor and wraps it with gymtorch.wrap_tensor now has nothing to wrap.

Take the tensors-off road only when you also give up GPU tensors and read images back over the CPU. If your model consumes the tensor on the GPU, fix the device and leave tensors on, keeping the pipeline on-card the way the GPU tensor input pattern expects. The same interop layer serves on-screen display when you do want a window; the CUDA OpenGL interop guide for 3D camera display covers that side.

Worked example: pinning one GPU on a two-GPU vision server

The inputs, with their sources:

  • 2 GPU cards installed — the dual-GPU workstation case.
  • Device ids start at 0, so the cards answer to 0 and 1 — standard CUDA numbering.
  • Pin: CUDA_VISIBLE_DEVICES=0 — exactly one card left visible.
  • Sim argument: graphics_device_id, the value handed to create_sim.

The rule in words: a process can only create a sim on a device id between 0 and the number of visible GPUs minus 1, and a camera handle is usable only when it is 0 or higher.

valid sim device ids = 0 .. (visible GPUs - 1) usable sensor handle = handle >= 0

The arithmetic: unpinned, the process sees both cards, so valid ids run 0 to 2 - 1 = 1. Pinned to card 0, the process sees 2 - 1 = 1 GPU, so the only valid id is 0. Passing graphics_device_id=1 after the pin addresses a card the process can no longer see — a guaranteed failure.

The result: create_sim on device 0 succeeds, and create_camera_sensor returns 0 — a real handle, at the bottom of the usable range. set_camera_location then places that sensor with its eye at (1.2, 0, 2.5) looking at (0, 0, 1.2). The placement distance checks out as d = √[(1.2 - 0)² + (0 - 0)² + (2.5 - 1.2)²] = √(1.44 + 0 + 1.69) = √3.13 ≈ 1.77 units — meters in the default sim set-up — from its target.

The decision: proceed when the handle is 0 or higher; stop and fix the device when it is -1. Keep exactly one visible GPU per process. The moment a second card is visible, the process is back in the split-device zone where create_sim crashes.

The fix, in order

  1. Smoke-test the machine before your code: run the interop example headless with python interop_torch.py --headless. If the example fails, the device set-up is the problem, not your script.
  2. Pin one GPU in the shell or service file: export CUDA_VISIBLE_DEVICES=0. Do it before launch, never inside the running process.
  3. Create the sim on a real graphics device — 0 under the pin. Never pass -1 to a run that creates camera sensors.
  4. Log the handle from create_camera_sensor immediately and refuse to continue on a negative value.
  5. Only then place the camera with set_camera_location, giving both the eye and the target as real coordinates.
  6. Check the tensor device before inference or training: the wrapped tensor must report the same GPU the sim renders on. Multi-view stacks slice per camera from one buffer, as in the per-view GPU tensor slicing guide.
  7. Re-run step 1 after every driver or SDK update; alignment between the two is its own failure mode, as the SDK and firmware version alignment guide shows.

Where this bites on real deployments

Rack controllers and remote servers run headless all day, so a perception stack that renders sensors or feeds GPU tensors needs a graphics device the process can see, sized like any other resource. Three shapes cover most deployments: a CPU vision controller with PCIe expansion takes a rendering card, an AI inference terminal carries its cards on board, and a multi-GPU training server makes per-process pinning mandatory.

Vision controllersHikrobot MV-VC5525-256G70-NN VC5000 Vision Controller, 12th-I5, 32GB+256G SSD, Supported PCIEMV-VC5525-256G70-NN
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
$4,479.00 · quotedView and quote
Vision controllersHikrobot MV-VAC5724-256G02 AI inference terminal VC5724 dual cardMV-VAC5724-256G02
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
$4,609.00 · quotedView and quote
Vision controllersHikrobot MV-TS1004-256GR-960G16TR10 AI training server 1000 quad-GPU standardMV-TS1004-256GR-960G16TR10
LAN ports
2 LAN ports
USB 3 ports
3-4 USB 3 ports
Price on requestView and quote

The MV-VC5525-256G70-NN pairs an Intel Core i5 with 32 GB of memory and PCIe expansion, so a rendering card fits beside the capture ports. The MV-VAC5724-256G02 carries two AI inference cards on board, with PoE camera ports and 8 built-in light channels. The MV-TS1004-256GR-960G16TR10 is a quad-GPU training server, exactly the box where the per-process pin from the worked example is mandatory; give each job its own card.

When the trained stack moves onto real hardware, the discipline follows it: depth and stereo 3D sensors build their own GPU buffers on the same box, and the unsupported compute device fixes apply when an SDK refuses a device the OS can see.

Prevent it on the next build

Four habits keep this failure class out of your logs:

  • Create every sim on a real graphics device id, even in headless runs; reserve -1 for pure compute jobs that create no sensors.
  • Pin the GPU in the launcher or service definition, so every process starts with exactly one visible device.
  • Log every handle at creation and stop the run on a negative value.
  • Keep the graphics stack, the CUDA runtime and the SDK build aligned, and re-run the interop example after each update.

Assemble the rest of the rig — capture ports, light channels, trigger wiring — in the build-a-system configurator, and send unusual deployments through the contact page.

Questions

Does a headless machine still need a GPU to create camera sensors?

Yes, when the sensors render. Headless removes the on-screen window only; a camera sensor is a rendered view and needs the graphics context a real device id provides. Pure compute paths run fine with graphics_device_id=-1, but sensor creation and GPU tensor extraction do not. A CPU-only headless run is legitimate exactly when you need neither rendered views nor GPU tensors.

What value does a valid camera sensor handle have?

A non-negative integer; the first sensor in an environment gets 0. Treat -1 as the library reporting that no sensor was created and stop the run there. Log the handle on the line after create_camera_sensor, and never hand a negative value to set_camera_location or a tensor call. Placement errors that mention handle -1 are always creation failures one call earlier.

Can several simulations share one multi-GPU server?

Yes, and one process per card is the reliable pattern. Export a different CUDA_VISIBLE_DEVICES for each job before it starts, so every process sees exactly one GPU and creates its sim on device 0. Two jobs pinned to the same card fight over memory and context, and an unpinned job on a multi-card box risks the split-device crash described above.

Why did setting enable_tensors to False stop the segfault but break my tensor code?

enable_tensors decides whether the sensor exposes its image as a GPU tensor through the CUDA interop path. With False the path is skipped, so the crash disappears, but get_camera_image_gpu_tensor has nothing to return and wrap_tensor has nothing to wrap. Images then travel over the CPU, which adds a copy per frame and changes your latency budget.

Which vision controller should run a GPU-rendering pipeline?

One with a graphics path sized for the job. A controller with PCIe expansion accepts a rendering card next to the capture ports, an AI inference terminal carries its cards on board, and a four-GPU training server needs a per-process CUDA_VISIBLE_DEVICES pin for every job. Size GPU memory to the largest model plus the sensor tensor buffers, and keep one visible GPU per process.