Skip to content

Commit f98ae91

Browse files
committed
add article
1 parent 7fe46f4 commit f98ae91

2 files changed

Lines changed: 187 additions & 1 deletion

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
title: Fixing MuJoCo Rendering
3+
description: When MuJoCo refuses to render on a headless server, the culprit is usually a missing NVIDIA EGL vendor library. Here is how to fix it entirely from your home directory.
4+
slug: mujoco-egl-render
5+
tags: [gpu, mujoco]
6+
---
7+
8+
You SSH into the server, activate your environment, and run the training script that renders a few evaluation videos. Everything worked perfectly on your laptop. Here, you get something like this:
9+
10+
```plaintext
11+
mujoco.FatalError: gladLoadGL error
12+
```
13+
14+
or, if you are slightly luckier:
15+
16+
```plaintext
17+
RuntimeError: Failed to initialize EGL display
18+
```
19+
20+
No GPU is broken. Your driver is fine — `nvidia-smi` prints a beautiful table. The problem is that MuJoCo wants to render **headlessly**, and the one small piece of the driver it needs for that is missing from the system. The annoying part is that fixing it normally requires `sudo`, which you probably do not have on a shared machine.
21+
22+
Good news: you *can* fix it from your own home directory. This post explains why the error happens and walks through the workaround.
23+
24+
<!-- truncate -->
25+
26+
:::note
27+
This guide targets Linux servers with an NVIDIA GPU and no display attached. If you are on your own desktop with a monitor plugged in, you very likely do not need any of this.
28+
:::
29+
30+
---
31+
32+
## Why It Breaks
33+
34+
On a machine with a monitor, OpenGL talks to an X server, and everything just works. On a headless server there is no X server, so MuJoCo falls back to **EGL**, an interface that lets you get an OpenGL context straight from the GPU with no window system involved. That is what `MUJOCO_GL=egl` selects.
35+
36+
EGL on Linux is not a single library. It uses a dispatch layer called **libglvnd**, which acts like a receptionist: your program calls `libEGL.so.1`, and libglvnd figures out *which vendor* should actually handle the call and forwards it there. To do that, it needs two things:
37+
38+
1. **A vendor ICD file** — a small JSON file (conventionally `/usr/share/glvnd/egl_vendor.d/10_nvidia.json`) that says "NVIDIA's EGL implementation lives in this library".
39+
2. **The vendor library itself**`libEGL_nvidia.so.0`, part of the NVIDIA driver's user-space side.
40+
41+
```mermaid
42+
graph TD
43+
MuJoCo["MuJoCo (MUJOCO_GL=egl)"]
44+
glvnd["libEGL.so.1 (libglvnd dispatch)"]
45+
ICD["10_nvidia.json (vendor ICD)"]
46+
Vendor["libEGL_nvidia.so.0"]
47+
Driver["NVIDIA kernel driver"]
48+
GPU["GPU"]
49+
50+
MuJoCo --> glvnd
51+
glvnd -.->|"looks up"| ICD
52+
ICD -.->|"points to"| Vendor
53+
glvnd --> Vendor
54+
Vendor --> Driver
55+
Driver --> GPU
56+
```
57+
58+
Plenty of servers are installed with a compute-only driver package, which ships CUDA but skips the graphics bits. The kernel module is there — that is why `nvidia-smi` works — but the EGL vendor library and its ICD file never got installed. libglvnd then has no vendor to dispatch to, and MuJoCo dies at context creation.
59+
60+
The fix is to supply those two files ourselves. Both are pure user-space, so neither needs root.
61+
62+
---
63+
64+
## Step 1: Grab the Matching Driver Package
65+
66+
The user-space libraries must match the **exact version** of the kernel driver already loaded. Check yours first:
67+
68+
```bash
69+
nvidia-smi --query-gpu=driver_version --format=csv,noheader
70+
# or
71+
cat /proc/driver/nvidia/version
72+
```
73+
74+
:::warning
75+
Version matching is not optional here. A `550.x` user-space library talking to a `570.x` kernel module will fail, often with a confusing error that looks nothing like a version mismatch. Use the number you just printed.
76+
:::
77+
78+
Then download the matching `.run` installer from [NVIDIA's driver archive](https://download.nvidia.com/XFree86/Linux-x86_64) and **extract** it. Extracting is not installing — it only unpacks the archive into a directory, so it is completely safe to do as a normal user:
79+
80+
```bash
81+
# Run this from your home directory. Replace xxx.xx.xx with your driver version.
82+
cd ~
83+
wget https://download.nvidia.com/XFree86/Linux-x86_64/xxx.xx.xx/NVIDIA-Linux-x86_64-xxx.xx.xx.run
84+
sh NVIDIA-Linux-x86_64-xxx.xx.xx.run --extract-only
85+
cd NVIDIA-Linux-x86_64-xxx.xx.xx
86+
```
87+
88+
Inside you will find the real library, named with its full version. libglvnd will look for the *soname* `libEGL_nvidia.so.0`, so create that link:
89+
90+
```bash
91+
ln -s libEGL_nvidia.so.xxx.xx.xx libEGL_nvidia.so.0
92+
```
93+
94+
---
95+
96+
## Step 2: Put the Library on the Search Path
97+
98+
The loader needs to find `libEGL_nvidia.so.0` at runtime. Add the extracted directory to `LD_LIBRARY_PATH` in your `~/.zshrc` or `~/.bashrc`:
99+
100+
```bash
101+
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/NVIDIA-Linux-x86_64-xxx.xx.xx"
102+
```
103+
104+
:::tip
105+
`LD_LIBRARY_PATH` and `LIBRARY_PATH` are easy to mix up. `LIBRARY_PATH` is used by the **compiler** when linking a program; `LD_LIBRARY_PATH` is used by the **dynamic loader** when running one. libglvnd `dlopen`s the vendor library at runtime, so `LD_LIBRARY_PATH` is the one that matters.
106+
:::
107+
108+
---
109+
110+
## Step 3: Write Your Own Vendor ICD File
111+
112+
Now tell libglvnd that this library exists. Create a `10_nvidia.json` anywhere you like — your home directory is fine:
113+
114+
```json
115+
{
116+
"file_format_version" : "1.0.0",
117+
"ICD" : {
118+
"library_path" : "libEGL_nvidia.so.0"
119+
}
120+
}
121+
```
122+
123+
Normally libglvnd scans `/usr/share/glvnd/egl_vendor.d/` for these files, and we cannot write there. Luckily it also honours an environment variable that overrides the search entirely. Add this to your shell config as well:
124+
125+
```bash
126+
export __EGL_VENDOR_LIBRARY_FILENAMES=/path/to/10_nvidia.json
127+
```
128+
129+
:::note
130+
The double underscore prefix is part of the name — `__EGL_VENDOR_LIBRARY_FILENAMES`, not `_EGL_...`. It accepts a colon-separated list of files, and it **replaces** the default directory scan rather than adding to it. If the system does have other working vendors you care about, list them too.
131+
:::
132+
133+
Note that `library_path` is a bare filename rather than an absolute path. That is deliberate: it makes the loader resolve it through `LD_LIBRARY_PATH`, which is exactly what we set up in the previous step. An absolute path works too if you prefer to be explicit.
134+
135+
---
136+
137+
## Step 4: Device Permissions (Needs an Admin)
138+
139+
Rendering touches the GPU device nodes directly, and those are typically owned by the `video` and `render` groups. If your account is not in them, you will get a permission error even with everything above configured correctly.
140+
141+
This one genuinely needs root, so it is the part to hand to whoever administers the machine:
142+
143+
```bash
144+
sudo usermod -aG video $USER
145+
sudo usermod -aG render $USER
146+
```
147+
148+
Group membership is only picked up at login, so **log out and back in** afterwards. Check with:
149+
150+
```bash
151+
groups
152+
ls -l /dev/dri/
153+
```
154+
155+
---
156+
157+
## Verify It Works
158+
159+
Open a fresh shell so the new environment variables are picked up, then:
160+
161+
```bash
162+
MUJOCO_GL=egl python -c "
163+
import mujoco
164+
model = mujoco.MjModel.from_xml_string('<mujoco><worldbody><geom type=\"sphere\" size=\".1\"/></worldbody></mujoco>')
165+
data = mujoco.MjData(model)
166+
renderer = mujoco.Renderer(model, 240, 320)
167+
renderer.update_scene(data)
168+
print('rendered frame:', renderer.render().shape)
169+
"
170+
```
171+
172+
If you see `rendered frame: (240, 320, 3)`, you are done. Set `MUJOCO_GL=egl` in your shell config so you do not have to remember it every time.
173+
174+
Still failing? Work through it in order:
175+
176+
* `echo $LD_LIBRARY_PATH` and confirm the extracted directory is really in there.
177+
* `ls -l $HOME/NVIDIA-Linux-x86_64-*/libEGL_nvidia.so.0` — a dangling symlink usually means a typo in the version number.
178+
* `cat $__EGL_VENDOR_LIBRARY_FILENAMES` — if this prints nothing, the variable is unset or the path is wrong.
179+
* `groups | grep -E 'video|render'` — if empty, Step 4 has not taken effect yet.
180+
181+
The same trick applies well beyond MuJoCo — any headless EGL renderer (PyOpenGL, Isaac-style simulators, `nvdiffrast`) hits the same wall and takes the same fix.

blog/tags.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ cloud-computing:
3131
package-management:
3232
label: Package Management
3333
permalink: /package-management
34-
description: Package Management
34+
description: Package Management
35+
36+
mujoco:
37+
label: MuJoCo
38+
permalink: /mujoco
39+
description: MuJoCo

0 commit comments

Comments
 (0)