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:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Data directories (user data, not tracked)
|
||||
data/
|
||||
zotero/data/
|
||||
|
||||
# Marimo cache
|
||||
notebooks/__marimo__/
|
||||
52
compose.yml
Normal file
52
compose.yml
Normal 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
47
notebooks/Dockerfile
Normal 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
236
notebooks/gpu_test.py
Normal 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
21
zotero/Dockerfile
Normal 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
|
||||
Reference in New Issue
Block a user