Portfolio_Risk_Management/docs/SPARK_APPTAINER.md
herzogflorian 54721801ba One data folder for API and CLI; README blocks safe to paste
On a student Spark the API answered with 746 stocks while `quantcore
risk` found no data, and after a restart the test bench answered 503
"no market data yet". Cause: `install.sh --rebuild-data` renamed the data
folder; a container that still has a folder mounted keeps using it under
its new name, and the data jobs ran inside the API -- so the rebuilt data
went into ~/quantcore/data.<date> while the commands read the new, empty
~/quantcore/data. Pasting the README's command block made it visible: it
contained `qc stop / qc start`, which stopped the service.

- install.sh --rebuild-data stops the service, checks that it stopped,
  and moves the folder's contents, never the folder itself
- data jobs (setup, update) always run in a fresh container on the data
  folder; a running API is restarted afterwards (stopped during setup);
  `qc data-import` restarts it too; stop waits and forces if needed
- `qc status` warns when the API serves a different data set than the
  folder, and lists earlier data sets with the command to restore one;
  `qc start` does the same when the folder is empty
- the CLI prints three lines instead of a traceback when there is no data
- README and guide: command blocks only read, so they can be pasted whole;
  start, stop, update, upgrade and --rebuild-data are in tables
- lookups that may find nothing are guarded under pipefail (one ended
  `qc status` silently)

Tested on a stripped-down machine: the README block pasted whole keeps the
service running; `quantcore update` restarts the API and status agrees;
the broken state is detected and restored with `qc data-import`;
`install.sh --rebuild-data` with the service running rebuilt 745 stocks,
and API, CLI and folder agree afterwards. 142 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:06:59 +02:00

305 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# quantcore on your DGX Spark (Apptainer, no root)
This guide sets up the quant service of the project on your own DGX Spark. The service covers
market data, risk measurement, Black–Litterman expected returns and the mean-CVaR optimiser.
Everything runs on the Spark itself:
- **cuOpt**, NVIDIA's GPU solver, runs in its own Apptainer container.
- **quantcore**, this repository, runs in a second Apptainer container. It calls cuOpt at
`http://127.0.0.1:5000`.
- The API and the browser test bench listen on `http://127.0.0.1:5001`. You reach them from the
Spark: its desktop browser, a terminal, a notebook, or the platform you build. They are not
reachable from the network.
You need no root rights, no Docker and no VPN. One script, `deploy/apptainer/qc.sh`, does
everything.
## What you need
| Requirement | Check |
|---|---|
| Apptainer 1.1 or newer | `apptainer --version` |
| The GPU | `nvidia-smi -L` lists the NVIDIA GB10. Without it, optimisation runs on HiGHS on the CPU. |
| About 5 GB in your home directory | The cuOpt image is 3.3 GB, quantcore 0.2 GB, the data 40 MB. Pulling cuOpt needs about 12 GB for a few minutes. |
| Internet access | Docker Hub for the images, PyPI for the Python libraries, Yahoo Finance for market data. |
## Quick start
Open a terminal on the Spark and run this one line:
```bash
wget -qO- https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management/raw/branch/main/install.sh | bash
```
If the Spark has no `wget`, one of these does the same:
```bash
curl -fsSL https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management/raw/branch/main/install.sh | bash
python3 -c "import urllib.request as u;print(u.urlopen('https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management/raw/branch/main/install.sh').read().decode())" | bash
```
The installer takes 20 to 30 minutes the first time and prints each step:
1. It checks Apptainer and the GPU. It also repairs Apptainer's temporary and cache folders if
your account points them at a folder that does not exist.
2. It downloads the code to `~/Portfolio_Risk_Management`. If the Spark has no git, it uses git
from a small container.
3. It gets the cuOpt image and the quantcore image.
4. It builds the market data from Yahoo Finance, which takes about 15 minutes. The candidate
stocks come from the list shipped in the repository, so every student gets the same universe.
Companies that Yahoo no longer lists are left out, and setup names them in one summary line.
5. It starts the service, runs the self-test and installs the commands `qc`, `quantcore` and
`quantcore-api`.
With a prepared data set from your lecturer, the data step takes seconds instead. Pass the file
with `--data`:
```bash
wget -qO- https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management/raw/branch/main/install.sh | bash -s -- --data ~/Downloads/quantcore_data.tar.gz
```
Running the installer again is safe. It skips what is done and updates the rest.
When it is done, open **http://127.0.0.1:5001/ui/** in the Spark's browser. The interactive API
documentation is at **http://127.0.0.1:5001/docs**.
Working on the Spark over SSH from your laptop? Forward the port, then open the same address in
the laptop's browser. Nothing else on the network can reach the service:
```bash
ssh -L 5001:127.0.0.1:5001 <your-user>@<your-spark>
```
The installer puts the commands `qc`, `quantcore` and `quantcore-api` into `~/.local/bin`. **Open a
new terminal** after the installation; a terminal that was already open does not find them. `qc` is
the same as `deploy/apptainer/qc.sh` in the code folder. `quantcore` and `quantcore-api` are the
commands of the README and of [USAGE.md](USAGE.md); on the Spark they run inside the container, so
nothing needs to be installed with `pip`.
## Daily use
| Command | What it does |
|---|---|
| `qc status` | Shows what runs, whether cuOpt is reachable, and how much data there is. It warns when the API serves a different data set than the data folder. |
| `qc logs` | Shows the API log. `qc logs cuopt` shows the solver's log; add `-f` to follow. |
| `qc start` | Starts cuOpt and the API. Run it again after a reboot. |
| `qc stop` | Stops both containers. |
| `qc restart` | Stops and starts both. |
| `qc update` | Fetches the newest prices and FX rates from Yahoo Finance, then restarts the API. |
| `qc upgrade` | Downloads the newest version of the code, rebuilds and restarts. |
| `~/Portfolio_Risk_Management/install.sh --rebuild-data` | Sets the data aside in `~/quantcore/data.<date>` and builds it anew (~10 min). |
The containers keep running after you log out. They stop when the Spark reboots.
## Using the service
**In the browser.** The test bench at `http://127.0.0.1:5001/ui/` shows every service: the
stock universe, risk and scenarios, expected returns and views, constraints, and the
optimisation with a before/after comparison.
**From your own code.** This is how the platform you build uses the service. The API speaks
JSON over HTTP, so any language works. Your code reaches it at `http://127.0.0.1:5001`. That
address also works inside your own Apptainer containers, because Apptainer containers share
the Spark's network. For Python there is a ready-made client:
```python
from quantcore.client import QuantcoreClient
qc = QuantcoreClient("http://127.0.0.1:5001")
print(qc.health()["cuopt"]["reachable"])
risk = qc.asset_risk(["NESN.SW", "AAPL"], horizon="week")
```
Install the client into your platform's own virtual environment with
`pip install "git+https://gitea.fhgr.ch/herzogfloria/Portfolio_Risk_Management.git"`. Ubuntu refuses
`pip install` outside a virtual environment. A quick script can also run with the container's Python,
where quantcore is already installed: `qc python my_script.py`. [USAGE.md](USAGE.md) describes every
endpoint and the request fields.
**From the command line, through the API.** `quantcore-api` is the API client. It talks to your
running service:
```bash
quantcore-api health
quantcore-api selftest # calls every endpoint, pass/fail table
quantcore-api risk NESN.SW AAPL --horizon week
quantcore-api optimize --cash 1000000 --cvar-limit 40000 --horizon week
```
**From the command line, without the API.** `quantcore` runs the services directly in the
container. The API does not need to be running:
```bash
quantcore status
quantcore risk NESN.SW AAPL --horizon week
quantcore optimize --cash 1000000 --cvar-limit 40000 --horizon week --max-assets 50
```
`quantcore serve` starts the background service, like `qc start`. `quantcore setup` and
`quantcore update` run through the service when it is running, so it picks up the new data.
`qc cli …` and `qc api …` are the same commands under their `qc` names.
## Settings
`qc install` writes the settings file `~/quantcore/quantcore.env`. Edit it, then run
`qc restart`. A value set in the shell wins over the file, for example
`QC_API_PORT=5011 qc start`.
| Setting | Default | Meaning |
|---|---|---|
| `QC_API_PORT` | `5001` | Port of the API and the test bench. |
| `QC_CUOPT_PORT` | `5000` | Port of the cuOpt server. |
| `QC_CUOPT_START` | `auto` | `auto` uses a cuOpt that already answers on the port, or starts one. `yes` always starts its own. `no` never starts one, so HiGHS solves every model. |
| `QC_CUOPT_IMAGE` | `docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14` | Where the cuOpt image comes from. Set the path of a `.sif` file if your Spark already provides one. |
| `QC_IMAGE` | `build` | How quantcore gets its container; see the next section. |
| `QUANTCORE_*` | | Passed into the container unchanged. Examples are `QUANTCORE_CUOPT_TIME_LIMIT` and `QUANTCORE_ENABLE_UI`. All of them are listed in `quantcore/config.py`. |
**Several users on one Spark.** Ports belong to the whole machine. If someone else already uses
port 5001, `qc start` stops and suggests a free port to put into your settings file. A cuOpt
that another user already runs on port 5000 is simply used: cuOpt keeps no state between
requests.
## How quantcore gets its container
`QC_IMAGE=build` is the default. `qc install` then builds the image `quantcore.sif` from this
repository with Apptainer's unprivileged build. Some Spark installations do not allow
unprivileged builds. In that case `qc install` falls back by itself to a setup that needs no
build. It pulls a plain Python image and installs quantcore into a virtual environment in
`~/quantcore/venv`. The libraries and the results are the same, because both setups install the
pinned versions in `deploy/apptainer/constraints.txt`. You can choose the fallback directly
with `QC_IMAGE=venv`.
`QC_IMAGE` also accepts the path of a prebuilt `quantcore.sif`, or a `docker://` or `oras://`
address to pull one from.
**New versions of the code** come with `qc upgrade`. It downloads the changes, rebuilds quantcore
and restarts the service.
## How it works
```
DGX Spark, your user account, no root
┌────────────────────────────────────────────────────────────────────────────┐
│ browser / your platform / qc api │
│ │ HTTP │
│ ▼ │
│ 127.0.0.1:5001 instance "quantcore-api" quantcore.sif │
│ FastAPI + test bench ~/quantcore/data -> /data │
│ │ HTTP (msgpack) │
│ ▼ │
│ 127.0.0.1:5000 instance "quantcore-cuopt" cuopt.sif, started with --nv │
│ cuOpt server ──► GB10 GPU │
└────────────────────────────────────────────────────────────────────────────┘
```
- An Apptainer **instance** is a container that runs in the background, like a service.
`apptainer instance list` shows yours.
- Apptainer containers use the Spark's network directly, with no port mapping. A server inside a
container that listens on `127.0.0.1` is therefore reachable from the Spark, and only from it.
- The images are read-only. Everything that changes lives in `~/quantcore/data`, which is
mounted into the container as `/data`.
- The containers start with `--cleanenv`. Variables from your shell, such as an activated
virtual environment or `PYTHONPATH`, cannot interfere. `qc.sh` passes only the settings above,
plus proxy variables if you have them.
- NVIDIA's cuOpt image sets its home directory to `/opt/cuopt`. That folder is writable under
Docker but read-only in an Apptainer image, and the solver would crash on its first job when
CuPy writes its kernel cache there. `qc.sh` therefore starts the cuOpt server with its home
directory in `~/quantcore/cuopt-home`.
- When cuOpt is not reachable, or stops during a solve, every optimisation falls back to HiGHS on
the CPU. The result says which solver produced it.
| Path | Content |
|---|---|
| `~/quantcore/quantcore.env` | your settings |
| `~/quantcore/data/` | universe, prices, FX, snapshots |
| `~/quantcore/images/cuopt.sif` | the cuOpt image |
| `~/quantcore/images/quantcore.sif` | the quantcore image; in the fallback, `python.sif` plus `~/quantcore/venv/` |
| `~/quantcore/cuopt-home/` | cuOpt's caches (CuPy kernels, CUDA) |
| `~/.apptainer/instances/logs/` | instance logs, also shown by `qc logs` |
| `~/.apptainer/cache/` | downloaded image layers; `qc clean-cache` frees the space |
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| `port 5001 is taken` | Another user or program uses the port. Set the suggested `QC_API_PORT` in `~/quantcore/quantcore.env`, then run `qc start`. |
| `qc status` shows cuOpt as not answering | Run `qc logs cuopt`. Check that `nvidia-smi` works. Then `qc restart`. Optimisations keep working on HiGHS in the meantime. |
| The cuOpt log says `Read-only file system: '/opt/cuopt/.cupy'` | cuOpt was started without `qc.sh`. Start it with `qc start`, or give it a writable home directory as described in "Running cuOpt by hand". |
| The test bench or `/optimize` answers 503, "no market data yet" | The data folder is empty. Run `qc status`: it lists earlier data sets in `~/quantcore/data.<date>` and the command to restore one, `qc data-import <folder>`. Otherwise build the data with `qc setup --offline` (~10 min). |
| `qc status` warns that the API serves a different data set | Run `qc restart`. |
| Yahoo errors during `qc setup` or `qc update` | Yahoo limits how fast it answers. Run the command again later; stored data stays intact. |
| `FATAL: ... failed to create build parent dir: stat /scratch/...` | Your account sets `APPTAINER_TMPDIR` to a folder that does not exist. The installer and `qc` now detect this, use a folder that works, and save the choice in `~/quantcore/quantcore.env`. Run the installer again. |
| `git: command not found` | Use the one-line installer. It needs no git. |
| Hundreds of red `Quote not found for symbol` lines during setup | Your installation predates the fix. Its setup used guessed symbols, so many European companies are missing from your data. Run `qc upgrade`, then `~/Portfolio_Risk_Management/install.sh --rebuild-data`. |
| `qc: command not found` or `quantcore: command not found` right after the installation | Open a new terminal, or run `export PATH="$HOME/.local/bin:$PATH"`. |
| `error: externally-managed-environment` from `pip install` | That is the laptop route of the README. On the Spark nothing needs to be installed with `pip`: `quantcore` and `quantcore-api` already work. For your own code, use a virtual environment. |
| `qc install` stops with "no 'instance run'" | The Spark's Apptainer is older than 1.1. Ask the administrator to update it. |
| The cuOpt pull fails with "no space left on device" | Apptainer unpacks images in `/tmp`. Point it at a larger directory: `APPTAINER_TMPDIR=~/quantcore/tmp qc pull-cuopt`. |
| Model status `FeasibleFound` instead of `Optimal` | The time limit was reached. Raise `time_limit` in the request or `QUANTCORE_CUOPT_TIME_LIMIT`, or reduce `n_scenarios` or `max_assets`. |
## Running cuOpt by hand
`qc start` does this for you. If you want to run the cuOpt image yourself, for example for another
project, start it like this:
```bash
apptainer pull cuopt.sif docker://nvidia/cuopt:26.8.0-cuda13.3-py3.14
mkdir -p ~/cuopt-home
apptainer instance run --nv --cleanenv --bind ~/cuopt-home cuopt.sif my-cuopt \
env HOME=$HOME/cuopt-home python -m cuopt_server.cuopt_service -i 127.0.0.1 -p 5000
curl http://127.0.0.1:5000/cuopt/health
apptainer instance stop my-cuopt
```
## For the lecturer
**Data set for the students.** Many students running `qc setup` at the same time from one
network can hit Yahoo's rate limits. A prepared data set avoids that, and every student then
works on the same data. Pack it on a machine that has the data:
```bash
qc data-export quantcore_data.tar.gz # or, from any data directory:
tar -czf quantcore_data.tar.gz -C <data dir> --exclude ./jobs --exclude '*.log' .
```
The archive is about 30 MB. It is not part of the public repository: Yahoo's terms do not allow
redistributing their data.
**Prebuilt image.** Build `quantcore.sif` on any aarch64 Linux machine where you can build
images, then hand out the file:
```bash
apptainer build quantcore.sif deploy/apptainer/quantcore.def # from the repository root
```
Students set `QC_IMAGE=/path/to/quantcore.sif` in their settings file. A copy built and tested on
the lecturer's Spark is described in the verification note below.
**The lecturer's own Spark** runs the Docker setup in [SPARK_DEPLOYMENT.md](SPARK_DEPLOYMENT.md).
That setup needs root and Docker. Students do not need it.
**Verification.** The setup was tested end to end on 7 October 2026 on a DGX Spark: NVIDIA GB10,
driver 580.159, Ubuntu 24.04, Apptainer 1.5.4, as a user without root. That Spark allowed no
unprivileged image builds, so `qc install` took the virtualenv fallback by itself. The image mode
was then tested with a prebuilt `quantcore.sif`. In both modes:
- `qc api selftest --full` passed on every endpoint, and the optimisation ran on cuOpt on the GPU.
- `qc update` fetched the newest Yahoo prices through the running API.
- A taken API port stopped `qc start` with a suggested free port, and that port worked.
- The test suite passed inside the container, including the round trip to the live cuOpt server.
The one-line installer was then tested on a stripped-down machine. It had no git, curl, wget or
python3, a home folder named like a domain account (`/home/name@edu.local`), and `APPTAINER_TMPDIR`
pointing to a `/scratch` folder that does not exist. The installer repaired the folders, fetched the
code with git from a container, installed, started and passed the self-test. `qc upgrade` picked up
a new commit, rebuilt quantcore and restarted it in under a minute. `qc setup` built a data set from
scratch inside the container.
**Where the candidate stocks come from.** The installer runs `qc setup --offline`. It takes the
candidates from the lists shipped in `quantcore/data/static/`: the S&P 500 and the iShares STOXX
Europe 600 holdings of September 2026, from which the course data set was built. `qc setup` without
`--offline` fetches the current lists instead. In October 2026 iShares moved the German download, so
setup now reads the same file from iShares' Swiss site. If that fails too, setup uses the shipped
list. It no longer uses Wikipedia's STOXX 600 table, whose codes mostly do not exist on Yahoo, for
example `BNPP.PA` instead of `BNP.PA`.