Add GPU-accelerated notebook and Zotero desktop services

- notebooks: Marimo notebook server with CUDA/Polars GPU support
  - Uses nvidia/cuda base with uv package manager
  - Includes cudf-polars for GPU-accelerated dataframes
  - GPU benchmark comparing Polars CPU/GPU and Pandas

- zotero: Web-accessible Zotero via Selkies EGL desktop
  - Uses nvidia-egl-desktop with KasmVNC for browser access
  - GPU-accelerated desktop streaming
  - Persistent Zotero data directory

- compose.yml: Docker Compose orchestration for all services

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kert
2026-02-04 00:48:22 -05:00
commit 906d3aa318
5 changed files with 362 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Data directories (user data, not tracked)
data/
zotero/data/
# Marimo cache
notebooks/__marimo__/

52
compose.yml Normal file
View File

@@ -0,0 +1,52 @@
services:
notebooks:
build: ./notebooks
container_name: notebooks
ports:
- "2718:2718"
volumes:
- ./notebooks:/home/kert/notebooks
- ./data:/home/kert/data
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
shm_size: "1g"
restart: unless-stopped
zotero:
build: ./zotero
container_name: zotero
runtime: nvidia
stdin_open: true
tty: true
ports:
- "8080:8080"
- "3478:3478"
- "3478:3478/udp"
volumes:
- ./zotero/data:/home/ubuntu/Zotero
- ./data:/home/ubuntu/data
tmpfs:
- /dev/shm:rw
environment:
- TZ=UTC
- DISPLAY_SIZEW=1920
- DISPLAY_SIZEH=1080
- DISPLAY_REFRESH=60
- DISPLAY_DPI=96
- DISPLAY_CDEPTH=24
- PASSWD=zotero
- KASMVNC_ENABLE=true
- SELKIES_ENABLE_BASIC_AUTH=false
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped

47
notebooks/Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
# syntax=docker/dockerfile:1
FROM nvidia/cuda:12.6.0-runtime-ubuntu24.04
ARG USERNAME=kert
ARG USER_UID=1000
ARG USER_GID=1000
ARG PYTHON_VERSION=3.13
ENV DEBIAN_FRONTEND=noninteractive \
PATH="/home/${USERNAME}/.local/bin:${PATH}" \
NVIDIA_VISIBLE_DEVICES=all \
NVIDIA_DRIVER_CAPABILITIES=compute,utility
# System dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Rename existing ubuntu user/group to kert and fix home ownership
RUN groupmod -n ${USERNAME} ubuntu \
&& usermod -l ${USERNAME} -d /home/${USERNAME} -m -s /bin/bash ubuntu \
&& chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY --from=ghcr.io/astral-sh/uv:latest /uvx /usr/local/bin/uvx
# Switch to user
USER ${USERNAME}
WORKDIR /home/${USERNAME}
# Initialize uv project and install dependencies
RUN uv python install ${PYTHON_VERSION} \
&& uv init workspace --python ${PYTHON_VERSION} \
&& cd workspace \
&& uv add "marimo[recommended]" polars cudf-polars-cu12 pandas numpy pyarrow \
--extra-index-url https://pypi.nvidia.com
EXPOSE 2718
CMD ["uv", "run", "--project", "/home/kert/workspace", \
"marimo", "edit", \
"--host", "0.0.0.0", "--port", "2718", "--headless", "--no-token", \
"/home/kert/notebooks"]

236
notebooks/gpu_test.py Normal file
View File

@@ -0,0 +1,236 @@
import marimo
__generated_with = "0.19.7"
app = marimo.App(width="medium")
with app.setup:
import marimo as mo
import subprocess
import platform
import os
import sys
import time
import numpy as np
import polars as pl
import pandas as pd
@app.cell(hide_code=True)
def gpu_diagnostics():
try:
smi_result = subprocess.run(
["nvidia-smi"],
capture_output=True, text=True, timeout=10
)
nvidia_smi_output = smi_result.stdout
gpu_detected = smi_result.returncode == 0
except Exception as exc:
nvidia_smi_output = str(exc)
gpu_detected = False
visible_devices = os.environ.get("NVIDIA_VISIBLE_DEVICES", "not set")
driver_capabilities = os.environ.get("NVIDIA_DRIVER_CAPABILITIES", "not set")
mo.md(f"""
# GPU & System Check
**GPU Available:** `{gpu_detected}`
**NVIDIA_VISIBLE_DEVICES:** `{visible_devices}`
**NVIDIA_DRIVER_CAPABILITIES:** `{driver_capabilities}`
```
{nvidia_smi_output}
```
""")
return
@app.cell(hide_code=True)
def polars_gpu_benchmark():
benchmark_rows = 50_000_000
bench_rng = np.random.default_rng(42)
bench_gen_start = time.perf_counter()
bench_df = pl.DataFrame({
"id": np.arange(benchmark_rows),
"group": bench_rng.choice(["A", "B", "C", "D", "E"], size=benchmark_rows),
"value_1": bench_rng.standard_normal(benchmark_rows),
"value_2": bench_rng.uniform(0, 1000, size=benchmark_rows),
"value_3": bench_rng.integers(0, 100, size=benchmark_rows),
})
bench_gen_elapsed = time.perf_counter() - bench_gen_start
# Pre-create lazy frame to exclude setup from timing
bench_lazy = bench_df.lazy()
# GPU-supported aggregations only (no quantile/median which cause fallback)
bench_agg_expr = [
pl.col("value_1").mean().alias("mean_v1"),
pl.col("value_1").std().alias("std_v1"),
pl.col("value_2").sum().alias("sum_v2"),
pl.col("value_2").min().alias("min_v2"),
pl.col("value_2").max().alias("max_v2"),
pl.col("value_3").mean().alias("mean_v3"),
pl.len().alias("count"),
]
# --- GPU collect ---
bench_gpu_agg_start = time.perf_counter()
bench_gpu_agg_result = (
bench_lazy
.group_by("group")
.agg(*bench_agg_expr)
.sort("group")
.collect(engine="gpu")
)
bench_gpu_agg_elapsed = time.perf_counter() - bench_gpu_agg_start
# --- CPU collect ---
bench_cpu_agg_start = time.perf_counter()
bench_cpu_agg_result = (
bench_lazy
.group_by("group")
.agg(*bench_agg_expr)
.sort("group")
.collect()
)
bench_cpu_agg_elapsed = time.perf_counter() - bench_cpu_agg_start
bench_agg_speedup = bench_cpu_agg_elapsed / bench_gpu_agg_elapsed if bench_gpu_agg_elapsed > 0 else float("inf")
# GPU-supported window functions only (no rank which causes fallback)
bench_window_expr = [
pl.col("value_1").mean().over("group").alias("group_mean"),
pl.col("value_2").sum().over("group").alias("group_sum"),
]
# --- GPU window ---
bench_gpu_window_start = time.perf_counter()
bench_gpu_window_result = (
bench_lazy
.with_columns(*bench_window_expr)
.head(5)
.collect(engine="gpu")
)
bench_gpu_window_elapsed = time.perf_counter() - bench_gpu_window_start
# --- CPU window ---
bench_cpu_window_start = time.perf_counter()
bench_cpu_window_result = (
bench_lazy
.with_columns(*bench_window_expr)
.head(5)
.collect()
)
bench_cpu_window_elapsed = time.perf_counter() - bench_cpu_window_start
bench_window_speedup = bench_cpu_window_elapsed / bench_gpu_window_elapsed if bench_gpu_window_elapsed > 0 else float("inf")
mo.md(f"""
# Polars GPU vs CPU — {benchmark_rows:,} rows
| Operation | GPU | CPU | Speedup |
|---|---|---|---|
| Data generation | `{bench_gen_elapsed:.3f}s` | — | — |
| GroupBy aggregation | `{bench_gpu_agg_elapsed:.3f}s` | `{bench_cpu_agg_elapsed:.3f}s` | **{bench_agg_speedup:.1f}x** |
| Window functions | `{bench_gpu_window_elapsed:.3f}s` | `{bench_cpu_window_elapsed:.3f}s` | **{bench_window_speedup:.1f}x** |
""")
mo.hstack([
mo.ui.table(bench_gpu_agg_result, label="GPU Aggregation Results"),
mo.ui.table(bench_gpu_window_result, label="GPU Window Functions (head 5)"),
])
return
@app.cell(hide_code=True)
def pandas_vs_polars_gpu():
cmp_rows = 50_000_000
cmp_rng = np.random.default_rng(99)
# Generate data once, outside timing
cmp_categories = cmp_rng.choice(["X", "Y", "Z"], size=cmp_rows)
cmp_amounts = cmp_rng.standard_normal(cmp_rows).astype(np.float64)
# --- Pandas (CPU only) - time only the compute, not DataFrame creation ---
cmp_pandas_df = pd.DataFrame({"category": cmp_categories, "amount": cmp_amounts})
cmp_pandas_start = time.perf_counter()
cmp_pandas_agg = cmp_pandas_df.groupby("category")["amount"].agg(["mean", "std", "sum"])
cmp_pandas_elapsed = time.perf_counter() - cmp_pandas_start
# --- Polars GPU - time only the compute ---
cmp_polars_df = pl.DataFrame({"category": cmp_categories, "amount": cmp_amounts})
cmp_lazy = cmp_polars_df.lazy()
cmp_gpu_start = time.perf_counter()
cmp_gpu_agg = (
cmp_lazy
.group_by("category")
.agg(
pl.col("amount").mean().alias("mean"),
pl.col("amount").std().alias("std"),
pl.col("amount").sum().alias("sum"),
)
.sort("category")
.collect(engine="gpu")
)
cmp_gpu_elapsed = time.perf_counter() - cmp_gpu_start
# --- Polars CPU ---
cmp_cpu_start = time.perf_counter()
cmp_cpu_agg = (
cmp_lazy
.group_by("category")
.agg(
pl.col("amount").mean().alias("mean"),
pl.col("amount").std().alias("std"),
pl.col("amount").sum().alias("sum"),
)
.sort("category")
.collect()
)
cmp_cpu_elapsed = time.perf_counter() - cmp_cpu_start
cmp_gpu_vs_pandas = cmp_pandas_elapsed / cmp_gpu_elapsed if cmp_gpu_elapsed > 0 else float("inf")
cmp_cpu_vs_pandas = cmp_pandas_elapsed / cmp_cpu_elapsed if cmp_cpu_elapsed > 0 else float("inf")
mo.md(f"""
# Three-Way Comparison — {cmp_rows:,} rows
| Engine | GroupBy Time | vs Pandas |
|---|---|---|
| Pandas (CPU) | `{cmp_pandas_elapsed:.3f}s` | 1.0x |
| Polars (CPU) | `{cmp_cpu_elapsed:.3f}s` | **{cmp_cpu_vs_pandas:.1f}x** |
| Polars (GPU) | `{cmp_gpu_elapsed:.3f}s` | **{cmp_gpu_vs_pandas:.1f}x** |
""")
return
@app.cell(hide_code=True)
def environment_info():
python_version = sys.version
platform_info = platform.platform()
cpu_cores = os.cpu_count()
marimo_version = mo.__version__
polars_version = pl.__version__
pandas_version = pd.__version__
numpy_version = np.__version__
mo.md(f"""
# Environment
| Component | Version |
|---|---|
| Python | `{python_version}` |
| Platform | `{platform_info}` |
| CPU cores | `{cpu_cores}` |
| Marimo | `{marimo_version}` |
| Polars | `{polars_version}` |
| Pandas | `{pandas_version}` |
| NumPy | `{numpy_version}` |
""")
return
if __name__ == "__main__":
app.run()

21
zotero/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1
FROM ghcr.io/selkies-project/nvidia-egl-desktop:latest
USER root
# Install Zotero
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& curl -sL https://raw.githubusercontent.com/retorquere/zotero-deb/master/install.sh | bash \
&& apt-get update && apt-get install -y --no-install-recommends \
zotero \
&& rm -rf /var/lib/apt/lists/*
# Create desktop shortcut for Zotero
RUN mkdir -p /home/ubuntu/Desktop \
&& cp /usr/share/applications/zotero.desktop /home/ubuntu/Desktop/ \
&& chmod +x /home/ubuntu/Desktop/zotero.desktop \
&& chown -R ubuntu:ubuntu /home/ubuntu/Desktop
USER ubuntu