Skip to main content

FUSE Adapter

The FUSE adapter mounts a virtual filesystem backed by the Context Transfer Engine (CTE). Applications read and write files using normal POSIX I/O, and data transparently flows through CTE's tiered storage system — no LD_PRELOAD, no code changes, no recompilation.

How It Works

ConceptMapping
FileCTE Tag (tag name = absolute FUSE path, e.g. /mnt/cte/data/model.bin)
DirectoryExplicit sentinel tag (path + /) created by mkdir, or implicit when any tag shares the prefix
File dataPage-indexed blobs within the tag ("0", "1", "2", …, default page size 1 MB)
Directory listingAsyncTagQuery with regex matching on tag names

No in-memory metadata structures are needed. All state — file contents, sizes, timestamps — is stored in CTE.


Platform Support

clio_cte_fuse runs on all three desktop platforms. The FUSE callbacks and the entire CTE data path below them are identical everywhere — only the kernel backend and the mount mechanics differ.

LinuxmacOSWindows
Backendlibfuse3macFUSE 5 (ships libfuse3)WinFsp 2.0
Ships in the pip wheelYesNo — source build requiredYes
MountpointAny directoryA directory (kext) or under /Volumes (FSKit)A drive letter (Z:) or a directory
Unmountfusermount3 -uumount / diskutil unmount forceStop the daemon process
Discoverypkg-config fuse3pkg-config fuse3WINFSP_ROOT (no .pc file)
CI coverageBuild + unit + ops + live mount + xfstests conformanceBuild + unit + ops enforced; mount smoke is a non-blocking probeBuild + unit + live WinFsp mount smoke
macOS is a source build

The macOS wheel does not ship clio_cte_fuse. Build from source against macFUSE — see the macOS tab below. (Older notes claiming macOS "has no FUSE3 API" predate macFUSE 5, which does ship libfuse3.)

The clio_cte_fuse console script is declared in every wheel, including macOS, so the command exists on PATH there even though the binary behind it does not. Running it prints Error: clio_cte_fuse binary not found at <path> and exits 1 — that is the expected symptom on a pip-installed macOS host, not a broken install.

macOS wheels are also published for Apple Silicon only (macosx_14_0_arm64), and there is no sdist, so pip install iowarp-core has nothing to resolve on an Intel Mac.


Installation

1. Install libfuse3. It is a system dependency and is deliberately not bundled in the wheel:

sudo apt install libfuse3-dev fuse3     # Ubuntu / Debian
sudo dnf install fuse3-devel fuse3 # RHEL / Fedora

If you only ever run a prebuilt binary, the runtime packages (fuse3 libfuse3-3 / fuse3 fuse3-libs) are enough; the -dev / -devel packages are needed to build the adapter.

2a. Prebuilt (pip). The Linux wheel already ships clio_cte_fuse:

pip install iowarp-core
clio_cte_fuse --help

2b. From source. The release-fuse preset is a Release build with the FUSE adapter enabled:

bash install.sh release-fuse
# or, configuring manually:
cmake --preset release-fuse
cmake --build build -j"$(nproc)"

To enable FUSE on any other preset, add -DCLIO_CTE_ENABLE_FUSE_ADAPTER=ON.

3. Verify. /dev/fuse must exist and be accessible:

ls -l /dev/fuse
fusermount3 --version

The adapter links against clio_cte_filesystem_client, clio_cte_core_client, and the platform's FUSE backend — it does not require MPI or ELF interception. The CLIO Runtime must also be installed; see Configuration.


Usage

1. Start the CLIO Runtime

clio_run start

Or with a custom configuration:

export CLIO_SERVER_CONF=/path/to/config.yaml
clio_run start

2. Mount the FUSE filesystem

The daemon create-or-binds the filesystem pool and the CTE pool underneath it, so no separate compose step is required. To size storage tiers explicitly instead of taking the defaults, compose a CTE pool first with clio_run compose start my_cte.yaml.

mkdir -p /mnt/cte

# Connect as a client to the already-running runtime
CLIO_WITH_RUNTIME=0 clio_cte_fuse /mnt/cte -f

Any directory works as a mountpoint.

Every argument after the mountpoint is handed to fuse_main, so the standard libfuse options apply:

FlagDescription
-fRun in the foreground (recommended for debugging, and what CI exercises). Omit to daemonize.
-dDebug mode — prints every FUSE callback to stderr.
-o allow_otherAllow other users to access the mount (requires user_allow_other in /etc/fuse.conf).
-sSingle-threaded mode. By default FUSE is multi-threaded.

Set CLIO_WITH_RUNTIME=0 so the FUSE daemon attaches to the runtime you already started instead of spawning its own embedded one. Without it the daemon comes up on a private runtime and the data will not be visible to other clients.

3. Use it

Any application can read and write files on the mount point with standard tools — nothing is IOWarp-specific, which is the whole point of the adapter. Each tab uses the mountpoint from the matching mount step above.

# Write
echo "Hello, IOWarp!" > /mnt/cte/greeting.txt
mkdir -p /mnt/cte/data
cp dataset.csv /mnt/cte/data/dataset.csv

# Read
cat /mnt/cte/greeting.txt
md5sum /mnt/cte/data/dataset.csv

# List
ls -l /mnt/cte/

# Delete
rm /mnt/cte/greeting.txt

4. Unmount

fusermount3 -u /mnt/cte
# older systems: fusermount -u /mnt/cte

Running Under Apptainer (Linux HPC)

Apptainer's --fusemount opens /dev/fuse and passes the FUSE binary a pre-opened file descriptor as the last argument, /dev/fd/<N>. libfuse 3's high-level argv parser rejects that token (it is a libfuse2-era convention), and Apptainer strips the mountpoint from argv, so there is no plain fuse_main invocation that works.

clio_cte_fuse detects a trailing /dev/fd/<N> and takes a separate path: it binds the descriptor to a mountpoint itself and drives the protocol with fuse_session_custom_io(). Two things this requires:

  1. CLIO_CTE_FUSE_MOUNTPOINT must be set before the binary is exec'd. Apptainer communicates the mountpoint through neither argv nor the environment, so the binary cannot discover it and exits with an error rather than guessing.
  2. CAP_SYS_ADMIN in the current user namespace — unprivileged Apptainer (no setuid starter) does not call mount(2) itself for a user-supplied binary; it only hands over the descriptor. Apptainer's userns mapping normally provides the capability.
export CLIO_CTE_FUSE_MOUNTPOINT=/mnt/cte
The pip wheel cannot do this

The custom-io path needs libfuse 3.14+ headers at build time. The manylinux images used to build Linux wheels ship libfuse 3.10.2, so the path is compiled out of the published wheel — running it in --fusemount mode prints a "needs 3.14+ headers at build time" error and exits. Normal mounting is unaffected. Build from source against libfuse 3.14+ if you need the Apptainer path.

This is Linux-only. Windows and macOS always take the ordinary fuse_main mount-and-serve route.


Quick Start Scripts

Ready-to-use scripts are available in context-transfer-engine/test/integration/fuse-manual/:

cd context-transfer-engine/test/integration/fuse-manual

# Start runtime + mount FUSE
./start.sh

# Copy /workspace into the FUSE mount and verify
./copy_workspace.sh

# Stop everything
./stop.sh

End-to-end mount check

The scripts CI uses to validate a real mount are the fastest way to confirm a fresh deployment works. They start the runtime, compose a CTE pool, mount, write + read back + verify a file, then tear everything down:

# Linux and macOS
CI/fuse_mount_smoke.sh <build-dir>

# macOS with the kext-free FSKit backend
sudo mkdir -p /Volumes/cte_smoke && sudo chown "$(whoami)" /Volumes/cte_smoke
CLIO_SMOKE_MOUNT_POINT=/Volumes/cte_smoke \
CLIO_SMOKE_FUSE_OPTS="-o backend=fskit" \
CI/fuse_mount_smoke.sh <build-dir>
# Windows — picks a free drive letter automatically
pwsh CI/fuse_mount_smoke.ps1 -BuildDir <build-dir>

<build-dir> is a CMake binary directory whose bin/ holds clio_run and clio_cte_fuse. Both scripts fail fast with an explicit message if the adapter binary is missing, which is the usual sign that the FUSE backend was not detected at configure time.


Configuration

The FUSE adapter inherits its CTE configuration from the running CLIO Runtime. The runtime's compose section controls storage backends, tiering, and placement policy. No FUSE-specific configuration file is needed.

Example runtime config (~/.clio/clio.yaml):

runtime:
num_threads: 4
queue_depth: 1024

compose:
- mod_name: clio_cte_core
pool_name: cte_main
pool_query: local
pool_id: "512.0"
storage:
- path: /mnt/ssd
bdev_type: file
capacity_limit: 50GB
score: 0.8
- path: /mnt/hdd
bdev_type: file
capacity_limit: 500GB
score: 0.2
dpe:
dpe_type: max_bw

With this config, files written to the FUSE mount are automatically placed across SSD and HDD tiers based on the max_bw data placement engine.


Docker

A FUSE mount created inside a container lives in that container's mount namespace — it is not visible on the host. On macOS and Windows the container also runs inside a Linux VM, so the mount is doubly unreachable from the host filesystem. If you want the mount usable from the host on those platforms, run the FUSE daemon natively (as above) and let containers reach the runtime over the network instead.

When mounting inside a Linux container, the container needs FUSE device access:

docker run --cap-add SYS_ADMIN --device /dev/fuse \
--security-opt apparmor:unconfined \
--security-opt seccomp=unconfined \
-v /workspace:/workspace \
iowarp/deps-cpu:latest \
bash -c "clio_run start & sleep 3 && \
CLIO_WITH_RUNTIME=0 clio_cte_fuse /mnt/cte -f"

A ready-made Docker Compose configuration for integration testing is available at:

context-transfer-engine/test/integration/fuse/docker-compose.yaml

Run it with:

cd context-transfer-engine/test/integration/fuse
./run_tests.sh

Performance

Benchmark results (50 MB dd write, 1 MB block size):

ConfigurationThroughput
FUSE + CTE (1 MB pages, SHM)~250 MB/s
FUSE + CTE (1 MB pages, TCP)~250 MB/s
Native filesystem~500 MB/s

The 1 MB default page size minimizes the number of CTE blob operations per write. The remaining overhead is from the FUSE kernel round-trip (user → kernel → FUSE daemon → CTE → back).


Supported Operations

FUSE OperationBehavior
getattrSentinel tags → directories; regular tags → files; prefix matches → implicit directories
readdirLists direct child tags, implicit subdirectories, and explicit empty directories (sentinel tags)
createCreates a new CTE tag with AsyncGetOrCreateTag
openLooks up existing tag
readPage-based AsyncGetBlob loop (1 MB pages)
writePage-based AsyncPutBlob loop (1 MB pages)
releaseFrees per-open file handle
unlinkDeletes tag with AsyncDelTag
mkdirCreates sentinel tag (path + /) so the directory is immediately visible
rmdirDeletes sentinel tag; fails with ENOTEMPTY if children exist
truncateAsyncTruncate on the filesystem chimod
utimensSets atime/mtime; honors UTIME_NOW (resolved server-side, sharing the tag clock) and UTIME_OMIT
renameAsyncRename; honors RENAME_NOREPLACE
chmod / chownAsyncChmod / AsyncChown — POSIX mode bits and ownership are stored
symlink / readlinkTarget string stored in a reserved marker blob under the link's tag
linkHard link — both names bind to the same CTE tag, so they share all data and inode
statfsReports real capacity via GetCapacity
setxattr / getxattr / listxattr / removexattrExtended attributes, stored per tag
fsync / flushNo-ops returning success — writes are already write-through, so there is nothing buffered to flush
fallocateLinux only. FALLOC_FL_KEEP_SIZE and FALLOC_FL_ZERO_RANGE; punch/collapse/insert return EOPNOTSUPP

Not supported

OperationNotes
RENAME_EXCHANGE / RENAME_WHITEOUTReturn EINVAL so callers fall back cleanly; would need chimod-level atomic swap / whiteout
fallocate punch / collapse / insertLayout-changing; return EOPNOTSUPP

Caching semantics

The init callback deliberately disables the kernel's attribute, entry, and negative caches (attr_timeout = entry_timeout = negative_timeout = 0). Metadata can change without this FUSE process being the one that changed it, and there is no upcall to invalidate a stale entry — so every getattr/lookup goes to the chimod, which is the source of truth. Without this, ln a b; stat a would report a's stale cached link count.

The kernel page cache for file data stays enabled (direct_io = 0). This is what makes mmap work: the high-level FUSE API has no .mmap callback, so the kernel faults mapped pages through read and flushes dirty pages through write. Turning direct_io on would bypass the page cache and make every mmap fail with ENODEV.


Comparison with Other Adapters

POSIX AdapterSTDIO AdapterFUSE Adapter
PlatformsLinux onlyLinux onlyLinux, macOS, Windows
MechanismLD_PRELOADLD_PRELOADKernel VFS mount
Requires preloadingYesYesNo
Requires recompilationNoNoNo
Works with any languageC/C++ onlyC/C++ onlyYes (any language)
Intercepts existing binariesYesYesYes
MPI dependencyYesYesNo
Performance overheadLow (direct SHM)Low (direct SHM)Moderate (kernel ↔ userspace copies)

The FUSE adapter trades some performance for universal compatibility — any program that can open a file path can use it, regardless of language or link-time dependencies.

The POSIX, STDIO, and HDF5 VFD adapters are built on the glibc ELF/dlsym interceptor, which has no macOS or Windows port. On those platforms FUSE is the only transparent-interception option. (The HDF5 VOL connector is portable and works on all three.)


Troubleshooting

clio_cte_fuse was not built

The adapter is skipped, not failed, when its backend is not found at configure time — so the symptom is a missing binary, not a build error. Re-run CMake and look for the warning:

  • LinuxFUSE3 not found. Install libfuse3-dev / fuse3-devel.
  • macOS — the fuse3 .pc file is not on the pkg-config path. Add /usr/local/lib/pkgconfig (and /opt/homebrew/lib/pkgconfig on Apple Silicon) to PKG_CONFIG_PATH.
  • WindowsWinFsp not found. Reinstall the MSI with ADDLOCAL=ALL so the Developer feature (inc/fuse3 + lib) is present, or pass -DWINFSP_ROOT=<dir>.

Data written through the mount is invisible to other clients

CLIO_WITH_RUNTIME was unset or non-zero, so the daemon started its own private embedded runtime. Set CLIO_WITH_RUNTIME=0 and mount again.

The mount point does not appear

Give the daemon a few seconds — the CI smoke tests poll for up to 20 s. If the daemon exits first, run it with -f and read stderr; -d adds a trace of every FUSE callback. On Windows, confirm the drive letter you chose is actually free.

macOS: mount hangs or is refused

The kext backend needs one-time approval in System Settings → Privacy & Security followed by a reboot. If you cannot approve a kext (managed machines, CI), use the FSKit backend instead: -o backend=fskit, macFUSE 5.1+ on macOS 15.4+, mountpoint under /Volumes. A hung mount is a known shape here — bound the attempt with a timeout rather than waiting on it.

Windows: winfsp-x64.dll could not be found

The MSI does not add WinFsp's bin to the system PATH. The pip console script handles this itself; a directly-invoked binary does not. Prepend it manually:

$env:PATH = "${env:ProgramFiles(x86)}\WinFsp\bin;$env:PATH"

mmap fails with ENODEV

Something has enabled direct_io, which bypasses the page cache the kernel needs to serve mapped pages. The adapter leaves it off by default; do not pass -o direct_io.