All posts by sufehmi

How to run Nemotron 3 Super 120B FP8 on vLLM on 2 x Nvidia H200 143GB VRAM

Running a 120-billion-parameter reasoning model across two GPUs is no longer an exotic exercise — in 2026, with NVIDIA’s FP8 quantization and the Hopper architecture’s native FP8 tensor cores, it is an afternoon project that delivers genuinely production-grade speed. This article walks through the complete, end-to-end deployment of Nemotron-3-Super-120B-A12B on a server with 2× NVIDIA H200 (143 GB each), served by vLLM behind a standard OpenAI-compatible API, with a 1-million-token context window, thinking/reasoning mode, tool calling, multi-turn conversations, and API-key authentication.

There is already an excellent guide for running this model on a single Blackwell RTX PRO 6000 using the NVFP4 (4-bit floating point) checkpoint. This article is the companion piece for the Hopper / H200 case — and as you’ll see, the model variant and several flags must change, because Hopper and Blackwell have different native low-precision capabilities. They are a different hardware, so the installation process turned out to be quite different.

The Target Hardware

Component Specification
GPU 2× NVIDIA H200
VRAM (per GPU) 143 GB (143,771 MiB)
Total VRAM ~287 GB
Compute Capability sm_90 (Hopper)
Driver 580.105.08 / CUDA 13.0
System RAM 440 GB
Storage 1.5 TB NVMe
OS Ubuntu 24.04.3 LTS

The Critical Decision: FP8, Not NVFP4

This is the single most important thing to get right, and it is where a blind copy of the Blackwell guide will bite you.

Nemotron-3-Super-120B-A12B ships in three quantization variants on Hugging Face:

  • NVFP4 — 4-bit floating point weights (~80 GB). Requires Blackwell architecture (B200/B300, RTX PRO 6000 Blackwell) with native FP4 tensor cores. On Hopper, FP4 has no native tensor-core support — it would be emulated and slow.
  • FP8 — 8-bit floating point weights (~128 GB). This is the native sweet spot for Hopper (H100/H200). The H200’s FP8 tensor cores run this at full speed.
  • BF16 — 16-bit, ~240 GB. Overkill and a waste of VRAM unless you need the absolute maximum accuracy.

For 2× H200, the correct choice is nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8. At ~128 GB it fits comfortably across both GPUs with tensor parallelism, leaving roughly 160 GB of headroom for a large FP8 KV cache — which is exactly what lets you run the full 1M-token context.

Rule of thumb: Blackwell (sm_120+) ? NVFP4. Hopper (sm_90) ? FP8. Do not mix these up — the performance difference is dramatic.

The Model: Nemotron-3-Super-120B-A12B

A quick refresher on why this model is special and how it achieves its speed:

  • 120 billion total parameters, only 12 billion active per token — a hybrid Latent Mixture-of-Experts design (22 experts routed out of 512 per layer). This is why it is fast despite its size.
  • Mamba-2 hybrid layers interleaved with MoE and attention layers. The Mamba layers have O(1) recurrent state (no growing KV cache), which is precisely how the model supports a 1,048,576-token (1M) context.
  • Configurable reasoning/thinking mode via the chat template’s enable_thinking flag — the model produces a reasoning trace, then a final answer.
  • Native tool calling (function calling) and multi-turn conversation support.
  • Multi-Token Prediction (MTP) layers for faster speculative generation.

On Hugging Face it lives at nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8. It is public and ungated — no token or license acceptance required to download.

Step 1 — Connect to the Server

ssh root@YOUR-SERVER-IP

Verify the GPUs are visible and idle:

nvidia-smi

You should see two H200s with ~143 GB each and 0 MiB used.

Step 2 — Install Docker and GPU Passthrough

Unlike the Blackwell guide (where Docker was pre-installed), our H200 server had no Docker at all — only the nvidia-container-toolkit package. Install Docker first:

apt-get update
apt-get install -y docker.io
systemctl enable docker
systemctl start docker

Then configure the NVIDIA container runtime so Docker can pass GPUs through to containers:

nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Verify both GPUs are visible inside a container:

docker run --rm --runtime=nvidia --gpus all ubuntu:22.04 \
  nvidia-smi --query-gpu=name,memory.total --format=csv,noheader

You should see:

NVIDIA H200, 143771 MiB
NVIDIA H200, 143771 MiB

Step 3 — Create a Python Environment

We need a small venv on the host for downloading the model. On Ubuntu 24.04, the python3-venv package must be installed explicitly:

apt-get install -y python3.12-venv

python3 -m venv /opt/nemotron-venv
/opt/nemotron-venv/bin/pip install --upgrade pip wheel setuptools
/opt/nemotron-venv/bin/pip install "huggingface_hub[cli]" hf_transfer

Step 4 — Download the Model (128 GB)

mkdir -p /opt/models

HF_HUB_CACHE=/opt/models /opt/nemotron-venv/bin/python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
    "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8",
    cache_dir="/opt/models",
    max_workers=16,
)
PY

This pulls 26 safetensors shards (~128 GB) plus the chat template, custom modeling code (modeling_nemotron_h.py), and a reasoning parser (super_v3_reasoning_parser.py). With 16 parallel workers on a fast connection, expect roughly 10–15 minutes.

Note the snapshot path — you’ll need it later:

find /opt/models -type d -path "*Nemotron-3-Super*FP8*/snapshots/*"

Step 5 — Generate an API Key

Create a random API key that clients must present as a Bearer token:

API_KEY="nm-$(openssl rand -hex 24)"
echo "$API_KEY"

mkdir -p /opt/nemotron-serve
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
chmod 600 /opt/nemotron-serve/.api_key

Keep this key secret — every request to the server must carry it.

Step 6 — Pull the vLLM Image

The model’s README pins vLLM 0.20.0. Use the official Docker image — it bundles a matched CUDA 13.x toolchain so all the FP8 kernels compile and load cleanly. Do not be tempted by pip install vllm; the pip path hits JIT compiler/header mismatches on Hopper that the Docker image avoids entirely.

docker pull vllm/vllm-openai:v0.20.0

Step 7 — The Launch Script

This is the heart of the deployment. Create the launch script:

cat > /opt/nemotron-serve/launch_docker.sh <<'EOF'
#!/bin/bash
set -euo pipefail

API_KEY="$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)"
# Path as it appears INSIDE the container (the HF_HOME mount point)
MODEL_PATH="/root/.cache/huggingface/models--nvidia--NVIDIA-Nemotron-3-Super-120B-A12B-FP8/snapshots/SNAPSHOT_HASH"

echo "Using model path: $MODEL_PATH"
echo "Using API key:    $API_KEY"

docker rm -f nemotron-serve 2>/dev/null || true

exec docker run --name nemotron-serve \
  --runtime nvidia --gpus all \
  --network host \
  --shm-size 16g \
  --restart unless-stopped \
  -e VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 \
  -e HF_HUB_OFFLINE=1 \
  -e HF_HOME=/root/.cache/huggingface \
  -e HF_MODULES_CACHE=/root/.cache/hf_modules \
  -v /opt/models:/root/.cache/huggingface:ro \
  -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw \
  vllm/vllm-openai:v0.20.0 \
    --model "$MODEL_PATH" \
    --served-model-name nvidia/nemotron-3-super \
    --host 0.0.0.0 \
    --port 20000 \
    --api-key "$API_KEY" \
    --async-scheduling \
    --dtype auto \
    --kv-cache-dtype fp8 \
    --tensor-parallel-size 2 \
    --enable-expert-parallel \
    --trust-remote-code \
    --gpu-memory-utilization 0.90 \
    --enable-chunked-prefill \
    --max-model-len 1048576 \
    --max-num-seqs 32 \
    --mamba-ssm-cache-dtype float32 \
    --reasoning-parser nemotron_v3 \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder
EOF

chmod +x /opt/nemotron-serve/launch_docker.sh
mkdir -p /opt/hf-cache/modules

Replace SNAPSHOT_HASH with the actual hash from Step 4.

What each key flag does

  • --tensor-parallel-size 2 — splits the model across both H200s. Essential for a 128 GB checkpoint.
  • --enable-expert-parallel — distributes the MoE experts across GPUs (NVIDIA’s recommended setting for multi-GPU Hopper).
  • --max-model-len 1048576 + VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 — unlocks the full 1M-token context. The model’s max_position_embeddings is 262144, but the Mamba/attention hybrid legitimately extends to 1M; the env var lets vLLM exceed the conservative default.
  • --kv-cache-dtype fp8 — FP8 KV cache, halving attention memory and fitting longer contexts in VRAM.
  • --mamba-ssm-cache-dtype float32 — the Mamba-2 recurrent state in fp32 (required for the hybrid layers).
  • --reasoning-parser nemotron_v3 — loads the parser that splits reasoning traces into the reasoning field of the response.
  • --enable-auto-tool-choice --tool-call-parser qwen3_coder — enables function-calling in the OpenAI-compatible API.
  • --max-num-seqs 32 — allows up to 32 concurrent sequences for batching.
  • --enable-chunked-prefill — processes long prompts in chunks for smoother scheduling.

Launch it

nohup /opt/nemotron-serve/launch_docker.sh > /opt/nemotron-serve/docker.log 2>&1 &

Startup takes about 2 minutes: ~30 s to load 128 GB of weights across two GPUs, then ~90 s for torch.compile and CUDA graph capture. The server is ready when the log shows:

INFO ... Application startup complete.

Follow along with:

tail -f /opt/nemotron-serve/docker.log

The Two Pitfalls (and Their Fixes)

I hit both of these during bring-up. The launch script above already avoids them, but they are worth understanding.

Pitfall #1 — --swap-space not recognized

Symptom: vllm: error: unrecognized arguments: --swap-space 0.

Cause: The model’s README (written for a newer vLLM) includes --swap-space 0, but vLLM 0.20.0’s argument parser does not accept this flag.

Fix: Simply remove --swap-space 0 from the launch script.

Pitfall #2 — Model path resolution inside the container

Symptom: HFValidationError: Repo id must be in the form 'repo_name' or 'namespace/repo_name': '/opt/models/...' or LocalEntryNotFoundError: Cannot find an appropriate cached snapshot folder.

Cause: There are two traps here. First, if you pass the host path (/opt/models/...), it does not exist inside the container (the weights are mounted at /root/.cache/huggingface/...). Second, if you pass the repo ID (nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8) with HF_HUB_OFFLINE=1, vLLM’s get_model_path() tries to look up a specific revision and fails because offline mode prevents the lookup.

Fix: Pass the path as it appears inside the container — i.e., under the HF_HOME mount point (/root/.cache/huggingface/models--nvidia--.../snapshots/HASH), not the host path. The launch script above does exactly this.

Step 8 — Verify Everything Works

Load the API key into a variable:

API_KEY=$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)

List models — confirms the 1M context

curl -s http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY" | python3 -m json.tool

The response includes "max_model_len": 1048576 — your 1M context is active.

Thinking / reasoning mode

Pass "reasoning": {"effort": "auto"} in the request body to activate the reasoning parser. The model’s thinking trace appears in the reasoning field, and the final answer in content:

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"A bat and ball cost $1.10 total. The bat costs $1.00 more than the ball. How much is the ball?"}],
    "max_tokens": 500,
    "temperature": 0.6,
    "reasoning": {"effort": "auto"}
  }'

You can also toggle thinking per-request via the chat template:

"chat_template_kwargs": {"enable_thinking": true}

Tool calling

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"What is the weather in Tokyo?"}],
    "max_tokens": 400,
    "tools": [{"type":"function","function":{
      "name":"get_weather",
      "description":"Get current weather for a city",
      "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
    }}]
  }'

The model returns "finish_reason": "tool_calls" with a structured call to get_weather({"city": "Tokyo"}).

Multi-turn conversations

Simply pass the full message history — the model retains context across turns:

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [
      {"role":"user","content":"My name is Alice. Remember it."},
      {"role":"assistant","content":"Nice to meet you, Alice!"},
      {"role":"user","content":"What is my name?"}
    ],
    "max_tokens": 200
  }'

API key enforcement

# Wrong key ? HTTP 401
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models \
  -H "Authorization: Bearer wrong-key"

# No key ? HTTP 401
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models

# Correct key ? HTTP 200
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY"

Benchmarks

Measured against the live server with single requests (unless noted), using the OpenAI-compatible API:

Test Prompt Output Wall time Throughput
Short generation 20 tok ~50 tok 0.35 s 144 tok/s
Medium generation 26 tok ~300 tok 1.87 s 160 tok/s
Long generation 37 tok ~800 tok 4.91 s 163 tok/s
Large prefill 8,827 tok 132 tok 8.16 s ~4,507 tok/s (prefill+decode)
Concurrent (8 parallel) 200 tok 1,600 tok 5.69 s 281 tok/s aggregate

~160 tokens/second steady-state decode from a 120B-parameter model — and remarkably flat across output lengths (144 ? 163), confirming the FP8 KV cache and Mamba SSM state are healthy. This is roughly 1.7× faster than the ~94 tok/s achieved on a single Blackwell RTX PRO 6000, thanks to the H200’s higher memory bandwidth plus tensor parallelism across two GPUs.

Under 8-way concurrency, aggregate output throughput reaches 281 tok/s, demonstrating that the batching engine (--max-num-seqs 32) scales well. For context: a full 800-token coding response generates in under 5 seconds.

Using the Server

The server speaks the standard OpenAI API. Point any OpenAI-compatible client at it:

from openai import OpenAI

client = OpenAI(
    base_url="http://YOUR-SERVER-IP:20000/v1",
    api_key="nm-YOUR-API-KEY",
)

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Design a REST API for a todo app."}],
    max_tokens=2000,
    temperature=0.6,
    extra_body={"reasoning": {"effort": "auto"}},
)
print(resp.choices[0].message.reasoning)  # thinking trace
print(resp.choices[0].message.content)     # final answer

Tool calling works the same way — pass tools=[...] and the model emits structured function calls. Multi-turn conversations work as expected (pass the full message history).

Operations Cheatsheet

# check container status
docker ps --filter name=nemotron-serve

# tail logs
docker logs -f nemotron-serve

# restart (picks up the launch script)
docker restart nemotron-serve

# GPU usage
nvidia-smi

# rotate the API key
API_KEY="nm-$(openssl rand -hex 24)"
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
docker restart nemotron-serve

GPU Memory Breakdown

With the 128 GB FP8 checkpoint split across two H200s at 90% memory utilization:

GPU 0: 129,757 MiB / 143,771 MiB used
GPU 1: 129,757 MiB / 143,771 MiB used

That is ~65 GB of weights per GPU plus a large FP8 KV cache and Mamba SSM state — leaving healthy headroom for the 1M-token context window.

Conclusion

The era of running a 120-billion-parameter reasoning model on affordable datacenter GPUs is firmly here. With two H200s and vLLM, you get a 1M-context reasoning model at ~160 tok/s, with native tool calling and thinking mode, behind a standard OpenAI-compatible API secured with an API key.

The key insight — and the reason this article exists alongside the Blackwell guide — is matching the quantization variant to your architecture: NVFP4 for Blackwell, FP8 for Hopper. Get that one decision right, and the rest of the deployment is straightforward infrastructure work: Docker GPU passthrough, the correct model path inside the container, and the right cache/memory flags. Once those are solved, the server is stable, fast, and ready for production traffic.


Model: NVIDIA-Nemotron-3-Super-120B-A12B-FP8, governed by the NVIDIA Nemotron Open Model License. Serving: vLLM 0.20.0 on 2× NVIDIA H200. All benchmarks are real, measured live on the described hardware.

How to setup Nemotron 3 Super 120B + vLLM as local LLM on Nvidia RTX6000 96GB VRAM

Introduction

Running a 120-billion-parameter reasoning model on a single workstation GPU used to be impossible. In 2026, thanks to NVIDIA’s NVFP4 quantization and the Blackwell architecture’s native FP4 tensor cores, it is now a practical afternoon project. This article walks through the complete, end-to-end deployment of nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 on a single RTX PRO 6000 Blackwell (96 GB) — every command, every pitfall, and the real benchmarks at the end.

We will cover: connecting to the server, choosing the right model artifact, installing the serving stack, fixing the four problems you will hit on Blackwell, and measuring throughput.

The Target Hardware

Component Specification
GPU NVIDIA RTX PRO 6000 Blackwell Workstation Edition
VRAM 96 GB (97,887 MiB)
Compute Capability sm_120 (Blackwell)
Driver 580.95.05 / CUDA 13.0
System RAM 147 GB
Storage 485 GB NVMe
OS Ubuntu 22.04

The Model: Nemotron-3-Super-120B-A12B

This is the star of the show. Nemotron-3-Super-120B-A12B is a hybrid Latent Mixture-of-Experts model:

  • 120 billion total parameters, but only 12 billion active per token (22 experts routed out of 512 per layer). This is why it runs fast despite its size.
  • Architecture: interleaved Mamba-2 state-space layers, MoE layers, and select attention layers. The Mamba layers have O(1) recurrent state — they do not grow a KV cache — which is exactly how the model supports a 1,048,576-token (1M) context on a single GPU.
  • NVFP4 quantization: experts are stored in 4-bit floating point (group size 16); attention and shared layers remain FP8. This shrinks the weights to ~80 GB, fitting comfortably in 96 GB VRAM with room for a large KV cache.
  • Capabilities: configurable reasoning/thinking mode (via the chat template’s enable_thinking flag), native tool calling, and a 1M-token context window.

On Hugging Face it lives at nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4. It is public and ungated — no token or license-acceptance gate is required to download it.

Important naming note: There is no model literally called “Nemotron 3 Super NVFP4.” The official repository id is nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4. An older, unrelated family member (nvidia/Llama-3.3-Nemotron-Super-49B-v1) is a DeciLM/NAS model — make sure you grab the right one.

Step 1 — Create a Python Environment

We do not need a full CUDA toolkit on the host; vLLM ships with everything inside its Docker image. We do need a small venv on the host for downloading the model and running benchmark scripts.

python3 -m venv /opt/nemotron-venv
/opt/nemotron-venv/bin/pip install --upgrade pip wheel setuptools
/opt/nemotron-venv/bin/pip install "huggingface_hub[cli]" hf_transfer

Step 2 — Download the Model (80 GB)

export HF_HUB_CACHE=/opt/models
mkdir -p /opt/models

/opt/nemotron-venv/bin/python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
    "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
    cache_dir="/opt/models",
    max_workers=16,
)
PY

This pulls 17 safetensors shards (~80 GB) plus the chat template and a custom reasoning parser (super_v3_reasoning_parser.py). Copy the parser somewhere stable — we will mount it into the container:

MODEL_PATH=$(find /opt/models -type d -path "*Nemotron-3-Super*/snapshots/*" | head -1)
cp "$MODEL_PATH/super_v3_reasoning_parser.py" /opt/super_v3_reasoning_parser.py

Step 3 — Generate an API Key

API_KEY="nm-$(openssl rand -hex 24)"
echo "$API_KEY"
mkdir -p /opt/nemotron-serve
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
chmod 600 /opt/nemotron-serve/.api_key

Keep this key secret — every request to the server must carry it as a Bearer token.

Step 4 — Fix Docker GPU Passthrough (Pitfall #1)

The host had Docker installed but no nvidia-container-toolkit, so --gpus all failed with could not select device driver "" with capabilities: [[gpu]]. The NVIDIA apt repository was already configured, so the fix was a one-liner:

apt-get install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Verify the GPU is visible inside a container:

docker run --rm --runtime=nvidia --gpus all ubuntu:22.04 \
  nvidia-smi --query-gpu=name,memory.total --format=csv,noheader

# NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 97887 MiB

Step 5 — Pull the vLLM Image

The model README pins vLLM 0.20.0 with a purpose-built Blackwell image. Do not be tempted by a newer pip install vllm — the pip path hit two more problems (see the pitfalls below). Use the official image, which bundles a matched CUDA 13.x toolkit:

docker pull vllm/vllm-openai:v0.20.0

Step 6 — Launch the Server

Create the launch script. The flags come straight from the model’s official README, with two critical additions discovered during bring-up (explained in the pitfalls section).

cat > /opt/nemotron-serve/launch_docker.sh <<'EOF'
#!/bin/bash
set -euo pipefail

API_KEY="$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)"
MODEL_PATH="/root/.cache/huggingface/models--nvidia--NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4/snapshots/4f0cf9daaeb7a4d5e23f80a00e7ed15f0e03caf6"

docker rm -f nemotron-serve 2>/dev/null || true

exec docker run --name nemotron-serve \
  --runtime nvidia --gpus all \
  --network host \
  --shm-size 16g \
  --restart unless-stopped \
  -e VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 \
  -e HF_HUB_OFFLINE=1 \
  -e HF_HOME=/root/.cache/huggingface \
  -e HF_MODULES_CACHE=/root/.cache/hf_modules \
  -v /opt/models:/root/.cache/huggingface:ro \
  -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw \
  -v /opt/super_v3_reasoning_parser.py:/app/super_v3_reasoning_parser.py:ro \
  vllm/vllm-openai:v0.20.0 \
    --model "$MODEL_PATH" \
    --served-model-name nvidia/nemotron-3-super \
    --host 0.0.0.0 \
    --port 20000 \
    --api-key "$API_KEY" \
    --async-scheduling \
    --dtype auto \
    --kv-cache-dtype fp8 \
    --tensor-parallel-size 1 \
    --trust-remote-code \
    --gpu-memory-utilization 0.90 \
    --enable-chunked-prefill \
    --max-model-len 1048576 \
    --max-num-seqs 32 \
    --mamba-ssm-cache-dtype float16 \
    --reasoning-parser-plugin /app/super_v3_reasoning_parser.py \
    --reasoning-parser super_v3 \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder
EOF
chmod +x /opt/nemotron-serve/launch_docker.sh

# writable HF modules dir for trust-remote-code
mkdir -p /opt/hf-cache/modules

# launch
nohup /opt/nemotron-serve/launch_docker.sh > /opt/nemotron-serve/docker.log 2>&1 &

What each key flag does

  • --max-model-len 1048576 + VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 — unlocks the full 1M-token context (the model’s max_position_embeddings is 262144, but the Mamba/attention hybrid legitimately extends to 1M; the env var lets vLLM exceed the conservative default).
  • --kv-cache-dtype fp8 — FP8 KV cache, halving attention memory and fitting longer contexts in VRAM.
  • --mamba-ssm-cache-dtype float16 — the Mamba-2 recurrent state in fp16 (required for the hybrid layers).
  • --reasoning-parser super_v3 + --reasoning-parser-plugin — loads the custom parser that splits reasoning traces into the reasoning_content field.
  • --enable-auto-tool-choice --tool-call-parser qwen3_coder — enables function-calling in the OpenAI-compatible API.
  • --max-num-seqs 32 — see Pitfall #4.

Startup takes about 90 seconds: ~25 s to load 80 GB of weights, then ~60 s for torch.compile and CUDA graph capture. The server is ready when the log shows:

INFO ... Application startup complete.

Step 7 — Verify It Works

API_KEY=$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)

# list models — confirms the 1M context
curl -s http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY" | python3 -m json.tool

# chat
curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"What is 17 * 23?"}],
    "max_tokens": 500,
    "temperature": 1.0,
    "top_p": 0.95
  }'

The Four Pitfalls (and Their Fixes)

I hit all four of these. If you follow the script above you will avoid them — but they are worth understanding.

Pitfall #1 — Docker GPU passthrough missing

Symptom: could not select device driver "" with capabilities: [[gpu]].

Cause: nvidia-container-toolkit not installed.

Fix: apt-get install -y nvidia-container-toolkit && nvidia-ctk runtime configure --runtime=docker && systemctl restart docker.

Pitfall #2 — Read-only filesystem for trust-remote-code

Symptom: OSError: [Errno 30] Read-only file system: '/root/.cache/huggingface/modules'.

Cause: The model uses trust-remote-code, which writes its custom nemotron_h Python module into a cache directory. I had mounted the HF cache as :ro.

Fix: Keep the weights mount read-only, but add a separate writable volume for HF_MODULES_CACHE: -e HF_MODULES_CACHE=/root/.cache/hf_modules -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw.

Pitfall #3 — max_num_seqs exceeds available Mamba blocks

Symptom: ValueError: max_num_seqs (1024) exceeds available Mamba cache blocks (685). Each decode sequence requires one Mamba cache block, so CUDA graph capture cannot proceed.

Cause: The hybrid Mamba-2 layers allocate one fixed-size state block per concurrent sequence. vLLM’s default max_num_seqs of 1024 was too high for the VRAM left after the 69 GB of weights.

Fix: Add --max-num-seqs 32 (the model README suggests 4 for DGX Spark, but 32 fits comfortably in the 685 available blocks and gives better batch throughput).

Pitfall #4 — Why not just pip install vllm?

I tried a native pip install vllm (which pulled v0.26.0 with torch 2.11 / CUDA 13). It hit two Blackwell-specific JIT problems that the Docker image avoids entirely:

  1. Missing nvcc at runtime: vLLM’s CUTLASS NVFP4 kernels need to JIT-compile. The host had no CUDA toolkit, only the driver. Fixable by pointing CUDA_HOME at the pip-installed nvidia/cu13 package — but that led to problem #2.
  2. CUDA compiler / header mismatch: FlashInfer’s sm_120 FP4/NVFP4 GEMM JIT (gen_gemm_sm120) failed with #error "CUDA compiler and CUDA toolkit headers are incompatible". The pip nvidia-cuda-nvcc (13.3) disagreed with the bundled CCCL/libcudacxx headers.

The purpose-built vllm/vllm-openai:v0.20.0 image contains a coherently-matched CUDA 13.x toolchain, so all the NVFP4 kernels compile and load cleanly on the first try. Use the Docker image.

Benchmarks

Measured against the live server with a single concurrent request:

Test Prompt Output Wall time Throughput
Short generation 25 tok 60 tok 0.66 s 90.3 tok/s
Medium generation 28 tok 300 tok 3.20 s 93.7 tok/s
Long generation 38 tok 800 tok 8.51 s 94.0 tok/s
Large prefill 2,427 tok 120 tok 1.43 s ~1,780 tok/s (prefill+decode)

~94 tokens/second steady-state decode from a 120B-parameter model on a single workstation GPU. That is remarkable, and it comes from three things working together: NVFP4 weights (4× compression with minimal accuracy loss), Blackwell’s native FP4 tensor cores, and the 12B-active Mamba/MoE hybrid architecture that keeps the per-token FLOPs low. Throughput is flat across output lengths, confirming the KV and Mamba caches are healthy.

Using the Server

The server speaks the standard OpenAI API. Point any OpenAI-compatible client at it:

from openai import OpenAI

client = OpenAI(
    base_url="http://175.28.230.22:20000/v1",
    api_key="nm-<your-key>",
)

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Design a REST API for a todo app."}],
    max_tokens=2000,
    temperature=1.0,
    top_p=0.95,
)
print(resp.choices[0].message.content)

Tool calling works the same way — pass tools=[...] and the model emits structured function calls. Multi-turn conversations work as expected (pass the full message history).

Thinking mode

The model’s chat template defaults to enable_thinking=True. When thinking is on, the model first produces a reasoning trace inside <think>...</think> tags, then the final answer. The super_v3 reasoning parser splits these into the response’s reasoning_content and content fields. To toggle it per-request:

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Prove the Pythagorean theorem."}],
    extra_body={"chat_template_kwargs": {"enable_thinking": True}},
    ...
)

Operations Cheatsheet

# check container status
docker ps --filter name=nemotron-serve

# tail logs
docker logs -f nemotron-serve

# restart (picks up the launch script)
docker restart nemotron-serve

# GPU usage
nvidia-smi

# rotate the API key
API_KEY="nm-$(openssl rand -hex 24)"
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
docker restart nemotron-serve

Conclusion

The era of “a 120-billion-parameter model on one GPU” has arrived. The Nemotron-3-Super-120B-A12B design — LatentMoE with Mamba-2 hybrid layers, trained natively in NVFP4 — is purpose-built for exactly this hardware moment. With an RTX PRO 6000 Blackwell you get a 1M-context reasoning model at 94 tok/s, behind a standard OpenAI-compatible API, for the cost of the electricity to run one card.

The tricky parts are all infrastructure: getting Docker GPU passthrough working, matching the CUDA toolchain for the NVFP4 JIT kernels, and sizing the Mamba/sequence caches correctly. Once those are solved (and the launch script above solves them), the deployment is stable and fast.


Model: NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4, governed by the NVIDIA Nemotron Open Model License. Serving: vLLM 0.20.0. All benchmarks are real, measured live on the described hardware.

Running Kimi K3 model with 4GB VRAM – using AirWrapperLLM and AirLLM

Introduction

Running a model too large for your GPU is a problem most people solve by giving up. AirLLM takes the opposite approach: it streams the model’s layers (or expert sublayers, for MoE) from disk into the GPU one at a time, so even a 2.8-trillion-parameter model like Kimi K3 can run on a single 16 GB card. The catch is that AirLLM is a Python library, not a server — there is no HTTP endpoint, no API key, no streaming, no tool-call parsing. You have to write that yourself.

That’s what AirWrapperLLM is: an OpenAI-compatible FastAPI server that wraps AirLLM and gives it the surface every modern agent expects. This guide walks through the complete install + first-run, including the hardware you’ll actually need and the speed you’ll actually get — both of which AirLLM’s marketing sometimes glosses over.

What AirWrapperLLM gives you

After one pip install and one server start, you have:

  • POST /v1/chat/completions — full OpenAI schema, both streaming and non-streaming responses.
  • GET /v1/models — model listing.
  • GET /health — liveness check, reports server version + uptime.
  • Bearer-token API-key auth — the key is auto-generated on first start and persisted with chmod 600.
  • Multi-turn conversations — pass message history, the tokenizer’s chat template renders it.
  • Thinking / reasoning mode — the model’s <think> trace is split out into message.reasoning.
  • Tool / function calling — pass tools=[...] in the standard OpenAI format; AirWrapperLLM parses the model’s XTML <tools> channel and returns structured tool_calls.
  • Thinking-effort controlreasoning={"effort": "low" | "medium" | "high" | "max"} in the request body, or lower-level chat_template_kwargs.

The XTML parser is a standalone, dependency-free module (airwrapper_xtml.py) with 7 unit tests. If you want to build your own service around AirLLM, you can lift the parser straight out.

Use it as a backend for your agent

AirWrapperLLM speaks the standard OpenAI HTTP API, so any agent that supports an OpenAI-compatible base_url works against it without code changes. Just point the agent at http://host:port/v1 with the Bearer token:

Agent Configuration
Hermes Agent Edit ~/.hermes/config.yaml: set model.base_url: "http://<host>:20002/v1" and model.api_key: "<your air-... key>".
OpenClaw / openclaw-agent Set LLM_BASE_URL=http://<host>:20002/v1 and LLM_API_KEY=<your air-... key>.
OpenCode Set OPENAI_API_BASE=http://<host>:20002/v1 and OPENAI_API_KEY=<your air-... key>.
Aider / Cline / Continue / LangChain / LiteLLM Wherever the agent asks for an OpenAI base_url and api_key, supply http://<host>:20002/v1 and your air-... key.

What works through the agent: multi-turn, tool calling, reasoning/thinking. What to watch out for: latency is high (first-token takes tens of seconds to minutes; tokens then trickle out as layers stream from disk), there is no live token-by-token streaming (the full sequence arrives in one batch), and the server is single-tenant — one generation at a time, period.

Hardware requirements (be honest with yourself)

AirLLM’s tagline is “run any model on any GPU.” That’s true in the sense that the model will load and produce output. It is not true in the sense that it will be fast. Before you commit, measure against this table:

Scenario GPU you need Disk space Realistic throughput
Kimi K3 (2.8T params, FP8) 1× any CUDA GPU with 8–16 GB ~1.5 TB during layer-split ~0.5–1 tok/s
DeepSeek-V3 (671B) 1× RTX 5090 / 6000 Ada (32 GB) ~340 GB ~2–5 tok/s
Llama 3.1 405B 1× A100 (80 GB) ~240 GB ~5–10 tok/s
Qwen3 32B 1× any 8 GB+ GPU ~65 GB ~10–25 tok/s

Three hardware rules that aren’t optional:

  1. Disk speed matters as much as GPU speed. The model is read off NVMe for every token. A SATA SSD will make 0.5 tok/s into 0.05 tok/s. Get NVMe with at least 1 GB/s sequential read.
  2. RAM matters too. AirLLM can pin host memory for layer prefetching; 16 GB is a minimum, 64 GB is comfortable for the 100B+ class.
  3. You need 2–3× the model size in free disk during the initial layer-split transform (the original Hugging Face snapshot plus the per-shard directory).

If you need vLLM-class speed (dozens to hundreds of tok/s), AirLLM is the wrong tool. AirLLM is the right tool when the model is too big to fit any other way and you’re willing to wait.

Install

Clone and create a virtualenv:

git clone https://github.com/sufehmi/AirWrapperLLM.git
cd AirWrapperLLM
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip wheel setuptools

Install PyTorch with the CUDA wheel matching your driver. The README’s recommendation is CUDA 12.8 for Blackwell GPUs (RTX 50-series, cc 10.0/12.0); check vast-capabilities | jq .hardware.gpu.cuda or your equivalent to confirm:

pip install torch --index-url https://download.pytorch.org/whl/cu128

Then everything else:

pip install -r requirements.txt

The pinned requirements include AirLLM 3.x, transformers 4.56.x (some remote-code models — Kimi K3 — refuse to load on 5.x), tiktoken, fla-core, bitsandbytes, compressed-tensors, fastapi, uvicorn, pydantic.

If your model uses flash attention, install it against the CUDA toolkit that matches your torch wheel:

CUDA_HOME=/usr/local/cuda-12.8 pip install flash-attn --no-build-isolation

There are no prebuilt flash-attn wheels for torch 2.11, so this takes a few minutes to compile. On Blackwell (cc 12.0) the compile is straightforward; on older arches you may need a CUDA toolkit match the driver, and on Hopper (cc 9.0) flash-attn 2.8.3 builds cleanly against CUDA 12.x.

Download a model

AirWrapperLLM is engine-agnostic over any AirLLM-compatible checkpoint. The current AirLLM readme highlights Kimi K3; smaller alternatives (Qwen3 32B, Llama 3.1 70B, DeepSeek-V3) work the same way.

python3 -c "
from huggingface_hub import snapshot_download
snapshot_download(
    'nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8',  # or any AirLLM-compatible model
    local_dir='./models/model-name',
    max_workers=16,
)
"

With max_workers=16 and a fast connection, expect roughly 10–15 minutes per 100 GB of weights. The Kimi K3 download alone is around 1.5 TB.

Start the server

python airwrapper.py \
  --model ./models/model-name \
  --host 0.0.0.0 \
  --port 20002 \
  --max-seq-len 32768 \
  --compression 4bit \
  --delete-original

Or use the bundled launch wrapper, which sources the right environment variables first:

./launch_docker.sh

On a successful start you’ll see:

================================================================
AirWrapperLLM — OpenAI-compatible server for AirLLM
================================================================
API key : air-XXXXXXXXXXXXXXXXXXXXXXXXXXXX
Model   : ./models/model-name
Listen  : 0.0.0.0:20002
Compress: 4bit
Delete original after split: True
Max seq : 32768
================================================================
...
[AirWrapperLLM] Model loaded in 1500s.
[AirWrapperLLM] Starting uvicorn on 0.0.0.0:20002
INFO:     Uvicorn running on http://0.0.0.0:20002

The first start is slow — AirLLM splits the checkpoint into per-layer shards on disk (and optionally 4bit/8bit-compresses them). Subsequent starts reuse the cached shards and are fast.

CLI flags and environment variables

Flag / env var Default Notes
--model / AIRWRAPPER_MODEL /workspace/kimi-k3 Path to a downloaded Hugging Face checkpoint.
--host / AIRWRAPPER_HOST 0.0.0.0 Bind address.
--port / AIRWRAPPER_PORT 20002 HTTP port.
--compression / AIRWRAPPER_COMPRESSION (empty) 4bit or 8bit. Halves disk reads at the cost of disabling layer prefetching.
--delete-original false After the first load, delete the original HF snapshot to save disk.
--dtype / AIRWRAPPER_DTYPE auto (bf16) Runtime dtype. bfloat16 is recommended for modern models.
--max-seq-len / AIRWRAPPER_MAX_SEQ_LEN 1048576 Maximum sequence length (context + generation). Memory use of attention layers scales with this; Mamba/SSM layers do not.
AIRWRAPPER_API_KEY_FILE /workspace/.airwrapper_api_key Where the API key is read/written.
AIRWRAPPER_DEVICE cuda:0 CUDA device for AirLLM to stream into.

Test it

Copy the API key from the server’s stdout, then:

API_KEY=air-XXXXXXXXXXXXXXXXXXXXXXXXXXXX

# Health check (no auth required)
curl http://localhost:20002/health
# {"status":"ready","version":"1.0.0","model":"...","uptime":...}

# Models
curl http://localhost:20002/v1/models \
  -H "Authorization: Bearer $API_KEY"

# Chat with thinking mode
curl http://localhost:20002/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "messages": [{"role":"user","content":"What is 17 * 23?"}],
    "max_tokens": 200,
    "temperature": 0.6,
    "reasoning": {"effort": "auto"}
  }'

Python with the official OpenAI SDK:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:20002/v1",
    api_key="air-XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
)

resp = client.chat.completions.create(
    model="your-model",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=100,
    extra_body={"reasoning": {"effort": "auto"}},
)
print(resp.choices[0].message.reasoning)   # thinking trace, if any
print(resp.choices[0].message.content)     # final answer

Tool calling

curl http://localhost:20002/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "messages": [{"role":"user","content":"What is the weather in Tokyo?"}],
    "tools": [{"type":"function","function":{
      "name":"get_weather",
      "description":"Get the current weather for a city",
      "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
    }}]
  }'

The response includes structured tool_calls:

{
  "choices": [{
    "finish_reason": "tool_calls",
    "message": {
      "role": "assistant",
      "tool_calls": [{
        "id": "call_abc123...",
        "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"Tokyo\"}"}
      }]
    }
  }]
}

Multi-turn

Just pass the full conversation history — the tokenizer’s chat template handles rendering:

resp = client.chat.completions.create(
    model="your-model",
    messages=[
        {"role": "user", "content": "My name is Alice. Remember it."},
        {"role": "assistant", "content": "Nice to meet you, Alice!"},
        {"role": "user", "content": "What is my name?"},
    ],
    max_tokens=50,
)

Run as a managed service

For production deployments you’ll want auto-restart on crash. The launch wrapper is supervisor-friendly:

[program:airwrapper]
environment=PROC_NAME="%(program_name)s"
command=/path/to/AirWrapperLLM/launch_docker.sh
autostart=true
autorestart=unexpected
startsecs=2
stopasgroup=true
killasgroup=true
stopsignal=TERM
stdout_logfile=/dev/stdout
redirect_stderr=true
stdout_logfile_maxbytes=0

Or under systemd. The server is single-process, single-GPU, so a simple supervisor/service is sufficient — no clustering, no shared state.

Operational notes

Rotating the API key: kill the server, regenerate the key file, restart.

echo "API_KEY=air-$(openssl rand -hex 24)" > /path/to/.airwrapper_api_key
chmod 600 /path/to/.airwrapper_api_key
supervisorctl restart airwrapper

Inspecting logs: with the supervisor config above, logs go to /dev/stdout and surface in the supervisor journal. The model’s quantization + loading phase prints tens of thousands of progress-bar lines — pipe through grep -v "Compressing\|Applying quant" if you want clean output.

When generation hangs: AirLLM is single-tenant. If a request times out on the client, the server still holds the generation lock. Wait for the in-flight run to finish (or use supervisorctl restart airwrapper to hard-stop — the server doesn’t expose a “cancel” signal yet).

Why no live token streaming? AirLLM returns the full sequence only after the last token. AirWrapperLLM emits the parsed result as SSE deltas when the run completes, not token-by-token as they generate. The OpenAI stream=true contract is honored, but in practice clients will see all tokens arrive at once. Improving this requires changes to AirLLM itself.

Limitations

  • Single request at a time. AirLLM has no batched-inference path. Two requests in flight queue; they don’t run in parallel.
  • No live token-by-token streaming. See above.
  • No LoRA / adapter support. AirLLM doesn’t expose this.
  • XTML parser is Kimi-K3-aware. Other models that use a different structural-token scheme may need a tweaked parser.
  • No multi-GPU. AirLLM runs on one device by design; AirWrapperLLM cannot override that without a different engine underneath.

Conclusion

AirWrapperLLM turns AirLLM’s “run any model from disk” engine into a normal OpenAI-compatible HTTP service: API key auth, multi-turn, thinking mode, tool calling, versioned releases. If you have a model that’s too big for your VRAM and you’re willing to wait for tokens, it’s the cleanest end-to-end stack available in 2026.

If you want real speed, use vLLM with a model that fits your VRAM. AirLLM and AirWrapperLLM exist for the case where that’s not an option.


AirWrapperLLM v1.0.0 — MIT licensed. Project: github.com/sufehmi/AirWrapperLLM. AirLLM is Apache 2.0 by Gavin Li.

Setting Up the Enigma Conky Suite on Debian / MATE

A practical, step-by-step guide to installing and configuring the
rew62/enigma conky suite on a Debian-based system running MATE (or any X11 desktop).
It also covers the common “can’t open display” pitfall and
how to make the suite auto-start cleanly on every login.

What is Enigma?

Enigma is a modular conky suite for Linux desktops. Each widget is a separate
conky process that runs inside a tmux session, so you can launch, stop, and rearrange
widgets independently. Out of the box it gives you:

  • Arc (horizon, planets, sun/moon, weather)
  • Multi-month calendar and a Lua calendar (espcal)
  • Sweep ring clock and a solar dial ring
  • Combined Net/Sys/Disk widget, network traffic panel, disk I/O monitor
  • Earth satellite image viewer and a day/night world map
  • Weather forecast (NWS for the US, Met.no fallback elsewhere)
  • Now Playing with album art, spectrum EQ, and lyrics
  • Stock indices and stock ticker
  • Google Calendar month view, horoscope, and more

The repo is built and tested on Linux Mint 22.3 / Cinnamon, but it works on any
X11 desktop with conky-all 1.22.x. It needs conky compiled with Lua, Cairo, Mouse
events, ARGB visuals, Own window, Xft, and XDBE (double buffering).

1. Prerequisites

You’re going to need the following. Install them with apt:

sudo apt install -y --no-install-recommends \
    conky-all tmux fzf xdotool playerctl cava \
    imagemagick jq vnstat librsvg2-bin pulseaudio-utils \
    python3-ephem luarocks curl wget git ca-certificates \
    fonts-noto-core fonts-noto-color-emoji fonts-dejavu fonts-liberation

Quick heads-up if any package name isn’t available in your distro’s repos: the
author’s setup script also tries to install fonts-ibm-plex, which isn’t
in Debian’s default repositories. The bundled fonts shipped with the repo cover
essentially the same roles, so this is fine to skip.

Verify your conky has the required build features:

conky -v 2>&1 | grep -iE "cairo|mouse|argb|xft|own window|xdbe"

You should see Cairo, Mouse events, ARGB visual,
Own window, Xft, and XDBE all listed. If any are missing,
install the conky-all package or build conky from source with the
required flags.

2. Save your existing conky setup (if any)

If you already have a conky configuration you want to keep as a fallback, back
it up first:

BACKUP_DIR="$HOME/.conky_backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -av "$HOME/.config/conky" "$BACKUP_DIR/conky_config"
cp -av "$HOME/.config/autostart/conky.desktop" "$BACKUP_DIR/" 2>/dev/null
[ -e "$HOME/.conkyrc" ] && echo "$HOME/.conkyrc -> $(readlink "$HOME/.conkyrc")" \
    > "$BACKUP_DIR/symlink_info.txt"
echo "INTERFACE=$(ls /sys/class/net | grep -v lo | head -1)" \
    > "$BACKUP_DIR/sysinfo.txt"
echo "BATTERY=$(ls /sys/class/power_supply/ | grep -iE 'BAT' | head -1)" \
    >> "$BACKUP_DIR/sysinfo.txt"

3. Clone the repository

By convention the suite lives at ~/.conky/enigma. That’s the
default lookup path baked into every widget, so leave it there unless you have a
good reason to move it.

mkdir -p ~/.conky
cd ~/.conky
git clone https://github.com/rew62/enigma.git

4. Configure the environment

Most widgets read keys and location from ~/.conky/enigma/.env. The
suite’s own enigma-config.sh will generate this file, but writing it
by hand is faster and easier to automate. The keys are:

  • OWM_API_KEY — OpenWeatherMap API key (free). Leave as
    placeholder if you’ll only use the Met.no fallback.
  • FINNHUB_API_KEY — FinnHub API key (free). Only needed for the
    stock ticker widget.
  • CITY_ID, LAT, LON — your location.
    Jakarta, Indonesia: city ID 1642911, lat/lon about
    -6.2088, 106.8456.
  • UNITSmetric or imperial.
  • INTERFACE_NAME — your network interface (e.g. wlp2s0,
    eth0). Check with ip route | grep default.
  • DISK_DEV — your disk device (e.g. sda,
    nvme0n1). Check with lsblk -dno NAME,TYPE.
cat > ~/.conky/enigma/.env <<EOF
OWM_API_KEY=replace_me_openweathermap
FINNHUB_API_KEY=replace_me_finnhub
CITY_ID=1642911
UNITS=metric
LAT=-6.2088
LON=106.8456
INTERFACE_NAME=wlp2s0
DISK_DEV=sda
EOF
chmod 600 ~/.conky/enigma/.env

5. Install the bundled fonts

The widgets reference several display fonts that aren’t in the default
repositories. The repo ships them under fonts/; install them to your
user font directory and refresh the cache:

FONT_DIR="$HOME/.local/share/fonts"
mkdir -p "$FONT_DIR"
find ~/.conky/enigma/fonts -maxdepth 2 -type f \
    \( -iname "*.ttf" -o -iname "*.otf" \) -print0 \
  | while IFS= read -r -d '' src; do
        family=$(fc-query --format='%{family}\n' "$src" 2>/dev/null | head -n1)
        if [ -n "$family" ] && fc-list | grep -qiF "$family"; then
            echo "  skipped: $(basename "$src") [$family]"
        else
            cp "$src" "$FONT_DIR/$(basename "$src")"
            echo "  installed: $(basename "$src") [$family]"
        fi
    done
fc-cache -f "$FONT_DIR"

Verify the fonts are picked up:

fc-match "Orbitron"
fc-match "Oxanium"
fc-match "Barlow Condensed"
fc-match "MonaspiceNe Nerd Font"

6. The “can’t open display” problem

When you run etmux from a remote SSH session, a non-graphical
terminal, or directly from an autostart entry, conky will fail with
can't open display:. That’s because $DISPLAY and
$XAUTHORITY aren’t set in those contexts — conky needs them to know
which X server to draw on and which credentials to use.

On a typical laptop, the user session lives on display :0 with the
authority file at ~/.Xauthority. The fix is to set these env vars
before invoking etmux:

export DISPLAY=:0
export XAUTHORITY=/home/<your-username>/.Xauthority
~/.conky/enigma/etmux

Replace <your-username> with your actual account name
(helen in this guide’s example).

7. Patching etmux for headless launches

The repository’s etmux launcher ends with
tmux attach-session -t conky, which is meant to give you a live
windowed view of every widget. That’s nice when you launch from a terminal, but
it fails when the launcher is invoked from a non-interactive context (like the
graphical autostart system) because there’s no TTY to attach to.

Patch the final line of etmux so it only attaches when a TTY is
actually available:

if [ -t 0 ]; then
    tmux attach-session -t "$SESSION"
else
    echo "Suite launched in detached tmux session '$SESSION' (no TTY for attach)."
    echo "Run './etmux' from a terminal to attach."
fi

Leave the earlier attach-session inside the goto
subcommand alone — that path needs to attach so you can navigate to a specific
widget pane.

8. A reusable launcher script

Putting the env vars and idempotency into a small launcher keeps things tidy
and makes it easy to start the suite from any terminal.

mkdir -p ~/bin
cat > ~/bin/enigma <<'EOF'
#!/bin/bash
# Launch the Enigma conky suite with the user's X session env attached.
# Idempotent: if a previous suite is already running, tear it down first.
ENV_FILE="/home/<your-username>/.conky/enigma/.env"
[ -f "$ENV_FILE" ] && set -a && . "$ENV_FILE" && set +a

if tmux has-session -t conky 2>/dev/null; then
    /home/<your-username>/.conky/enigma/etmux quit >/dev/null 2>&1
    sleep 1
fi

pkill -u "$(id -u)" -f "conky -c" 2>/dev/null
sleep 1

exec /home/<your-username>/.conky/enigma/etmux "$@"
EOF
chmod +x ~/bin/enigma

After this, from any terminal you can simply type:

enigma             # launch the default group
enigma quit        # stop everything
enigma help        # list all widgets and groups

9. Autostart on every login

The MATE desktop (and GNOME, XFCE, Cinnamon) honours XDG autostart entries in
~/.config/autostart/. Drop a .desktop file in there to
have the suite launch on every login:

cat > ~/.config/autostart/conky.desktop <<EOF
[Desktop Entry]
Type=Application
Name=Conky (Enigma)
Comment=Enigma conky suite (default group, no horoscope)
Exec=env DISPLAY=:0 XAUTHORITY=/home/<your-username>/.Xauthority /home/<your-username>/bin/enigma
Icon=conky
Terminal=false
Categories=System;Monitor;
X-GNOME-Autostart-enabled=true
Hidden=false
EOF

The env DISPLAY=:0 XAUTHORITY=... prefix is what makes this work
reliably at login time, when the desktop environment hasn’t yet propagated those
variables to the autostart child process.

10. Choosing what runs

By default etmux launches the default group, which
is 15 widgets:

c e ec ed em en es ev g m si wf wt zen zkr

Mapping those codes to widgets:

Code Widget
c Multi-month calendar
e Earth satellite image viewer
ec Lua calendar (espcal)
ed Disk I/O monitor
em Day/night world map
en Network traffic panel
es System monitor
ev vnstat bandwidth summary
g Google Calendar month view
m Now Playing (music2)
si Stock indices
wf Weather forecast strip
wt Temperature bar
zen ENIGMA logotype
zkr Killroy Was Here

Notice that the h code (horoscope) is not in the default
group. If you want even fewer widgets, or you want to add specific ones, just pass
the codes as arguments:

~/bin/enigma t c wf wt    # ring clock, calendar, weather forecast, temperature bar
~/bin/enigma stack1       # named group: a sd st r t zkr
~/bin/enigma help         # full list of codes and groups

11. Quick troubleshooting

Suite launches but nothing appears on the desktop.
Confirm the widgets can actually open the display:

DISPLAY=:0 XAUTHORITY=$HOME/.Xauthority conky -c ~/.conky/enigma/widgets/multimon.rc

If you see can't open display, the env vars aren’t reaching the
widget. Re-check the autostart entry and the ~/bin/enigma launcher.

Some widgets show “no data” or empty fields.
Most likely an .env placeholder that wasn’t filled in. The weather
widgets, world map, and sun/moon calculations all need a real
LAT/LON.

Fonts look wrong.
Confirm the bundle was installed:

fc-match "Orbitron"      # should resolve to Orbitron, not Noto Sans
fc-match "Oxanium"       # should resolve to Oxanium

If they fall back to Noto, run fc-cache -f as your user and retry.

tmux session “conky” already exists.
You tried to launch a second suite while one was already running. Either run
enigma quit first, or use the launcher in
~/bin/enigma which handles this automatically.

Widgets flicker or look low-contrast.
The bundled sample-luma.sh script tries to read the desktop
wallpaper luminance to pick a transparent or opaque theme. If it can’t reach
the wallpaper daemon (e.g. on a fresh MATE session), it defaults to transparent.
Forcing /dev/shm/conky/enigma_luma=0.0 gives a transparent look;
setting it to 1.0 gives the opaque theme. Make
/dev/shm/conky writable if you want the script to write it:

sudo mkdir -p /dev/shm/conky
sudo chmod 1777 /dev/shm/conky

Recap

  1. Install the apt dependencies; verify conky has all required build features.
  2. Back up your existing conky config if any.
  3. Clone the repo to ~/.conky/enigma.
  4. Write ~/.conky/enigma/.env with your location and API keys.
  5. Install the bundled fonts and refresh the font cache.
  6. Patch etmux so it doesn’t crash when launched without a TTY.
  7. Add a reusable ~/bin/enigma launcher that sets
    DISPLAY and XAUTHORITY.
  8. Drop a .desktop file into ~/.config/autostart/
    using that launcher.

After that, the suite starts on every login, with the default group of 15
widgets ready to go, and you can fine-tune which ones to run from any terminal
with enigma <codes...>.

How to setup NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 on vLLM with 2x RTX 5090 – A full walkthrough of the install, the gotchas, and how to benchmark it

This article documents exactly what it took to get Nemotron-3-Nano-30B-A3B running on vLLM with 1M context, thinking mode, tool calling, and API-key auth on a 2x RTX 5090 (Blackwell, sm_120) box running Ubuntu 22.04. The model itself is a hybrid Mamba-2/MoE/Attention architecture with NVFP4 weights – one of the first production models that exercises every cutting- edge path in vLLM at once.

The article is split into:

1. The plan and feasibility math
2. The install
3. The eight things that go wrong on Blackwell + Nemotron-H + NVFP4
4. The launch script
5. Benchmarking (with real numbers)
6. Gotchas and lessons learned

1. THE PLAN

Hardware (confirmed via nvidia-smi):

2x NVIDIA GeForce RTX 5090 (32 GB each, sm_120 / Blackwell)
Driver 580.95.05, CUDA 13.0 available, 131 GB RAM, 478 GB disk free
Ubuntu 22.04.5 LTS, Python 3.10.12, no nvcc initially, port 20000 free

Model (nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4):

Architecture : NemotronHForCausalLM (hybrid Mamba-2 + MoE + 6 attn layers)
Quantization : NVFP4 (modelopt, weights ~16 GB, kv_cache_quant=fp8)
Native context : 262 144 (256k)
Hidden size : 2688
Layers : 52 (23 Mamba + 23 MoE + 6 Attention)
Attention heads : 32, KV heads = 2 (heavy GQA), head_dim = 128
MoE : 128 routed experts, 1 shared expert, top-6
Active params : ~3B (out of ~30B total)
Chat template : includes reasoning tags
Tool calling : Hermes format (chat_template.jinja ships in repo)
Gated : NO – public download, no HF token needed

Why 1M context actually fits on 32 GB GPUs:

    Only 6 of 52 layers have a KV cache. The Mamba-2 SSM is 
    recurrent with O(1) state per sequence (it does NOT 
    grow with context length), and the MoE layers have 
    no attention at all.

    KV @ 1M ctx (bf16, TP=2 replicated):
      = 2 (K+V) * 2 (KV heads) * 128 (head dim) * 2 bytes * 6 layers
      = 6.1 KB / token
      = 5.95 GB total per GPU at 1M tokens

    Add fp8 KV cache (NVIDIA pre-configured this in 
    hf_quant_config.json with kv_cache_quant_algo: FP8) 
    and we're at ~2.9 GB per GPU.

    So 1M context is comfortable; the trick is YaRN to extend 
    attention positions from the native 256k out to 1M.

2. THE INSTALL

Step 1 – Set up a Python 3.10 venv at /opt/vllm-venv:


apt-get install -y python3.10-venv
python3 -m venv /opt/vllm-venv
/opt/vllm-venv/bin/pip install --upgrade pip wheel setuptools

Step 2 – Install PyTorch + CUDA 13.0 system-wide (needed for nvcc + JIT header paths that vLLM workers look up via $CUDA_HOME):


DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
cuda-nvcc-13-0 \
cuda-cudart-dev-13-0 \
cuda-cccl-13-0 \
cuda-driver-dev-13-0 \
cuda-nvrtc-dev-13-0 \
libcusparse-dev-13-0 \
libcusparselt0-dev-cuda-13 \
libcublas-dev-13-0 \
libcurand-dev-13-0 \
libcudnn9-dev-cuda-13

Reasoning: vLLM nightly is a single abi3 wheel that bundles its own CUDA-13 Python runtime, but the worker subprocesses still spawn nvcc to JIT-compile flashinfer CUTLASS kernels for the local GPU arch.

That nvcc needs the matching dev headers; otherwise you get “fatal error: cublasLt.h: No such file or directory” deep inside a ninja build.

IMPORTANT: after this install, /usr/local/cuda is re-pointed via update-alternatives to cuda-13.0 (so compute_120 is in nvcc’s –list-gpu-arch output). If you ever install a different cuda-* package later, re-check that symlink – it’s the silent foot-gun.

Step 3 – Install vLLM nightly (stable has NO Blackwell cu128 wheel):


/opt/vllm-venv/bin/pip install --extra-index-url \
https://wheels.vllm.ai/nightly vllm

This pulls vllm-0.26.0rc1 + torch 2.11.0+cu130 + flashinfer 0.6.14 + a full CUDA-13 Python stack. The venv ends up around 11 GB.


/opt/vllm-venv/bin/pip install --quiet ninja huggingface_hub

Step 4 – Download the model:


/opt/vllm-venv/bin/hf download nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 \
--local-dir /root/models/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 \
--max-workers 8

~19 GB across 18 files (5 safetensors shards + chat_template.jinja + the nano_v3_reasoning_parser.py and modeling_nemotron_h.py that vLLM auto-imports via trust_remote_code-style auto_map).

Step 5 – Patch config.json to add YaRN rope scaling.

vLLM 0.26 dropped the –rope-scaling CLI flag (you set it in the model’s config.json now, or via JSON overrides baked in at serve time).

Edit /root/models/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4/config.json and add:


"rope_parameters": {
"rope_type": "yarn",
"factor": 4.0,
"original_max_position_embeddings": 262144
}

(New transformers >= 4.50 reads “rope_parameters”; “rope_scaling” still works as legacy fallback but you get a warning. Use rope_parameters.)

Also keep a backup so you can revert if you decide to stay at native 256k context:

cp config.json config.json.bak

3. THE EIGHT THINGS THAT GO WRONG (and exactly how to fix each)

If you skip straight to “vllm serve” you will hit, in order:

(1) “unrecognized arguments: –rope-scaling / –rope-theta / –swap-space” -> vLLM 0.26 removed these CLI flags. Move them into config.json (rope_parameters) and drop –swap-space entirely.

(2) “User-specified max_model_len (1056768) is greater than the derived max_model_len (max_position_embeddings=1048576.0)” -> Set max_model_len = 1048576 exactly (= 262144 * 4), not higher. If you must exceed it, export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 (it warns about NaN risk on out-of-range rope positions).

(3) “Could not find nvcc and default cuda_home=’/usr/local/cuda’ doesn’t exist” -> Install cuda-nvcc-12-8 OR cuda-nvcc-13-0 via apt, then set PATH=/usr/local/cuda/bin:$PATH in the launch env. For Blackwell sm_120 you need nvcc 13.0 – 12.8 errors with “SM 12.x requires CUDA >= 12.9” when probing GPU archs.

(4) “FlashInfer requires GPUs with sm75 or higher” -> Deceptive error: flashinfer isn’t failing to detect the GPU, it’s failing because its TARGET_CUDA_ARCHS list is empty. Caused by nvcc –list-gpu-arch returning empty. Fixed by installing nvcc 13.0 so compute_120 shows up.

(5) “‘ninja’ not found” -> pip install ninja into the venv, and put /opt/vllm-venv/bin FIRST in PATH so the worker subprocesses inherit it.

(6) “fatal error: curand_kernel.h: No such file or directory” -> apt install libcurand-dev-13-0. flashinfer CUTLASS paths include device-side curand headers for random sampling in MoE routing.

(7) “fatal error: cublasLt.h / nvrtc.h / cusparse.h not found” -> apt install libcublas-dev-13-0, cuda-nvrtc-dev-13-0, libcusparse-dev-13-0, cuda-cccl-13-0. These are the dev-13.0 packages; -dev-13-1/-13-2/-13-3 exist too but 13-0 matches what vLLM’s bundled nvcc 13.0.88 was compiled against.

(8) “Free memory on device cuda:1 (19.67/31.36 GiB) on startup is less than desired GPU memory utilization (0.9, 28.22 GiB)” -> A previous failed vLLM run left zombie VLLM::Worker_TP0/TP1 processes holding 11 GB each. pkill -f “vllm serve” doesn’t kill them because their process name is different. Use nvidia-smi –query-compute-apps=pid,process_name then `kill -9 ` directly. You can also drop gpu-memory-utilization to 0.85 to give yourself a 5% buffer.

There’s a 9th gotcha that’s not an error but will save you 10 minutes:

(9) The model directory must contain nano_v3_reasoning_parser.py at the root. Pass –reasoning-parser-plugin /nano_v3_reasoning_parser.py AND –reasoning-parser nano_v3 on the CLI. If you use a different parser name (like deepseek_r1) the reasoning extraction silently breaks – test it by checking that response.choices[0].message has a “reasoning” field, not just “content”.

4. THE LAUNCH SCRIPT


# /root/start-nemotron.sh - launches Nemotron vLLM fully detached
set -u
VENV=/opt/vllm-venv
MODEL_DIR=/root/models/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4
API_KEY=$(cat /root/vllm-api.key)
LOG=/var/log/vllm/nemotron.log
PIDFILE=/var/run/nemotron-vllm.pid

mkdir -p /var/log/vllm /var/run
pkill -9 -f "vllm serve" 2>/dev/null || true
sleep 2

export PATH=/opt/vllm-venv/bin:/usr/local/cuda/bin:$PATH
export CUDA_HOME=/usr/local/cuda
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export NCCL_P2P_DISABLE=0
export TOKENIZERS_PARALLELISM=false
export VLLM_LOGGING_LEVEL=INFO
export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1

# Generate the API key once:
# python -c "import secrets; print(secrets.token_urlsafe(32))" > /root/vllm-api.key
# chmod 600 /root/vllm-api.key

setsid bash -c "
exec $VENV/bin/vllm serve $MODEL_DIR \
--host 0.0.0.0 \
--port 20000 \
--served-model-name nemotron-nano \
--api-key $API_KEY \
--tensor-parallel-size 2 \
--max-model-len 1048576 \
--max-num-seqs 16 \
--gpu-memory-utilization 0.90 \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--reasoning-parser nano_v3 \
--reasoning-parser-plugin $MODEL_DIR/nano_v3_reasoning_parser.py \
--enable-prefix-caching \
--enable-chunked-prefill \
--dtype bfloat16 \
>> $LOG 2>&1
" /dev/null 2>&1 &
echo $! > "$PIDFILE"
disown 2>/dev/null || true
echo "Launched. PID=$PID, log=$LOG"

Run it with `bash /root/start-nemotron.sh`.

First boot takes 5-10 minutes because the worker JIT-compiles flashinfer CUTLASS sm_120 kernels (a few hundred cu files, ~150 nvcc invocations).

Subsequent restarts skip the kernel cache at /root/.cache/flashinfer/0.6.14/120f/ and start in ~60s.

Watch the log for the magic line: INFO: Application startup complete.

5. BENCHMARKS

Using the OpenAI-compatible /v1/chat/completions endpoint with stream=true and stream_options.include_usage=true:

SINGLE-STREAM DECODE (512 generation tokens, varying prompt size):

    prompt  gen      TTFT    total     decode_tok/s
    -----  ----     ----    -----     ------------
      243   145    0.16s    0.77s          189.4
      927   162    0.14s    0.81s          200.7
     3658   200    0.41s    1.23s          162.5
    14580   275    0.33s    1.43s          192.5
    58270   364    0.64s    2.11s          172.7
   116524   450    8.31s   10.13s           44.4   <- prompt processing dominates

LONG GENERATION (1024 output, 24-token prompt): prompt=24 gen=1024 total=4.36s decode_tok/s=235.0

CONCURRENT (8 parallel streams, 512 gen each): total=4096 tokens wall=3.24s aggregate=1263 tok/s (about 158 tok/s per stream under 8-way concurrency)

THINKING MODE (reasoning_effort=medium, "What is 17 * 24?"): gen=529 total=2.19s decode_tok/s=241.5 -> response.choices[0].message.reasoning has the chain-of-thought -> response.choices[0].message.content has the final answer

TOOL CALL (force tool_choice=function for "weather in Tokyo?"): tool_calls[0].function.name = "get_weather" tool_calls[0].function.arguments = '{"location":"Tokyo"}' -> Hermes parser emits clean OpenAI-format tool_calls

TOOL CALL WITHOUT tool_choice: Often returns the answer in plain text. The model has the ability but chooses prose. Force it with `tool_choice` or a clear system prompt like "You MUST call the get_weather function".

These numbers are sane for a 30B-A3B NVFP4 hybrid on 2x Blackwell. For comparison, the BF16 sibling would be ~2-2.5x slower on decode (weight memory bandwidth bound) and would NOT fit on 32 GB GPUs at 1M context.

6. LESSONS LEARNED

- NemotronH is a hybrid model. 23 of 52 layers are Mamba-2 (recurrent, fixed-state), 23 are MoE (no attention), only 6 are attention. So prompt-context cost is mostly MoE weight bandwidth and a small KV cache, NOT quadratic attention.

- vLLM nightly is the only path to Blackwell + NemotronH + NVFP4 right now. PyPI stable has no cu128/cu130 wheel for Blackwell, and the NemotronHForCausalLM modelopt loader landed in late-2025.

- The launch environment matters enormously. /usr/local/cuda must point to a CUDA version whose nvcc can list compute_120. PATH must include /opt/vllm-venv/bin (for ninja) and /usr/local/cuda/bin (for nvcc). CUDA_HOME must be set. All four are needed.

- The first cold start compiles a few hundred flashinfer CUTLASS cu files. Don't panic if the log is just ninja [N/56] lines for 5-10 minutes. Look for the actual fatal error (one of the eight above) and fix it; subsequent restarts are fast.

- After a failed launch, ALWAYS nvidia-smi --query-compute-apps before restarting. The VLLM::Worker_TP0/TP1 children survive pkill -f "vllm serve" because their argv[0] is different, and they hold ~11 GB of GPU memory each. Kill them by PID.

- Reasoning output goes in a separate field. response.choices[0].message has .reasoning (chain-of-thought) AND .content (final answer). If you only see .content, your reasoning parser isn't loaded correctly.

- For 1M context attention quality, expect some degradation past the native 256k. The model was trained with rope up to 262144; YaRN with factor=4 extends it but is not free. For workloads that mostly live under 256k, you can drop YaRN entirely (just remove the rope_parameters block) and you'll get full training-quality attention.

- The serve command does NOT need --trust-remote-code. vLLM 0.26 resolves NemotronHForCausalLM natively via the auto_map entry in config.json. Modeling code lives in the model dir.

- Bind to 0.0.0.0 if you want reachability from inside the same host or via SSH tunnel. If you want public reachability, open the cloud firewall (security group / cloud firewall rule / iptables) to allow inbound TCP on 20000 from your client IPs.

APPENDIX A - Quick health check

# Is it up?
curl -sS http://localhost:20000/v1/models \
-H "Authorization: Bearer $(cat /root/vllm-api.key)"

# Quick chat test
curl -sS http://localhost:20000/v1/chat/completions \
-H "Authorization: Bearer $(cat /root/vllm-api.key)" \
-H "Content-Type: application/json" \
-d '{
"model": "nemotron-nano",
"messages": [{"role":"user","content":"In one sentence, what is the capital of France?"}],
"max_tokens": 60,
"temperature": 0
}'

# GPU usage
nvidia-smi --query-gpu=index,memory.used,memory.free,utilization.gpu --format=csv

# vLLM logs
tail -f /var/log/vllm/nemotron.log

# Restart cleanly
pkill -9 -f "vllm serve"; nvidia-smi --query-compute-apps=pid \
| awk -F, 'NR>1 {print $1}' | xargs -r kill -9
bash /root/start-nemotron.sh

APPENDIX B - File layout on disk


/opt/vllm-venv/ Python 3.10 venv, 11 GB
/root/models/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4/
config.json patched with rope_parameters
config.json.bak original (no rope scaling)
chat_template.jinja reasoning + tool-call template
nano_v3_reasoning_parser.py reasoning parser plugin
modeling_nemotron_h.py remote modeling code
configuration_nemotron_h.py remote config class
model-00001-of-00005.safetensors ~4 GB each
...
/root/vllm-api.key API key, mode 0600
/root/start-nemotron.sh launch script
/var/log/vllm/nemotron.log server log
/var/run/nemotron-vllm.pid current PID
/root/.cache/flashinfer/0.6.14/120f/ JIT-compiled CUTLASS .so's
/root/.cache/vllm/torch_compile_cache/ AOT-compiled torch graphs

End of article. Tested August 2026
on 2x RTX 5090 + vLLM nightly 0.26.0

Indonesia dan Cybersecurity

Insiden hacking PDNS yang baru terjadi ini bukan suatu yang mengejutkan bagi mereka yang paham tentang kondisi cybersecurity di Indonesia. Misalnya, laporan “Hi-Tech Crime Trends Report 2023/2024” menyatakan bahwa Indonesia adalah juara / paling banyak mengalami insiden Ransomware di ASEAN. Sangat disayangkan, tapi itulah faktanya.

Di sisi lain, Indonesia menyimpan segudang talent / bakat luar biasa di bidang Cybersecurity ini. Sudah banyak yang muncul, ataupun memilih tidak muncul. Selalu ada harapan bahwa Indonesia bisa.

CYBERSECURITY

Topik ini terkesan sangat rumit dan sulit. Dan berbagai tokoh yang muncul juga tidak membantu, banyak yang malah memilih menebar jargon dan istilah-istilah yang memusingkan, sehingga terkesan cerdas – namun, tidak bermanfaat bagi masyarakat. Padahal cybersecurity itu seharusnya sederhana dan bisa disederhanakan.

Seperti berbagai pihak turut bekerja sama menjaga berbagai aspek keamanan di lingkungan sekitar kita, demikian pula dengan cybersecurity. Keamanan di dunia online, dunia siber, alias Cybersecurity, bisa dan perlu kita urus bersama.

Akademisi bisa membantu menyediakan data, informasi, melakukan riset, dan edukasi publik. Pemerintah bisa membuat aturan-aturan (public policies) yang memastikan terciptanya kondisi keamanan siber yang baik. Dan yang paling penting, masyarakat, bisa membantu mendampingi dan mengawasi pemerintah, dan saling mendidik sesama dalam berbagai hal ; seperti pemakaian perangkat yang aman, selalu berpikir kritis, agar tidak mudah menjadi bulan-bulanan para penipu digital.

Dunia siber adalah dunia yang luas sekali. Tidak mungkin hanya diurus oleh pemerintah, apalagi jika dibebankan hanya ke beberapa instansi. Kita semua bisa, dan perlu ikut serta juga.

APA HUBUNGANNYA DENGAN KITA ?

Ada satu hal yang menarik ketika berbincang dengan keluarga, salah satunya dengan ibu saya, tentang insiden hacking PDNS ini. Pertanyaan beliau sangat sederhana: “apa hubungan kejadian ini dengan saya?”

Apa hubungannya antara insiden yang menimpa institusi PDNS yang sangat besar itu, dengan saya, seorang masyarakat awam, ibu rumah tangga?

Masyarakat yang cerdas tentu akan mempertanyakan hal yang sama juga ; dan wajar jika kemudian pada menarik kesimpulan yang sama : tidak ada, ah itu urusannya yang diatas, dst.

Kesimpulan tersebut sangat bisa dipahami, karena mayoritas orang masih memisahkan dunia online dengan dunia offline.
Kita mengira bahwa dunia online, dunia siber, itu adalah sebuah dunia yang jauh nun disana. Beda planet. Sedikit sekali sentuhannya dengan kita. Dan seterusnya.

Sayangnya, tidak demikian halnya.

KITA DAN CYBERSECURITY

Berkat Internet, kini dunia siber sudah menyentuh semua aspek kehidupan kita. Dunia online dan dunia offline adalah dua dunia yang saling paralel berada di ruang yang sama, yaitu dunia kita.

Rekening bank kita sudah berada di dunia siber juga. Data-data pribadi kita ada / eksis di dua dunia tersebut juga. Berbagai kegiatan bahkan sosialisasi pun kita lakukan di dua dunia secara bersamaan. Dan seterusnya.

Insiden seperti PDNS, dampaknya bisa baru muncul & dirasakan belakangan. Tapi sayangnya, kadang buntutnya ini bisa terus berlangsung bahkan sampai bertahun-tahun setelah kejadian insidennya.

Misalnya, data yang bocor dari berbagai insiden hacking / peretasan itu bisa jatuh ke tangan sindikat penipu – yang kemudian jadi bisa menelpon kita, dan lalu berpura-pura menjadi bank atau polisi atau entitas lainnya; dan bisa meyakinkan karena tahu informasi yang seharusnya rahasia.

Atau data bocor tersebut jatuh ke tangan sindikat peretas, yang kemudian memakai data kita untuk melakukan transaksi dengan berbagai pengusaha pinjol (pinjaman online). Kemudian kita yang kebingungan karena tetiba dikejar-kejar oleh para debt collector.

Dan berbagai skenario lainnya, hanya terbatasi oleh imajinasi para oknum-oknum penjahat ini.

APA YANG BISA KITA LAKUKAN ?

Sungguh mengerikan membayangkan para penjahat yang gentayangan di dunia siber tapi bisa membahayakan kita. Apa yang bisa kita lakukan?

Untungnya senjata paling ampuh untuk melawan ini sudah kita miliki ; yaitu akal sehat.

Hanya perlu sering latihan untuk menggunakannya. Seperti senjata api yang perlu latihan agar bisa digunakan dengan baik, demikian pula dengan akal kita.

Perlu sering kita latih berpikir kritis, agar tidak mudah menjadi korban penipuan. Dapat kiriman surat undangan nikah? Jangan langsung klik – cek dulu pengirimnya. Tanya ke nama yang disebutkan di undangan tersebut, apakah memang mengirim surat undangan itu, atau sebetulnya itu virus? Dan seterusnya.

Tetiba dikejar debt collector pinjol bahkan sampai ke rumah? Jangan panik dan emosional – cari tahu apa aturan terkait hal ini. Cari tahu siapa saja yang bisa membantu kita; RT, RW, Babinsa, OJK, dan seterusnya. Kontak korban-korban serupa dan saling berbagi informasi & saling bekerjasama. Dan seterusnya.

Ada tawaran menggiurkan yang rasanya mustahil? Hampir bisa dipastikan, ya, itu mustahil, dan sebetulnya adalah penipuan.

Skeptis & berpikir kritis adalah senjata utama kita semua.

APA YANG PEMERINTAH BISA LAKUKAN ?

Terkait infrastruktur IT, cybersecurity musti menjadi pondasinya. Lalu semua hal lainnya dibangun di sekitar dan berdasarkan ini.

Karena security itu adalah proses, bukan suatu alat yang bisa dibeli.

Dokumen pengadaan / tender musti ditingkatkan kualitasnya. Tidak bisa cuma menuntut sertifikasi ini dan itu; namun musti membahas sampai detail rincian terkait berbagai kegiatan cybersecurity yang perlu dilakukan. Kawan-kawan akademisi bisa membantu disini, juga yang lainnya.

Vendor mahal dan ternama bukan jaminan. Banyak yang di dalamnya bekerja dengan sangat amatiran, dan tidak paham berbagai detail teknis yang diperlukan. Pemerintah musti kritis terkait kerjasamanya dalam bidang ini.

Cybersecurity musti menjadi prioritas utama bagi semua pihak. Maka yang lainnya otomatis akan jadi bisa berjalan dengan baik.

KESIMPULAN

Insiden PDNS ini, dengan segala aspeknya yang terkait, sebenarnya pada hakikatnya adalah “wake up call”, terapi kejut.

Ini yang kena serang baru PDNS, PDN Sementara.

Bukan PDN.
PDN yang sebenarnya masih dalam proses pembangunan di beberapa kota.

Namun, PDNS saja sudah seperti ini dampaknya. Luar biasa sekali.

Musti langsung kita sadari secara kolektif : Bagaimana jika PDN yang kena serang?

Apa yang akan terjadi? Apakah negeri ini akan jadi lumpuh seketika karenanya?

Maka bersama-sama kita harus mempersiapkan ini, agar tidak terjadi lagi.

Kita harus asumsikan yang terburuk yang bisa terjadi, dan lalu mempersiapkan untuk mencegahnya.
Seperti kata pepatah, “hope for the best, but plan for the worst”

Cukup PDNS saja menjadi korbannya. Cukuplah ini menjadi pelajaran bagi kita. Jangan ada lagi.

Dipublikasikan di Koran Tempo pada tanggal 1 Juli 2024 : https://koran.tempo.co/read/opini/489011/ransomware-pdns-dan-keamanan-siber

On Hoaxes / Mis-Disinformation

I’ve been interviewed by many researchers, and everyone pretty much always asked this particular question:

“Why fact-checking is not working?” or “Is it enough to solve the mis/disinformation problem by doing fact-checking?”

By which my answer is always the same: doing fact-checking, alone, will NEVER solve this problem.

There is no silver bullet for this monster.

We need to do so much in order to be able to combat hoax, our umbrella term for the whole spectrum of mis/disinformation, effectively.

We need to educate the public. We need to advocate the governments, so they can develop the right regulations & policies. We need to work together with academics and researchers, so they can pinpoint the right courses of actions. We need to work with journalists, so people will always have trusted sources of information. And of course we need to do hoax busting / fact-checking as well.

It’s a massive scope of work which require massive amount of efforts & resources.

Therefore I’m always so grateful to Mafindo’s volunteers. Due to their sheer numbers and depth & breadth of their skills capacity & capabilities, Mafindo has been able to do all of the above.

Indonesia is so lucky to have them. I hope we will always be able to support their efforts effectively.

Isometric Exercise – olahraga yang paling sehat

“Isometric Exercise” adalah olah raga yang membebani otot, namun tidak bergerak / minim gerakan. Contoh: planking, yoga, pilates, dll. Dan penelitian terbaru menemukan bahwa ini adalah jenis olah raga yang paling sehat.

Kelebihan isometric exercise daripada olahraga lainnya adalah (1) tidak membutuhkan ruang yang luas (2) bisa dilakukan tanpa alat, sehingga (3) jadi bisa dilakukan kapan saja

Beberapa contoh isometric exercises yang mudah dilakukan adalah sbb:

Overhead Hold

Otot yang terdampak : core, triceps, shoulder girdle, upper trapezius

Alat yang diperlukan : beban ringan, seperti barbel 2 kg, atau bahkan kaleng makanan.

  1. Angkat tangan ke atas, dan tahan. Pastikan otot core / tubuh terasa turut bereaksi.
  2. Pastikan tangan dianggkat lurus ke atas. Karena jika bengkok, maka otot yang terdampak akan beda (hanya biceps dan triceps)
  3. Tahan selama 20-30 detik (tapi jangan segan turunkan sebelum itu jika dirasa beban bisa terlepas dari tangan)
  4. Istirahat sejenak, lalu ulangi lagi dari poin 1
  5. Ulangi sebanyak 2 atau 3 kali

Variasi : lakukan dengan berdiri di satu kaki.

High plank

Alat yang diperlukan : tidak ada

Otot yang terdampak : abdominals, quadriceps, glutes, semua otot tangan, dada, pundak

  1. Start dengan posisi seperti push-up, dengan bertumpu di lutut
  2. Tegakkan kedua tangan, dan lalu luruskan kaki. Sehingga tubuh Anda jadi seperti di posisi saat naik di push-up. Pastikan tangan sama rata dengan pundak, kaki lurus, dan seluruh otot core terasa aktif.
  3. Tahan selama bisa, istirahat sejenak, lalu ulang 2 kali.

Variasi : bertumpu di lengan

Side Plank

Alat yang diperlukan : tidak ada

Otot yang terdampak : obliques (otot samping perut), spinal stabilizers, quadriceps, glutes, serratus anterior, shoulder stabilizers, hip abductors

  1. Mulai dengan berbaring menyamping.
  2. Bertumpu pada lengan, dan naikkan badan.
  3. Tangan yang satu lagi bisa di samping, atau diluruskan ke atas
  4. Pastikan postur tubuh lurus dari ujung kaki sampai kepala
  5. Tahan selama mungkin, istirahat sejenak, lalu ganti sisi.

Variasi : tangan tumpuan diluruskan.

Bahaya fenomena “Pakar Medsos”

Dunia medsos jadi memungkinkan siapa saja untuk tampil di panggung. Ini seperti pisau bermata dua – ketika yang muncul adalah yang baik, maka jadi banyak yang bisa mendapatkan manfaatnya.

Namun jika yang muncul adalah yang buruk, atau jahat – maka juga jadi banyak yang bisa dirugikan, atau terzalimi.

Salah satu fenomena yang sudah cukup lama marak adalah para pakar palsu. Mengandalkan penampilan yang memukau, dan kata-kata yang manis – mereka menipu & mengecoh banyak orang.


Salah satunya adalah Edy Nurhan, dengan akun @edynurhan di berbagai medsos- videonya sedang viral di WhatsApp, karena dianggap menyampaikan info yang bagus tentang diabetes.

Padahal sebenarnya banyak yang salah, dan bahkan menyesatkan.

Beberapa yang langsung jelas misalnya adalah:

1/ Fungsi insulin ketika ada gula yang berlebih adalah mengubahnya menjadi lemak.

Karena itu kelebihan gula / karbohidrat menyebabkan kegemukan.


2/ Menyarankan kentang daripada french fries = SALAH, keduanya tetap saja sama-sama karbohidrat.

Bagi yang sedang musti diet karbohidrat, klaim-klaim seperti ini bisa berdampak fatal.


3/ Menyarankan jagung daripada popcorn = SALAH, keduanya tetap saja sama-sama karbohidrat.

Bagi yang sedang musti diet karbohidrat, klaim-klaim seperti ini bisa berdampak fatal.


4/ Klaim bahwa “olahraga adalah yang paling penting” – SALAH, yang paling penting adalah kualitas asupan makanan.

Ini sudah menjadi pemahaman umum di kalangan pakar kebugaran/ fitness.Olahraga sekeras apapun, tapi makanan / asupannya tidak diubah – maka dampaknya akan minim.


5/ Anggap diabetes lebih berbahaya daripada covid19 = lebih bodoh daripada orang awam, bisa menyesatkan, dan bisa menyebabkan korban nyawa.

Kedua-duanya sama = berbahaya.

Tidak boleh malah pakai salah satunya untuk remehkan yang lainnya.Itu adalah kelakuan yang amat serampangan.


Semoga para soothsayer seperti ini, orang bodoh tapi manis mulutnya – segera musnah dari medsos Indonesia ; sehingga tidak lagi bisa menyesatkan masyarakat awam.

ref: video ybs yang sedang menyebar di WhatsApp:

Travel & Dietary Requirements of Harry Sufehmi

I’ve been traveling quite a lot lately, and in turn have been asked the same questions many times by the event organizer. And being rather forgetful – sometimes I forgot some of it, and had to trouble the EO at the last minutes.

So to avoid that from happening again, here they are:

  1. Allergy : I’m allergic to (human skin) dust.

    Please ask the hotel staff to clean the room’s AC filter before my arrival.
    Otherwise my throat will inflame, and I’ll have trouble breathing.

    I’d prefer hotel with “split AC” / dedicated AC unit (because the filter can be cleaned).
    I’ve had many problems with hotel with Central AC facility – in many cases I fell sick, probably because its AC filter can only be cleaned by its vendor / technician.

    Also if there’s carpet / sofa in the hotel room – please ask the hotel staff to vacuum it, extra clean.
    My house does not have carpet because it keeps triggering this allergy. So if the hotel room have carpet, it’s alright, as long as it’s clean.
    .
  2. Accommodation preference : Non-smoking, high floor, breakfast included.
    .
  3. Food / dietary requirement : Halal, or vegetarian, or vegan.

    I have very, very low alcohol tolerance. Just a little bit of it is enough to make me nauseous.
    Please ask the caterer to ensure that there’s no alcohol in my food & drink.

    I’m not a picky eater, so don’t worry, I’ll appreciate anything that’s provided.
    .
  4. Airplane : short flight = window seat. long flight (> 3 hours) = aisle seat.
    .
  5. Train : preferably single seat / not having someone else next to me.
    .
  6. Bus : sleeper seat if possible, otherwise anything is fine.
    .
  7. Venue / Event : strictly non-smoking please – I suffer from asthma, cigarette’s smoke will cause me breathing problems.

Thank you.

Your WordPress Website is Slow? – perhaps you have WP-Statistics plugin installed?

A few days ago a client contacted me and said that their website is down. I checked the server, and indeed the server is very overloaded. In total it got 36 cores – and they’re all 100% utilized.

As usual I checked the whole stack, but today it’s something different – the MySQL / database server was the culprit. It was performing very slowly, and in turn caused the webserver to slow down as well.

In the slow query log, some queries kept showing up with crazy query times, in tens of seconds.
For comparison – all of the other queries finishes in less than a second.

MySQL’s slow query log is your friend – it enable you to find problematic queries very quickly.

And all those slow queries are in these tables:

wp_statistics_useronline
wp_statistics_visitor

I checked the currently running queries with “mysqladmin processlist“, and almost all (hundreds of them) queued queries are those involving those tables, looking for specific content in the field “ip”. They’re all looked like these:

SELECT location FROM wp_statistics_visitor WHERE ip = '88.88.88.88'
SELECT * FROM wp_statistics_useronline WHERE ip = '99.99.99.99'

On a hunch, I checked the structure of those tables. And right enough, there’s no index for “ip”

An index can increase a query’s performance by a, very, significant amount.

When the size of those tables are big enough, and you have a higher traffic than usual (they just published a very important information that’s of interest to a lot of people) – then suddenly these seemingly innocent queries were able to bring down a 36-core server to its knees.

Anyway, now we know the culprit, the solution is easy enough:

alter table wp_statistics_useronline add index (ip);
alter table wp_statistics_visitor add index (ip);

And voilà – in an instant, the website was up again, and the CPU utilization dropped to nearly zero.

Everyone’s happy, and I have also notified the developers about the issue as well.

Manajemen Finansial

For English (and other) speakers: this is a post about general financial management. Click on the “Translate” button on the right to have this article translated into your language.

Tidak sengaja saya membaca artikel tentang FI/RE (Financial Independence/Retiring Early). Di dunia yang ideal, semua orang punya akses ke UBI (Universal Basic Income) – yaitu dimana Pemerintah menjamin bahwa setiap bulan Anda akan selalu mendapatkan sejumlah uang.

Namun ketika ini belum ada, maka kita perlu melakukan berbagai usaha lainnya agar kondisi finansial keluarga kita selalu aman. Artikel ini adalah catatan pribadi saya tentang berbagai informasi terkait hal ini.

Disclaimer: artikel ini hanya catatan pribadi saya. Keuntungan/kerugian finansial yang terjadi karena membaca isi artikel ini bukan tanggung jawab saya. Dengan mengakses artikel ini, maka Anda telah setuju dengan disclaimer ini.

Mayoritas orang hidup dari gajian ke gajian. Jumlah tabungan minim. Atau kalaupun ada tabungan/investasi, kurang dikelola – sehingga kalah cepat dari besaran inflasi
= jumlah uang Anda malah berkurang setiap tahunnya ……

INFLASI – Target untuk dikalahkan

Selama 2010 s/d 2020, rata-rata inflasi per tahun adalah 4.48%

Artinya : nilai uang Anda berkurang sebesar 4.48% setiap tahun.

Maka, strategi investasi / tabungan Anda harus memberikan hasil yang lebih besar dari ini.
(setelah dipotong pajak, zakat, komisi, dst).

Mari kita lihat beberapa strategi investasi / tabungan yang ada:

1. EMAS

Dengan rata-rata peningkatan nilai investasi sebesar 21,67% setiap tahun, emas jelas bisa mengalahkan gerogot inflasi pada harta Anda.

RESIKO KERUGIAN INVESTASI : rendah

Tantangan

  1. Penyimpanan : Safe deposit bank : bukan jaminan aman, di luar negeri sudah sering terjadi kasus safe deposit box yang lenyap.
  2. Penyimpanan : Rumah : ukuran emas memang kecil sehingga mudah disembunyikan, namun jangan sampai lupa tempatnya. Keberadaan emas di rumah juga bisa membahayakan penghuni jika sampai diketahui oleh penjahat.

PROS

  1. Ukuran kecil, mudah disimpan.
  2. Cukup likuid / mudah dijual kembali, beda dengan misalnya perak.

CONS

  1. Resiko menyimpan benda berharga yang berbentuk fisik.
  2. Harganya cenderung agak mahal.
  3. Musti paham emas seperti apa yang harganya stabil – beberapa bentuk emas, seperti dinar dll, harganya bisa jatuh cukup banyak ketika dijual kembali.

x

TANAH / RUMAH / PROPERTI

RESIKO KERUGIAN INVESTASI : Agak tinggi

PROS

  1. Jika tepat memilih lokasi, ROI (return on investment) bisa cukup tinggi.

CONS

  1. Sangat tergantung pada lokasi : jika salah pilih lokasi, malah bisa turun harga dan/atau sulit dijual kembali.
  2. Cenderung tidak likuid : susah dijual dalam waktu cepat, kecuali jika harganya diturunkan jauh di bawah harga pasar.
  3. Potensi resiko : mafia tanah : bukan sekali dua kali kejadian mendadak tanah sudah dikuasai oleh pihak lainnya, lengkap dengan sertifikat tanah asli.
  4. Modal tinggi : membutuhkan dana dalam jumlah besar untuk melakukan investasi jenis ini.

.

LAIN-LAIN

Tentu saja masih ada banyak sekali skema-skema tabungan & investasi lainnya. Misalnya di luar negeri ada berbagai institusi Fund Management yang bisa membantu mengelola dana Anda. Anda bisa turut bergabung dengan dana yang tidak besar dan cenderung aman.

Silakan jika ada saran / masukan, bisa disampaikan via Telegram atau Facebook di bawah ini.

.

Penutup

Artikel ini masih belum selesai, karena saya masih terus mendalami soal ini.
Artikel ini akan terus diperbaharui setiap kali saya mendapatkan informasi baru.

Berbagai data & chart di artikel ini bisa dilihat di Google Sheet ini :

Performa Berbagai Investasi versus Inflasi Indonesia” = https://docs.google.com/spreadsheets/d/1pRF3W8BuAYGyG5RVHtvN-DeZXuz2CEKCkzl5lm4XGKo/

Jika ada data yang ingin Anda sumbangkan, silakan kontak saya via Telegram di “sufehmi” (respons cepat), atau via Facebook https://www.facebook.com/sufehmi (jarang saya cek).
Sumbangan data silakan dikirim dengan format CSV, dan lalu akan saya gabungkan ke dokumen di atas.

Jika ada kekeliruan / koreksi / masukan, jangan segan kontak saya via Telegram / Facebook di atas.

Demikian artikel ini, semoga bermanfaat bagi Anda.

Referensi / Bacaan tambahan

  1. https://brianlovin.com/writing/investing-for-designers-and-developers
  2. https://i.imgur.com/Vlt0DOR.png

Cloud and DRC



In my years of experience as IT architect, it’s quite shocking to see how many institutions are slacking about their backup system once they moved to the cloud. Especially with their DRC (disaster recovery center). They thought that once they go “up” to the cloud, then it’s all right. No need to worry anymore with troublesome stuff such as backup.

As harsh as it may sound, my friend said that “cloud is other people’s computer”, and it’s a fact. And computer will fail. It’s just a matter of when, not if. And cloud did indeed fail from time to time.

When your organization does not have a solid backup system, then when the cloud fail – you are in for a very unpleasant experience.

“There’s no such thing as too much backup” – this is another principle that’s true. I have been in various data loss incidents, one of them were saved by the fifth (5th) backup mechanism. All other four failed.

But of course the implementation of the backup system will need to balance between levels of data safety and actual available resources.


A DRC can be of various shapes and sizes, customized to fit one’s system recovery needs versus available resources/budget. There are 8 levels of Disaster Preparedness, and we can choose the one that fits our needs & available resources.
But it simply has to exist. Any institution with data & systems considered important, need to have a working DRC facility.

And a DRC does not always have to be complex or expensive. There are ways to make a fully working DRC with minimum resources. And along time, it can be tweaked even further.

Moving to the cloud is not an excuse to avoid having a good backup strategy. We don’t need to be caught with our pants down.

Linux and Logitech MX Anywhere 3

This mouse feels good to touch. It just feels nice. The scroll wheel, called Magspeed Wheel, feels really good to use with its tactile feeling. However try pressing the black button in the middle – then it changed from Ratchet mode to Freespin mode , basically it flies. You can spin it really, really fast. Awesome.

However, Logitech does not provide its configuration software on Linux. But no worries, we can use LogiOps for that.

Copy-paste these lines to set it up ; these are for Ubuntu 20.04, if you’re using different Linux distro, you might need to change some of it.

sudo apt-get install -y install cmake libevdev-dev libudev-dev libconfig++-dev

cd /tmp ; wget https://github.com/PixlOne/logiops/archive/refs/heads/master.zip ; mkdir tmp ; cd tmp ; unzip ../master.zip ; cd logiops-master ; mkdir build ; cd build

cmake .. ; make ; sudo make install 

sudo nano /etc/logid.cfg

sudo systemctl enable --now logid

You may notice that we’re creating a configuration file name logid.cfg , there’s a [ guide to create it ], however some may find it confusing.

Therefore please find a sample logid.cfg for MX Anywhere 3 below. It will enable a reasonably nice usage of the device, and also enable you to change the mouse’s DPI by pressing the side buttons.

Enjoy.

// Logiops (Linux driver) configuration for Logitech MX Master 3.
// Includes gestures, smartshift, DPI.
// Tested on logid v0.2.2-35-g1c209ed.

// File location: /etc/logid.cfg

devices: ({
  name: "MX Anywhere 3";

  smartshift: {
    on: true;
    threshold: 15;
  };

  hiresscroll: {
    hires: true;
    invert: false;
    target: true;
       up: {
            mode: "Axis";
            axis: "REL_WHEEL_HI_RES";
            axis_multiplier: 1;
        },
        down: {
            mode: "Axis";
            axis: "REL_WHEEL_HI_RES";
            axis_multiplier: -1;
        },
  };

  dpi: 1600; // max=4000


    buttons: (
        {
            cid: 0x52;
            action =
            {
                type: "Gestures";
                gestures: (
                    {
                        direction: "Left";
                        mode: "OnInterval";
			interval: 10;
                        action =
                        {
                            type: "Keypress";
                            keys: ["KEY_VOLUMEDOWN"];
                        };
                    },
                    {
                        direction: "Right";
                        mode: "OnInterval";
			interval: 10;
                        action =
                        {
                            type: "Keypress";
                            keys: ["KEY_VOLUMEUP"];
                        };
                    },
                    {
                        direction: "None"
                        mode: "OnRelease";
                        action =
                        {
			    type: "Keypress";
			    keys: ["BTN_MIDDLE"];
                        }
                    }
                );
            };
        },
        {
            cid: 0x53;
            action =
            {
//                type: "Keypress";
//                keys: ["KEY_BACK"];
		type: "ChangeDPI";
              	inc: -1000;            
            };
	},
        {
            cid: 0x56;
            action =
            {
//                type: "Keypress";
//                keys: ["KEY_FORWARD"];
		type: "ChangeDPI";
              	inc: 1000;            
            };
	}
    );
}
);

How to play Roblox on Linux

I used to play Roblox with my kids from my Linux-based (Ubuntu) laptop – by having a virtual machine set up (on VirtualBox) with Windows, and play there. Of course it’s slow, but I still CAN play.

However one day Roblox decided to disable playing from Virtual Machine. And there goes my little bit of happiness with my children.

One day I found this article about using Steam Link to enable remote access to a Windows desktop, and it got me an idea – can I use this trick to play Roblox again from my laptop?

So I setup Google Chrome to be streamable from my gaming computer on my house’s second floor – and voila, it showed up in my Steam account indeed !

By invoking Google Chrome on my Steam app on Linux – then I can browse to Roblox.com, and then play all the games there.

Mission accomplished !


Use Steam to Stream Your Desktop Instead of Your Games : https://lifehacker.com/use-steam-to-stream-your-desktop-instead-of-your-games-1818722875

Setup OpenVPN Server on Proxmox LXC

I needed to do this, but all the tutorials that I could find are incomplete, or already outdated, such as this.

After hacking around for a while, here’s how to correctly setup OpenVPN server in a container on Proxmox:

(btw if you just need to setup an OpenVPN Server in a normal server / non-container, then just do the “in container” part below)

IN HOST

# create special device "tun" for OpenVPN
mkdir -p /devcontainer/net
mknod /devcontainer/net/tun c 10 200
chown 100000:100000 /devcontainer/net/tun

# enable your container to use that tun device
# change 124 into your container's number : pct list
echo "lxc.mount.entry: /devcontainer/net dev/net none bind,create=dir" >> /etc/pve/lxc/124.conf

# forward OpenVPN traffic to your container's IP address
# change 10.10.60.6 to your container's IP address
iptables -t nat -A PREROUTING -i vmbr0 -p tcp -m tcp --dport 1194 -j DNAT --to-destination 10.10.60.6:1194

iptables -t nat -A PREROUTING -i vmbr0 -p udp -m udp --dport 1194 -j DNAT --to-destination 10.10.60.6:1194

iptables -t nat -A PREROUTING -i vmbr1 -p tcp -m tcp --dport 53 -j DNAT --to-destination 10.10.60.6:53

# save iptables's rule
iptables-save > /etc/iptables.rules

IN CONTAINER

# execute the automated OpenVPN installation script 
mkdir /root/scripts
cd /root/scripts

wget git.io/vpn --no-check-certificate -O openvpn-install.sh ; chmod +x openvpn-install.sh ; ./openvpn-install.sh
 
# if you'd like to change the default 10.8.0.xxx IP address, do this :
# vi openvpn-install.sh
# :%s/10.8.0/10.88.0/g

# setup NAT, so the OpenVPN clients can connect to the internet 
# while connected to this OpenVPN server
iptables -I POSTROUTING -t nat -s 10.88.0.0/24 -j MASQUERADE

# save iptables's rule
iptables-save > /etc/iptables.rules

After executing the /root/scripts/openvpn-install.sh script , it will result in a file with ovpn extension

Download that to your computer / client,
install OpenVPN client,
and use that ovpn file as the configuration

Enjoy !


In case that very helpful OpenVPN Server install script suddenly disappear, here it is :

#!/bin/bash
#
# https://github.com/Nyr/openvpn-install
#
# Copyright (c) 2013 Nyr. Released under the MIT License.


# Detect Debian users running the script with "sh" instead of bash
if readlink /proc/$$/exe | grep -q "dash"; then
	echo 'This installer needs to be run with "bash", not "sh".'
	exit
fi

# Discard stdin. Needed when running from an one-liner which includes a newline
read -N 999999 -t 0.001

# Detect OpenVZ 6
if [[ $(uname -r | cut -d "." -f 1) -eq 2 ]]; then
	echo "The system is running an old kernel, which is incompatible with this installer."
	exit
fi

# Detect OS
# $os_version variables aren't always in use, but are kept here for convenience
if grep -qs "ubuntu" /etc/os-release; then
	os="ubuntu"
	os_version=$(grep 'VERSION_ID' /etc/os-release | cut -d '"' -f 2 | tr -d '.')
	group_name="nogroup"
elif [[ -e /etc/debian_version ]]; then
	os="debian"
	os_version=$(grep -oE '[0-9]+' /etc/debian_version | head -1)
	group_name="nogroup"
elif [[ -e /etc/centos-release ]]; then
	os="centos"
	os_version=$(grep -oE '[0-9]+' /etc/centos-release | head -1)
	group_name="nobody"
elif [[ -e /etc/fedora-release ]]; then
	os="fedora"
	os_version=$(grep -oE '[0-9]+' /etc/fedora-release | head -1)
	group_name="nobody"
else
	echo "This installer seems to be running on an unsupported distribution.
Supported distributions are Ubuntu, Debian, CentOS, and Fedora."
	exit
fi

if [[ "$os" == "ubuntu" && "$os_version" -lt 1804 ]]; then
	echo "Ubuntu 18.04 or higher is required to use this installer.
This version of Ubuntu is too old and unsupported."
	exit
fi

if [[ "$os" == "debian" && "$os_version" -lt 9 ]]; then
	echo "Debian 9 or higher is required to use this installer.
This version of Debian is too old and unsupported."
	exit
fi

if [[ "$os" == "centos" && "$os_version" -lt 7 ]]; then
	echo "CentOS 7 or higher is required to use this installer.
This version of CentOS is too old and unsupported."
	exit
fi

# Detect environments where $PATH does not include the sbin directories
if ! grep -q sbin <<< "$PATH"; then
	echo '$PATH does not include sbin. Try using "su -" instead of "su".'
	exit
fi

if [[ "$EUID" -ne 0 ]]; then
	echo "This installer needs to be run with superuser privileges."
	exit
fi

if [[ ! -e /dev/net/tun ]] || ! ( exec 7<>/dev/net/tun ) 2>/dev/null; then
	echo "The system does not have the TUN device available.
TUN needs to be enabled before running this installer."
	exit
fi

new_client () {
	# Generates the custom client.ovpn
	{
	cat /etc/openvpn/server/client-common.txt
	echo "<ca>"
	cat /etc/openvpn/server/easy-rsa/pki/ca.crt
	echo "</ca>"
	echo "<cert>"
	sed -ne '/BEGIN CERTIFICATE/,$ p' /etc/openvpn/server/easy-rsa/pki/issued/"$client".crt
	echo "</cert>"
	echo "<key>"
	cat /etc/openvpn/server/easy-rsa/pki/private/"$client".key
	echo "</key>"
	echo "<tls-crypt>"
	sed -ne '/BEGIN OpenVPN Static key/,$ p' /etc/openvpn/server/tc.key
	echo "</tls-crypt>"
	} > ~/"$client".ovpn
}

if [[ ! -e /etc/openvpn/server/server.conf ]]; then
	clear
	echo 'Welcome to this OpenVPN road warrior installer!'
	# If system has a single IPv4, it is selected automatically. Else, ask the user
	if [[ $(ip -4 addr | grep inet | grep -vEc '127(\.[0-9]{1,3}){3}') -eq 1 ]]; then
		ip=$(ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}')
	else
		number_of_ip=$(ip -4 addr | grep inet | grep -vEc '127(\.[0-9]{1,3}){3}')
		echo
		echo "Which IPv4 address should be used?"
		ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | nl -s ') '
		read -p "IPv4 address [1]: " ip_number
		until [[ -z "$ip_number" || "$ip_number" =~ ^[0-9]+$ && "$ip_number" -le "$number_of_ip" ]]; do
			echo "$ip_number: invalid selection."
			read -p "IPv4 address [1]: " ip_number
		done
		[[ -z "$ip_number" ]] && ip_number="1"
		ip=$(ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | sed -n "$ip_number"p)
	fi
	# If $ip is a private IP address, the server must be behind NAT
	if echo "$ip" | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)'; then
		echo
		echo "This server is behind NAT. What is the public IPv4 address or hostname?"
		# Get public IP and sanitize with grep
		get_public_ip=$(grep -m 1 -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' <<< "$(wget -T 10 -t 1 -4qO- "http://ip1.dynupdate.no-ip.com/" || curl -m 10 -4Ls "http://ip1.dynupdate.no-ip.com/")")
		read -p "Public IPv4 address / hostname [$get_public_ip]: " public_ip
		# If the checkip service is unavailable and user didn't provide input, ask again
		until [[ -n "$get_public_ip" || -n "$public_ip" ]]; do
			echo "Invalid input."
			read -p "Public IPv4 address / hostname: " public_ip
		done
		[[ -z "$public_ip" ]] && public_ip="$get_public_ip"
	fi
	# If system has a single IPv6, it is selected automatically
	if [[ $(ip -6 addr | grep -c 'inet6 [23]') -eq 1 ]]; then
		ip6=$(ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}')
	fi
	# If system has multiple IPv6, ask the user to select one
	if [[ $(ip -6 addr | grep -c 'inet6 [23]') -gt 1 ]]; then
		number_of_ip6=$(ip -6 addr | grep -c 'inet6 [23]')
		echo
		echo "Which IPv6 address should be used?"
		ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}' | nl -s ') '
		read -p "IPv6 address [1]: " ip6_number
		until [[ -z "$ip6_number" || "$ip6_number" =~ ^[0-9]+$ && "$ip6_number" -le "$number_of_ip6" ]]; do
			echo "$ip6_number: invalid selection."
			read -p "IPv6 address [1]: " ip6_number
		done
		[[ -z "$ip6_number" ]] && ip6_number="1"
		ip6=$(ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}' | sed -n "$ip6_number"p)
	fi
	echo
	echo "Which protocol should OpenVPN use?"
	echo "   1) UDP (recommended)"
	echo "   2) TCP"
	read -p "Protocol [1]: " protocol
	until [[ -z "$protocol" || "$protocol" =~ ^[12]$ ]]; do
		echo "$protocol: invalid selection."
		read -p "Protocol [1]: " protocol
	done
	case "$protocol" in
		1|"") 
		protocol=udp
		;;
		2) 
		protocol=tcp
		;;
	esac
	echo
	echo "What port should OpenVPN listen to?"
	read -p "Port [1194]: " port
	until [[ -z "$port" || "$port" =~ ^[0-9]+$ && "$port" -le 65535 ]]; do
		echo "$port: invalid port."
		read -p "Port [1194]: " port
	done
	[[ -z "$port" ]] && port="1194"
	echo
	echo "Select a DNS server for the clients:"
	echo "   1) Current system resolvers"
	echo "   2) Google"
	echo "   3) 1.1.1.1"
	echo "   4) OpenDNS"
	echo "   5) Quad9"
	echo "   6) AdGuard"
	read -p "DNS server [1]: " dns
	until [[ -z "$dns" || "$dns" =~ ^[1-6]$ ]]; do
		echo "$dns: invalid selection."
		read -p "DNS server [1]: " dns
	done
	echo
	echo "Enter a name for the first client:"
	read -p "Name [client]: " unsanitized_client
	# Allow a limited set of characters to avoid conflicts
	client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
	[[ -z "$client" ]] && client="client"
	echo
	echo "OpenVPN installation is ready to begin."
	# Install a firewall in the rare case where one is not already available
	if ! systemctl is-active --quiet firewalld.service && ! hash iptables 2>/dev/null; then
		if [[ "$os" == "centos" || "$os" == "fedora" ]]; then
			firewall="firewalld"
			# We don't want to silently enable firewalld, so we give a subtle warning
			# If the user continues, firewalld will be installed and enabled during setup
			echo "firewalld, which is required to manage routing tables, will also be installed."
		elif [[ "$os" == "debian" || "$os" == "ubuntu" ]]; then
			# iptables is way less invasive than firewalld so no warning is given
			firewall="iptables"
		fi
	fi
	read -n1 -r -p "Press any key to continue..."
	# If running inside a container, disable LimitNPROC to prevent conflicts
	if systemd-detect-virt -cq; then
		mkdir /etc/systemd/system/openvpn-server@server.service.d/ 2>/dev/null
		echo "[Service]
LimitNPROC=infinity" > /etc/systemd/system/openvpn-server@server.service.d/disable-limitnproc.conf
	fi
	if [[ "$os" = "debian" || "$os" = "ubuntu" ]]; then
		apt-get update
		apt-get install -y openvpn openssl ca-certificates $firewall
	elif [[ "$os" = "centos" ]]; then
		yum install -y epel-release
		yum install -y openvpn openssl ca-certificates tar $firewall
	else
		# Else, OS must be Fedora
		dnf install -y openvpn openssl ca-certificates tar $firewall
	fi
	# If firewalld was just installed, enable it
	if [[ "$firewall" == "firewalld" ]]; then
		systemctl enable --now firewalld.service
	fi
	# Get easy-rsa
	easy_rsa_url='https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.8/EasyRSA-3.0.8.tgz'
	mkdir -p /etc/openvpn/server/easy-rsa/
	{ wget -qO- "$easy_rsa_url" 2>/dev/null || curl -sL "$easy_rsa_url" ; } | tar xz -C /etc/openvpn/server/easy-rsa/ --strip-components 1
	chown -R root:root /etc/openvpn/server/easy-rsa/
	cd /etc/openvpn/server/easy-rsa/
	# Create the PKI, set up the CA and the server and client certificates
	./easyrsa init-pki
	./easyrsa --batch build-ca nopass
	EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass
	EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full "$client" nopass
	EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl
	# Move the stuff we need
	cp pki/ca.crt pki/private/ca.key pki/issued/server.crt pki/private/server.key pki/crl.pem /etc/openvpn/server
	# CRL is read with each client connection, while OpenVPN is dropped to nobody
	chown nobody:"$group_name" /etc/openvpn/server/crl.pem
	# Without +x in the directory, OpenVPN can't run a stat() on the CRL file
	chmod o+x /etc/openvpn/server/
	# Generate key for tls-crypt
	openvpn --genkey --secret /etc/openvpn/server/tc.key
	# Create the DH parameters file using the predefined ffdhe2048 group
	echo '-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----' > /etc/openvpn/server/dh.pem
	# Generate server.conf
	echo "local $ip
port $port
proto $protocol
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
auth SHA512
tls-crypt tc.key
topology subnet
server 10.8.0.0 255.255.255.0" > /etc/openvpn/server/server.conf
	# IPv6
	if [[ -z "$ip6" ]]; then
		echo 'push "redirect-gateway def1 bypass-dhcp"' >> /etc/openvpn/server/server.conf
	else
		echo 'server-ipv6 fddd:1194:1194:1194::/64' >> /etc/openvpn/server/server.conf
		echo 'push "redirect-gateway def1 ipv6 bypass-dhcp"' >> /etc/openvpn/server/server.conf
	fi
	echo 'ifconfig-pool-persist ipp.txt' >> /etc/openvpn/server/server.conf
	# DNS
	case "$dns" in
		1|"")
			# Locate the proper resolv.conf
			# Needed for systems running systemd-resolved
			if grep -q '^nameserver 127.0.0.53' "/etc/resolv.conf"; then
				resolv_conf="/run/systemd/resolve/resolv.conf"
			else
				resolv_conf="/etc/resolv.conf"
			fi
			# Obtain the resolvers from resolv.conf and use them for OpenVPN
			grep -v '^#\|^;' "$resolv_conf" | grep '^nameserver' | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | while read line; do
				echo "push \"dhcp-option DNS $line\"" >> /etc/openvpn/server/server.conf
			done
		;;
		2)
			echo 'push "dhcp-option DNS 8.8.8.8"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS 8.8.4.4"' >> /etc/openvpn/server/server.conf
		;;
		3)
			echo 'push "dhcp-option DNS 1.1.1.1"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS 1.0.0.1"' >> /etc/openvpn/server/server.conf
		;;
		4)
			echo 'push "dhcp-option DNS 208.67.222.222"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS 208.67.220.220"' >> /etc/openvpn/server/server.conf
		;;
		5)
			echo 'push "dhcp-option DNS 9.9.9.9"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS 149.112.112.112"' >> /etc/openvpn/server/server.conf
		;;
		6)
			echo 'push "dhcp-option DNS 94.140.14.14"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS 94.140.15.15"' >> /etc/openvpn/server/server.conf
		;;
	esac
	echo "keepalive 10 120
cipher AES-256-CBC
user nobody
group $group_name
persist-key
persist-tun
status openvpn-status.log
verb 3
crl-verify crl.pem" >> /etc/openvpn/server/server.conf
	if [[ "$protocol" = "udp" ]]; then
		echo "explicit-exit-notify" >> /etc/openvpn/server/server.conf
	fi
	# Enable net.ipv4.ip_forward for the system
	echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/30-openvpn-forward.conf
	# Enable without waiting for a reboot or service restart
	echo 1 > /proc/sys/net/ipv4/ip_forward
	if [[ -n "$ip6" ]]; then
		# Enable net.ipv6.conf.all.forwarding for the system
		echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.d/30-openvpn-forward.conf
		# Enable without waiting for a reboot or service restart
		echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
	fi
	if systemctl is-active --quiet firewalld.service; then
		# Using both permanent and not permanent rules to avoid a firewalld
		# reload.
		# We don't use --add-service=openvpn because that would only work with
		# the default port and protocol.
		firewall-cmd --add-port="$port"/"$protocol"
		firewall-cmd --zone=trusted --add-source=10.8.0.0/24
		firewall-cmd --permanent --add-port="$port"/"$protocol"
		firewall-cmd --permanent --zone=trusted --add-source=10.8.0.0/24
		# Set NAT for the VPN subnet
		firewall-cmd --direct --add-rule ipv4 nat POSTROUTING 0 -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to "$ip"
		firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to "$ip"
		if [[ -n "$ip6" ]]; then
			firewall-cmd --zone=trusted --add-source=fddd:1194:1194:1194::/64
			firewall-cmd --permanent --zone=trusted --add-source=fddd:1194:1194:1194::/64
			firewall-cmd --direct --add-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
			firewall-cmd --permanent --direct --add-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
		fi
	else
		# Create a service to set up persistent iptables rules
		iptables_path=$(command -v iptables)
		ip6tables_path=$(command -v ip6tables)
		# nf_tables is not available as standard in OVZ kernels. So use iptables-legacy
		# if we are in OVZ, with a nf_tables backend and iptables-legacy is available.
		if [[ $(systemd-detect-virt) == "openvz" ]] && readlink -f "$(command -v iptables)" | grep -q "nft" && hash iptables-legacy 2>/dev/null; then
			iptables_path=$(command -v iptables-legacy)
			ip6tables_path=$(command -v ip6tables-legacy)
		fi
		echo "[Unit]
Before=network.target
[Service]
Type=oneshot
ExecStart=$iptables_path -t nat -A POSTROUTING -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to $ip
ExecStart=$iptables_path -I INPUT -p $protocol --dport $port -j ACCEPT
ExecStart=$iptables_path -I FORWARD -s 10.8.0.0/24 -j ACCEPT
ExecStart=$iptables_path -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
ExecStop=$iptables_path -t nat -D POSTROUTING -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to $ip
ExecStop=$iptables_path -D INPUT -p $protocol --dport $port -j ACCEPT
ExecStop=$iptables_path -D FORWARD -s 10.8.0.0/24 -j ACCEPT
ExecStop=$iptables_path -D FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT" > /etc/systemd/system/openvpn-iptables.service
		if [[ -n "$ip6" ]]; then
			echo "ExecStart=$ip6tables_path -t nat -A POSTROUTING -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to $ip6
ExecStart=$ip6tables_path -I FORWARD -s fddd:1194:1194:1194::/64 -j ACCEPT
ExecStart=$ip6tables_path -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
ExecStop=$ip6tables_path -t nat -D POSTROUTING -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to $ip6
ExecStop=$ip6tables_path -D FORWARD -s fddd:1194:1194:1194::/64 -j ACCEPT
ExecStop=$ip6tables_path -D FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT" >> /etc/systemd/system/openvpn-iptables.service
		fi
		echo "RemainAfterExit=yes
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/openvpn-iptables.service
		systemctl enable --now openvpn-iptables.service
	fi
	# If SELinux is enabled and a custom port was selected, we need this
	if sestatus 2>/dev/null | grep "Current mode" | grep -q "enforcing" && [[ "$port" != 1194 ]]; then
		# Install semanage if not already present
		if ! hash semanage 2>/dev/null; then
			if [[ "$os_version" -eq 7 ]]; then
				# Centos 7
				yum install -y policycoreutils-python
			else
				# CentOS 8 or Fedora
				dnf install -y policycoreutils-python-utils
			fi
		fi
		semanage port -a -t openvpn_port_t -p "$protocol" "$port"
	fi
	# If the server is behind NAT, use the correct IP address
	[[ -n "$public_ip" ]] && ip="$public_ip"
	# client-common.txt is created so we have a template to add further users later
	echo "client
dev tun
proto $protocol
remote $ip $port
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
auth SHA512
cipher AES-256-CBC
ignore-unknown-option block-outside-dns
block-outside-dns
verb 3" > /etc/openvpn/server/client-common.txt
	# Enable and start the OpenVPN service
	systemctl enable --now openvpn-server@server.service
	# Generates the custom client.ovpn
	new_client
	echo
	echo "Finished!"
	echo
	echo "The client configuration is available in:" ~/"$client.ovpn"
	echo "New clients can be added by running this script again."
else
	clear
	echo "OpenVPN is already installed."
	echo
	echo "Select an option:"
	echo "   1) Add a new client"
	echo "   2) Revoke an existing client"
	echo "   3) Remove OpenVPN"
	echo "   4) Exit"
	read -p "Option: " option
	until [[ "$option" =~ ^[1-4]$ ]]; do
		echo "$option: invalid selection."
		read -p "Option: " option
	done
	case "$option" in
		1)
			echo
			echo "Provide a name for the client:"
			read -p "Name: " unsanitized_client
			client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
			while [[ -z "$client" || -e /etc/openvpn/server/easy-rsa/pki/issued/"$client".crt ]]; do
				echo "$client: invalid name."
				read -p "Name: " unsanitized_client
				client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
			done
			cd /etc/openvpn/server/easy-rsa/
			EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full "$client" nopass
			# Generates the custom client.ovpn
			new_client
			echo
			echo "$client added. Configuration available in:" ~/"$client.ovpn"
			exit
		;;
		2)
			# This option could be documented a bit better and maybe even be simplified
			# ...but what can I say, I want some sleep too
			number_of_clients=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V")
			if [[ "$number_of_clients" = 0 ]]; then
				echo
				echo "There are no existing clients!"
				exit
			fi
			echo
			echo "Select the client to revoke:"
			tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | nl -s ') '
			read -p "Client: " client_number
			until [[ "$client_number" =~ ^[0-9]+$ && "$client_number" -le "$number_of_clients" ]]; do
				echo "$client_number: invalid selection."
				read -p "Client: " client_number
			done
			client=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n "$client_number"p)
			echo
			read -p "Confirm $client revocation? [y/N]: " revoke
			until [[ "$revoke" =~ ^[yYnN]*$ ]]; do
				echo "$revoke: invalid selection."
				read -p "Confirm $client revocation? [y/N]: " revoke
			done
			if [[ "$revoke" =~ ^[yY]$ ]]; then
				cd /etc/openvpn/server/easy-rsa/
				./easyrsa --batch revoke "$client"
				EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl
				rm -f /etc/openvpn/server/crl.pem
				cp /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server/crl.pem
				# CRL is read with each client connection, when OpenVPN is dropped to nobody
				chown nobody:"$group_name" /etc/openvpn/server/crl.pem
				echo
				echo "$client revoked!"
			else
				echo
				echo "$client revocation aborted!"
			fi
			exit
		;;
		3)
			echo
			read -p "Confirm OpenVPN removal? [y/N]: " remove
			until [[ "$remove" =~ ^[yYnN]*$ ]]; do
				echo "$remove: invalid selection."
				read -p "Confirm OpenVPN removal? [y/N]: " remove
			done
			if [[ "$remove" =~ ^[yY]$ ]]; then
				port=$(grep '^port ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
				protocol=$(grep '^proto ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
				if systemctl is-active --quiet firewalld.service; then
					ip=$(firewall-cmd --direct --get-rules ipv4 nat POSTROUTING | grep '\-s 10.8.0.0/24 '"'"'!'"'"' -d 10.8.0.0/24' | grep -oE '[^ ]+$')
					# Using both permanent and not permanent rules to avoid a firewalld reload.
					firewall-cmd --remove-port="$port"/"$protocol"
					firewall-cmd --zone=trusted --remove-source=10.8.0.0/24
					firewall-cmd --permanent --remove-port="$port"/"$protocol"
					firewall-cmd --permanent --zone=trusted --remove-source=10.8.0.0/24
					firewall-cmd --direct --remove-rule ipv4 nat POSTROUTING 0 -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to "$ip"
					firewall-cmd --permanent --direct --remove-rule ipv4 nat POSTROUTING 0 -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to "$ip"
					if grep -qs "server-ipv6" /etc/openvpn/server/server.conf; then
						ip6=$(firewall-cmd --direct --get-rules ipv6 nat POSTROUTING | grep '\-s fddd:1194:1194:1194::/64 '"'"'!'"'"' -d fddd:1194:1194:1194::/64' | grep -oE '[^ ]+$')
						firewall-cmd --zone=trusted --remove-source=fddd:1194:1194:1194::/64
						firewall-cmd --permanent --zone=trusted --remove-source=fddd:1194:1194:1194::/64
						firewall-cmd --direct --remove-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
						firewall-cmd --permanent --direct --remove-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
					fi
				else
					systemctl disable --now openvpn-iptables.service
					rm -f /etc/systemd/system/openvpn-iptables.service
				fi
				if sestatus 2>/dev/null | grep "Current mode" | grep -q "enforcing" && [[ "$port" != 1194 ]]; then
					semanage port -d -t openvpn_port_t -p "$protocol" "$port"
				fi
				systemctl disable --now openvpn-server@server.service
				rm -rf /etc/openvpn/server
				rm -f /etc/systemd/system/openvpn-server@server.service.d/disable-limitnproc.conf
				rm -f /etc/sysctl.d/30-openvpn-forward.conf
				if [[ "$os" = "debian" || "$os" = "ubuntu" ]]; then
					apt-get remove --purge -y openvpn
				else
					# Else, OS must be CentOS or Fedora
					yum remove -y openvpn
				fi
				echo
				echo "OpenVPN removed!"
			else
				echo
				echo "OpenVPN removal aborted!"
			fi
			exit
		;;
		4)
			exit
		;;
	esac
fi

OBS (Open Broadcasting Software) As Video source (for Zoom, Google Meet, Skype, etc) in Ubuntu 20.04

I used to use my Android smartphone as webcam for Zoom / Skype / Google Meet / etc because my laptop’s webcam is so bad. This is possible thanks to the Droidcam app.

But sometimes there are problems, like the wifi got interference so the video would slow down or freeze for a while. And for long conference / meeting, it got my phone pretty hot because sometimes I have to charge it. And of course I can’t use my phone while it’s being used as webcam.

So I bought Logitech C920 Pro webcam, and started using it instead. In Linux it’s recognized and can bs used straight away.
But you may need to tweak its image quality a bit using guvcview before being used for work.

The picture quality is not as good as my smartphone’s , because my smartphone is heavily processing the images, so it came out even with HDR quality, in real-time. But as a daily work webcam, this Logitech webcam is good enough.

Then I need to start using Green screen as well with this webcam. There’s one problem – my Green screen is not wide enough to cover the webcam’s wide angle.

With Droidcam, this is not a problem, there’s a “Zoom” feature. So I just Zoom-in, until the green screen fills the view.
But since Logitech does not provide any kind of software for this webcam on Linux, I use OBS instead.

Using OBS, I can set up green screen in it, so we don’t need to use Zoom’s green screen / Virtual Background feature. And also that means all other software (Google Meet, Skype, etc) will automatically got the already green screened video from OBS.

To zoom-in in OBS, I just enlarge the webcam’s image box, until the green screen fill the view.
To activate green screen, I use the Chroma key filter.

choose Tools – V4L2 Video Output to enable OBS as Video Source for other software
Make sure to tick the “Auto Start” option
(no green screen tho when I took this screenshot)

To make OBS become a video source, we’ll need to install obs-v4l2sink : https://github.com/CatxFish/obs-v4l2sink

Turned out there are a few problems installing it in Ubuntu 20.04 , we’ll discuss here the workaround for those:

# Another possible solution is using Snap's version of OBS, 
# which already include v4l2loopback kernel module 
# & obs-v4l2sink plugin
# sudo snap install obs-studio.
# sudo modprobe v4l2loopback video_nr=10 card_label=”OBS Video Source” exclusive_caps=1

# If by any reason you can't use this Snap-based solution, 
# then continue : 


# download needed software for compilation
sudo apt-get update ; sudo apt-get install -y install obs-studio git cmake build-essential libobs-dev ffmpeg qtbase5-dev

cd /tmp ; mkdir myobscode ; cd myobscode

# get OBS' source code
git clone --recursive https://github.com/obsproject/obs-studio.git

# get plugin's source code
git clone https://github.com/CatxFish/obs-v4l2sink

# compile the OBS plugin
cd ~/obs-v4l2sink
mkdir build && cd build
cmake -DLIBOBS_INCLUDE_DIR="../../obs-studio/libobs" -DCMAKE_INSTALL_PREFIX=/usr ..

make -j4
sudo make install
sudo cp v4l2sink.so /usr/lib/obs-plugins/
sudo cp /usr/lib/obs-plugins/v4l2sink.so /usr/lib/x86_64-linux-gnu/obs-plugins/

# Turned out we need to compile and build v4l2loopback by ourselves - this is because the Ubuntu's version is too old
# Thanks to user jplandrain : 
# https://github.com/CatxFish/obs-v4l2sink/issues/54#issuecomment-722966599
cd ..
sudo apt-get remove v4l2loopback-dkms
git clone --branch v0.12.5 https://github.com/umlaeute/v4l2loopback.git
cd v4l2loopback
make && sudo make install

# make v4l2loopback automatically loaded by kernel after reboot
echo "v4l2loopback" >> /etc/modules-load.d/modules.conf
echo 'options v4l2loopback video_nr=2' >> /etc/modprobe.d/v4l2loopback.conf

echo 'options v4l2loopback card_label="VirtualCam"' >> /etc/modprobe.d/v4l2loopback.conf

echo 'options v4l2loopback exclusive_caps=1' >> /etc/modprobe.d/v4l2loopback.conf

# load the loopback module into kernel now
sudo modprobe v4l2loopback video_nr=10 card_label="OBS Video Source" exclusive_caps=1

# start OBS - now there should be a new menu : 
#     Tools - V4L2 Video Output
# also you'll need to tick "Autostart" option after choosing that menu
obs &

How to run Proxmox with only a single public IP address

IPv4 address is becoming rarer by each day. In some cases, it can be pretty hard to get multiple IPv4 address for your Proxmox server.

Thankfully, Proxmox is basically a Debian Linux OS with Proxmox layer on top of that. So that gives us quite a lot of flexibility.

This tutorial will help you to create a fully functional Proxmox server running multiple containers & virtual machines, using only a single IPv4 address.

These are the main steps :

  1. Create port forwarding rules
  2. Make sure it’s executed automatically everytime the server is restarted
  3. Setup a reverse-proxy server : to forward HTTP/S requests to the correct container / virtual machine
  4. Setup HTTPS

For CT (container) / VM (virtual machine) that contains webserver, point 3 is important – because there’s only one public IP address, so there’s only one port 80 and 443 that’s facing the Internet.

By forwarding port 80 and 443 to a reverse-proxy in a CT, then we’ll be able to forward incoming visitors, by hostname / domain name, to the correct CT/VM.

1. CREATE PORT FORWARDING RULES

Modify the following to match your host’s interface name & CT/VM’s internal IP addresses, then copy-paste to terminal :

###### All HTTP/S traffic are forwarded to reverse proxy
iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 80 -j DNAT --to 10.10.50.1:80

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 443 -j DNAT --to 10.10.50.1:443

###### SSH ports to each existing CT/VM
iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22101 -j DNAT --to 10.10.50.1:22

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22102 -j DNAT --to 10.10.50.2:22

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22103 -j DNAT --to 10.10.50.3:22

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22104 -j DNAT --to 10.10.50.4:22

Then we save it :

iptables-save > /etc/iptables.rules

2. EXECUTE IPTABLES AT SERVER RESTART

Edit /etc/network/interfaces file, find your network interface name that’s facing the Internet (in my case, vmbr0) – then add the pre-up line as follows :

auto vmbr0
pre-up iptables-restore < /etc/iptables.rules

3. SETUP REVERSE-PROXY

In a CT, install Nginx. Then for each domain, create a configuration file like this, for example: /etc/nginx/sites-available/www.my_website.com :

server {
listen 80;
server_name www.my_website.com;

location / {
    proxy_pass http://10.10.50.2:80;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

}

To activate it (assuming you’re using Ubuntu) link it to /etc/nginx/sites-enabled/ , then restart Nginx :

ln -s /etc/nginx/sites-available/www.my_website.com /etc/nginx/sites-enabled/www.my_website.com

/etc/init.d/nginx restart

note: as noted before, all HTTP/s traffic will have to go through this reverse-proxy. You may wish to tune this Nginx installation accordingly.

4. SETUP HTTPS

It’s very easy with Let’s Encrypt once you’ve done point 3 above. Do the following on the reverse-proxy CT :

sudo apt-get update ; sudo apt-get install -y certbot python3-certbot-nginx

sudo certbot --nginx

sudo /etc/init.d/nginx restart

Reference:

https://gist.githubusercontent.com/basoro/b522864678a70b723de970c4272547c8/raw/a985657453f72683040fbe38b1db6b1989618116/proxmox-proxy

Installing HTTrack on Ubuntu from Source

Today I needed to have the latest version of HTTrack installed to make a (static) mirror of a website that I managed

After a few attempts, this is how you compile & install HTTrack from source on Ubuntu :

wget "http://download.httrack.com/cserv.php3?File=httrack.tar.gz"

mv cserv.php3\?File\=httrack.tar.gz  httrack.tar.gz

tar xzvf httrack.tar.gz

cd httrack-3.49.2/

### the following is the key to a successful install
apt-get install zlib1g-dev libssl-dev build-essential

./configure && make && make install

Cutting a Table out of a mysqldump output file

I was restoring the backup of a MySQL 5.x server into MySQL 8.x server – and found out that it corrupt the MySQL 8.x ‘s mysql table

Which stores the usernames and passwords.

So I had to delete the mysql table from the backup, before trying to restore it again

Turn out it’s pretty easy, just will take some time since it’s a pretty big backup :

# search for beginning of 'mysql' table
cat backup.mysql | grep -n Current Database: `mysql`

# 155604:-- Current Database: `mysql`

# search for ending of 'mysql' table
tail -n +155604 backup.mysql | grep -n "Current Database"

# 1:  -- Current Database: `mysql`
# 916:-- Current Database: `phpmyadmin`

# cut that table out
head -155603 backup.mysql                > new.mysql
tail -n +$(( 155603+916 )) backup.mysql >> new.mysql

# voila !