From c31fe1e75ab415ef8dd64f7e97561acb8f6880d6 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Mon, 17 Jun 2024 20:55:11 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + MANIFEST.in | 2 + README.md | 80 +++++++++ benchmarks/mail_after.png | Bin 0 -> 20215 bytes benchmarks/mail_before.png | Bin 0 -> 38295 bytes fib.py | 25 +++ icon.svg | 81 +++++++++ install_package.sh | 16 ++ magic/__init__.py | 2 + magic/hocuspocus.c | 342 +++++++++++++++++++++++++++++++++++++ magic/magic.py | 44 +++++ setup.py | 29 ++++ trivial.py | 14 ++ 13 files changed, 642 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 benchmarks/mail_after.png create mode 100644 benchmarks/mail_before.png create mode 100644 fib.py create mode 100644 icon.svg create mode 100755 install_package.sh create mode 100644 magic/__init__.py create mode 100644 magic/hocuspocus.c create mode 100644 magic/magic.py create mode 100644 setup.py create mode 100644 trivial.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..989f5dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +cache_dir +dist +*.egg-info +*.assets +.venv +*/__pycache__ + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9f316f5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include src/magic/hocuspocus.c + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d1e31b --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +

+
+ +

+ + +# MAGIC (**M**agic **A**ccelerates via **G**eneral **I**ntercept-based **C**acheing) + +This python module **MAGIC**ally mitigates performance issues on BWUni caused by faulty OS cache configuration. + +The term "**HOCUSPOCUS**" (**H**ocuspocus **O**vercomes **C**onfiguration **U**psies for **S**uperior **P**erformance: **O**ptimized **C**aching via **I**ntercepting **U**serspace **S**yscalls) finds its origins in a misinterpretation from the 17th century, rooted in the Latin phrase "Hoc est corpus" used during the Catholic Mass to signify the transformation of bread into the Body of Christ. To those unfamiliar with Latin, this sacred invocation sounded like mystical jargon, which they mockingly or mistakenly transformed into "hocus pocus." + +## Function + +`hocuspocus(inform_cache_hit=False, inform_cache_miss=False, inform_cache_write=True, filetype_whitelist=".py,.pyc,.so,.dll", file_blacklist="")` + +A function to configure the caching mechanism. It uses RAM and the local SSD for caching. By default, it caches specific file types (.py, .pyc, .so, .dll) and informs about cache writes. Call `hocuspocus()` at the beginning of your script to initialize the caching mechanism. It needs to be called before importing other packages. (Or more accurately: Any code before `magic.hocuspocus` will be run twice, so it must not have any side-effects!). If you want to customize the caching behavior, use the parameters provided. + +### Parameters: +- `inform_cache_hit`: Boolean flag to print cache hits (default: False). +- `inform_cache_miss`: Boolean flag to print cache misses (default: False). +- `inform_cache_write`: Boolean flag to print cache writes (default: True). +- `filetype_whitelist`: Comma-separated string of file extensions to cache (default: ".py,.pyc,.so,.dll"). +- `file_blacklist`: Comma-separated string of file paths to exclude from caching (default: ""). + +## Example Usage + +```python +import magic +magic.hocuspocus() + +import numpy as np +import torch as th +# Your code here +``` + +## How It Works + +It's actually not magic... Python relies on the OS cache to keep actively used modules in RAM for quick access. However, an issue at BWUni causes these modules to be evicted repeatedly, leading to frequent and unnecessary reloads that need to pass through the internal network backbone. ATIS has confirmed this as the underlying issue but has yet to find a solution. + +We make us of the `LD_PRELOAD` trick to inject a shared library (`hocuspocus.so`) into the Python process. This library overrides some standard file-related system calls with our custom implementations. These then intercept all file operations, allowing us to listen in and, when passing our white- and blacklists, forge the returned file descriptors to point to a local cache which we automatically populate instead of referencing the original files. (OS-level VFS caching is also functional on these cached copies, so we get a 2 level RAM/SSD cache overall.) This explicit caching prevents the erroneous evictions caused by the misconfiguration; once a module is loaded from the cache, it remains quickly accessible, reducing the overhead of repeated file loading and significantly improving performance. + +This approach results in a significant decrease in training time and an even more significant decrease in the number of automatic e-mails sent by ATIS regarding 'high I/O activity'. + +This package is only meant for python applications; but the provided `hocuspocus.so` could also work on a wide range of other applications. Have a look at our fairly minimal source code if you wanna try to adapt it... + +## Benchmarks + +### Training wall-clock-time reduction for RL workloads + +TODO + +### Automatic ATIS e-mail reduction + +#### Before: + +![mail_before](benchmarks/mail_before.png) + +#### After: + +![mail_after](benchmarks/mail_after.png) + +We achieve a 100% reduction in automatic mails received from ATIS. + +## Authors + +ChatGPT-4o (Lead Developer) +Dominik Roth (Manager, Assistant Developer and Benchmarking) + +Questions should primarely be directed at the lead developer (ChatGPT-4o). + +## Donations + +DogeCoin: DGUjmkYd3pzV2ovUydRs6c1dmd6AHV4Aby + +Up to 50% of the total funds received through donations will be forwarded to Sam Altman's 7 trillion USD funding round. + + + +*Note: ATIS seems to be highly competence most of the time. This repo is not meant as an attack, it is merely the result of coding while in a silly goofy mood.* diff --git a/benchmarks/mail_after.png b/benchmarks/mail_after.png new file mode 100644 index 0000000000000000000000000000000000000000..93ba0621d2be8b4cd921dd0f1119e815b53e9636 GIT binary patch literal 20215 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 + + + + + + + diff --git a/install_package.sh b/install_package.sh new file mode 100755 index 0000000..f0da9dd --- /dev/null +++ b/install_package.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +pip uninstall magic-cache + +# Remove old build artifacts +rm -rf build dist *.egg-info + +# compile interceptor +rm magic/hocuspocus.so +gcc -shared -fPIC -o magic/hocuspocus.so magic/hocuspocus.c -ldl + +# Build the package +python -m build + +# Install the package +pip install dist/*.whl diff --git a/magic/__init__.py b/magic/__init__.py new file mode 100644 index 0000000..a442f0a --- /dev/null +++ b/magic/__init__.py @@ -0,0 +1,2 @@ +# magic/__init__.py +from .magic import hocuspocus diff --git a/magic/hocuspocus.c b/magic/hocuspocus.c new file mode 100644 index 0000000..dffd13c --- /dev/null +++ b/magic/hocuspocus.c @@ -0,0 +1,342 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static int (*real_open)(const char *pathname, int flags, ...) = NULL; +static int (*real_open64)(const char *pathname, int flags, ...) = NULL; +static int (*real_openat)(int dirfd, const char *pathname, int flags, + ...) = NULL; +static FILE *(*real_fopen)(const char *pathname, const char *mode) = NULL; +static FILE *(*real_fopen64)(const char *pathname, const char *mode) = NULL; + +static int inform_cache_hit = 0; +static int inform_cache_miss = 0; +static int inform_cache_write = 1; +static int verbose = 0; +static char **filetype_whitelist = NULL; +static int filetype_whitelist_count = 0; +static char **file_blacklist = NULL; +static int file_blacklist_count = 0; +static char cache_dir[4096] = {0}; + +void init_real_functions() { + real_open = dlsym(RTLD_NEXT, "open"); + real_open64 = dlsym(RTLD_NEXT, "open64"); + real_openat = dlsym(RTLD_NEXT, "openat"); + real_fopen = dlsym(RTLD_NEXT, "fopen"); + real_fopen64 = dlsym(RTLD_NEXT, "fopen64"); + + if (!real_open || !real_open64 || !real_openat || !real_fopen || + !real_fopen64) { + fprintf(stderr, + "[hocuspocus.so] Error: Failed to initialize real functions\n"); + exit(EXIT_FAILURE); + } + + if (verbose) { + fprintf(stderr, + "[hocuspocus.so] Real functions initialized successfully\n"); + } +} + +void init_config() { + verbose = getenv("MAGIC_VERBOSE") ? atoi(getenv("MAGIC_VERBOSE")) : 0; + inform_cache_hit = getenv("MAGIC_INFORM_CACHE_HIT") + ? atoi(getenv("MAGIC_INFORM_CACHE_HIT")) + : 0; + inform_cache_miss = getenv("MAGIC_INFORM_CACHE_MISS") + ? atoi(getenv("MAGIC_INFORM_CACHE_MISS")) + : 0; + inform_cache_write = getenv("MAGIC_INFORM_CACHE_WRITE") + ? atoi(getenv("MAGIC_INFORM_CACHE_WRITE")) + : 1; + + const char *cache_dir_env = getenv("MAGIC_CACHE_DIR"); + if (cache_dir_env) { + strncpy(cache_dir, cache_dir_env, sizeof(cache_dir) - 1); + cache_dir[sizeof(cache_dir) - 1] = '\0'; + } + + if (verbose) { + fprintf(stderr, + "[hocuspocus.so] Config initialized. inform_cache_hit: %d, " + "inform_cache_miss: %d, inform_cache_write: %d, verbose: %d\n", + inform_cache_hit, inform_cache_miss, inform_cache_write, verbose); + } + + char *whitelist = getenv("MAGIC_FILETYPE_WHITELIST"); + if (whitelist) { + filetype_whitelist_count = 1; + for (char *p = whitelist; *p; p++) { + if (*p == ',') filetype_whitelist_count++; + } + filetype_whitelist = malloc(filetype_whitelist_count * sizeof(char *)); + if (!filetype_whitelist) { + fprintf( + stderr, + "[hocuspocus.so] Error: Failed to allocate memory for whitelist\n"); + exit(EXIT_FAILURE); + } + filetype_whitelist[0] = strtok(whitelist, ","); + for (int i = 1; i < filetype_whitelist_count; i++) { + filetype_whitelist[i] = strtok(NULL, ","); + } + } + + char *blacklist = getenv("MAGIC_FILE_BLACKLIST"); + if (blacklist) { + file_blacklist_count = 1; + for (char *p = blacklist; *p; p++) { + if (*p == ',') file_blacklist_count++; + } + file_blacklist = malloc(file_blacklist_count * sizeof(char *)); + if (!file_blacklist) { + fprintf( + stderr, + "[hocuspocus.so] Error: Failed to allocate memory for blacklist\n"); + exit(EXIT_FAILURE); + } + file_blacklist[0] = strtok(blacklist, ","); + for (int i = 1; i < file_blacklist_count; i++) { + file_blacklist[i] = strtok(NULL, ","); + } + } +} + +int should_cache(const char *pathname) { + for (int i = 0; i < file_blacklist_count; i++) { + if (strstr(pathname, file_blacklist[i])) { + return 0; + } + } + + if (filetype_whitelist_count == 0) return 1; + + const char *ext = strrchr(pathname, '.'); + if (ext) { + for (int i = 0; i < filetype_whitelist_count; i++) { + if (strcmp(ext, filetype_whitelist[i]) == 0) { + return 1; + } + } + } + return 0; +} + +int is_cached_file(const char *pathname) { + if (!cache_dir[0]) return 0; + + static char cached_path[8192]; + if (snprintf(cached_path, sizeof(cached_path), "%s/%s", cache_dir, + pathname) >= sizeof(cached_path)) { + fprintf(stderr, + "[hocuspocus.so] Warning: Cached path is too long and was " + "truncated: %s/%s\n", + cache_dir, pathname); + return 0; + } + + return access(cached_path, F_OK) == 0; +} + +void copy_to_cache(const char *src, const char *dest) { + FILE *src_file = fopen(src, "rb"); + if (!src_file) { + if (inform_cache_write) { + fprintf(stderr, + "[hocuspocus.so] Warning: Could not open source file %s: %s\n", + src, strerror(errno)); + } + return; + } + + FILE *dest_file = fopen(dest, "wb"); + if (!dest_file) { + if (inform_cache_write) { + fprintf( + stderr, + "[hocuspocus.so] Warning: Could not open destination file %s: %s\n", + dest, strerror(errno)); + } + fclose(src_file); + return; + } + + char buffer[4096]; + size_t bytes; + while ((bytes = fread(buffer, 1, sizeof(buffer), src_file)) > 0) { + fwrite(buffer, 1, bytes, dest_file); + } + + fclose(src_file); + fclose(dest_file); + + if (inform_cache_write) { + fprintf(stderr, "[hocuspocus.so] Cached write: %s\n", dest); + } +} + +const char *get_cached_path(const char *pathname) { + static char cached_path[8192]; + + char safe_path[4096]; + strncpy(safe_path, pathname, sizeof(safe_path) - 1); + safe_path[sizeof(safe_path) - 1] = '\0'; + for (char *p = safe_path; *p; ++p) { + if (*p == '/') *p = '_'; + } + + if (snprintf(cached_path, sizeof(cached_path), "%s/%s", cache_dir, + safe_path) >= sizeof(cached_path)) { + fprintf(stderr, + "[hocuspocus.so] Warning: Cached path is too long and was " + "truncated: %s/%s\n", + cache_dir, safe_path); + exit(EXIT_FAILURE); + } + + return cached_path; +} + +const char *get_rerouted_path(const char *pathname) { + int should_cache_file = should_cache(pathname); + int cache_hit = should_cache_file && is_cached_file(pathname); + const char *cached_path = pathname; + + if (should_cache_file) { + cached_path = get_cached_path(pathname); + if (!cache_hit) { + copy_to_cache(pathname, cached_path); + } + if (cache_hit && inform_cache_hit) { + printf("[hocuspocus.so] Using cached path: %s\n", cached_path); + } else if (!cache_hit && inform_cache_miss) { + printf("[hocuspocus.so] Intercepted open (cache miss): %s\n", pathname); + } + } + + if (verbose) + fprintf(stderr, "[hocuspocus.so] rerouted path: %s -> %s\n", pathname, + cached_path); + + return cached_path; +} + +int open_common(const char *pathname, int flags, va_list args) { + const char *cached_path = get_rerouted_path(pathname); + + int fd; + if (flags & O_CREAT) { + mode_t mode = va_arg(args, mode_t); + fd = real_open(cached_path, flags, mode); + } else { + fd = real_open(cached_path, flags); + } + + if (fd == -1) { + fprintf(stderr, "[hocuspocus.so] Error: open failed for %s: %s\n", + cached_path, strerror(errno)); + } + + return fd; +} + +FILE *fopen_common(const char *pathname, const char *mode, int is_fopen64) { + const char *cached_path = get_rerouted_path(pathname); + FILE *file; + + if (verbose) { + fprintf(stderr, "[hocuspocus.so] Attempting to fopen%s: %s with mode: %s\n", + is_fopen64 ? "64" : "", cached_path, mode); + } + + if (is_fopen64) { + file = real_fopen64(cached_path, mode); + } else { + file = real_fopen(cached_path, mode); + } + + if (!file) { + fprintf(stderr, "[hocuspocus.so] Error: fopen%s failed for %s: %s\n", + is_fopen64 ? "64" : "", cached_path, strerror(errno)); + } else { + if (verbose) { + fprintf(stderr, "[hocuspocus.so] fopen%s succeeded for %s\n", + is_fopen64 ? "64" : "", cached_path); + } + } + + return file; +} + +__attribute__((constructor)) void init() { + init_config(); + init_real_functions(); + if (verbose) { + fprintf(stderr, "[hocuspocus.so] Initialization complete\n"); + } +} +int open(const char *pathname, int flags, ...) { + if (verbose) + fprintf(stderr, "[hocuspocus.so] Intercepting open: %s\n", pathname); + + va_list args; + va_start(args, flags); + int fd = open_common(pathname, flags, args); + va_end(args); + + return fd; +} + +int open64(const char *pathname, int flags, ...) { + if (verbose) + fprintf(stderr, "[hocuspocus.so] Intercepting open64: %s\n", pathname); + + va_list args; + va_start(args, flags); + int fd = open_common(pathname, flags, args); + va_end(args); + + return fd; +} + +int openat(int dirfd, const char *pathname, int flags, ...) { + if (verbose) + fprintf(stderr, "[hocuspocus.so] Intercepting openat: %s\n", pathname); + + va_list args; + va_start(args, flags); + int fd = open_common(pathname, flags, args); + va_end(args); + + return fd; +} + +FILE *fopen(const char *pathname, const char *mode) { + if (verbose) + fprintf(stderr, "[hocuspocus.so] Intercepting fopen: %s with mode: %s\n", + pathname, mode); + + return fopen_common(pathname, mode, 0); +} + +FILE *fopen64(const char *pathname, const char *mode) { + if (verbose) { + fprintf(stderr, "[hocuspocus.so] Intercepting fopen64: %s with mode: %s\n", + pathname, mode); + } + + FILE *file = fopen_common(pathname, mode, 1); + + if (!file && verbose) { + fprintf(stderr, "[hocuspocus.so] fopen64 returned NULL for %s\n", pathname); + } + + return file; +} diff --git a/magic/magic.py b/magic/magic.py new file mode 100644 index 0000000..878e55e --- /dev/null +++ b/magic/magic.py @@ -0,0 +1,44 @@ +import os +import sys +import subprocess + +def hocuspocus(inform_cache_hit=False, inform_cache_miss=False, inform_cache_write=True, + filetype_whitelist=".py,.pyc,.so,.dll", file_blacklist="", verbose=False): + # Check if already active + if os.getenv("MAGIC_ACTIVE") == "1": + return + + # Gather the current Python executable and script arguments + python_executable = sys.executable + script_name = sys.argv[0] + script_args = sys.argv[1:] + + # Set environment variables for hocuspocus.so + env = os.environ.copy() + env["MAGIC_ACTIVE"] = "1" + env["MAGIC_INFORM_CACHE_HIT"] = "1" if inform_cache_hit else "0" + env["MAGIC_INFORM_CACHE_MISS"] = "1" if inform_cache_miss else "0" + env["MAGIC_INFORM_CACHE_WRITE"] = "1" if inform_cache_write else "0" + env["MAGIC_FILETYPE_WHITELIST"] = filetype_whitelist + env["MAGIC_FILE_BLACKLIST"] = file_blacklist + env["MAGIC_CACHE_DIR"] = os.path.abspath("cache_dir") + env["MAGIC_CURRENT_DIR"] = os.path.abspath(".") + env["MAGIC_VERBOSE"] = "1" if verbose else "0" + + # Ensure the cache directory exists + os.makedirs(env["MAGIC_CACHE_DIR"], exist_ok=True) + + # Ensure hocuspocus.so exists + intercept_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "hocuspocus.so")) + if not os.path.exists(intercept_path): + raise FileNotFoundError(f"hocuspocus.so not found at {intercept_path}") + + # Re-run the script with LD_PRELOAD set to the intercept library + env["LD_PRELOAD"] = intercept_path + + # Re-execute the current script with the same arguments + command = [python_executable, script_name] + script_args + result = subprocess.run(command, env=env) + + # Exit with the same return code as the subprocess + sys.exit(result.returncode) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7e2b3d4 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +import setuptools +import os +import subprocess + +class CustomBuild(setuptools.Command): + description = "Compile the C extension" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + cwd = os.path.abspath(os.path.dirname(__file__)) + subprocess.check_call(["bash", os.path.join(cwd, "magic", "compile.sh")]) + +setuptools.setup( + name="magic_cache", + version="0.1.0", + description="A magic caching library for Python", + author="Your Name", + author_email="your.email@example.com", + packages=setuptools.find_packages(where="."), + cmdclass={"build_ext": CustomBuild}, + include_package_data=True, +) + diff --git a/trivial.py b/trivial.py new file mode 100644 index 0000000..c2fe7fe --- /dev/null +++ b/trivial.py @@ -0,0 +1,14 @@ +import magic +magic.hocuspocus(inform_cache_hit=True, inform_cache_miss=True, inform_cache_write=False, verbose=True) + +print('hi') +import numpy as np +import torch + +print("Running actual code:") +np_array = np.array([1, 2, 3]) +print(f"Numpy array: {np_array}") + +tensor = torch.tensor([1, 2, 3]) +print(f"Torch tensor: {tensor}") +