40 Commits

Author SHA1 Message Date
Bill Holcombe
dc8494bb2a Fix all four Phase 1 correctness bugs (dynamic buffer, DLPack ownership, thread safety, padding)
- 1.1: Remove hardcoded 4096*512 buffer from PinnedBatcher; allocate host+device per-call
- 1.2: Wrap CudaSlice in DLPackContext owned by DLManagedTensor.manager_ctx; deleter frees
  device memory when PyTorch releases tensor — no more silent overwrite across calls
- 1.3: batch_encode_to_gpu now takes &self (no mutable shared state); TokenizerEngine wraps
  Arc<PinnedBatcher> and encode_batch takes &self — safe for concurrent Python threads
- 1.4: seq_len = max across all encodings; host buffer prefilled with pad_id before token write

Also: switch cudarc gpu feature from cuda-version-from-build-system to cuda-12050 to fix
build on machines with CUDA 13.x (cudarc 0.11.9 only knows up to 12.5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 14:10:31 -06:00
Bill Holcombe
6f8b6440aa Add roadmap, commercialization strategy, talk outline, and handoff notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 11:13:17 -06:00
f8ad40d302 made zero-copy JSON - shared memory logic 2026-02-11 06:29:08 +00:00
Bill
409363bf80 Merge remote-tracking branch 'origin/main' 2026-02-09 16:25:35 -07:00
Bill
6d3a7f0b48 updated markdown 2026-02-09 16:25:23 -07:00
BillTheMaker
bb4bc1ec57 Enhance publish workflow for multi-OS support
Updated GitHub Actions workflow to support multiple OS builds and simplified wheel upload process.
2025-12-12 15:09:11 -07:00
Bill
a9ff2f1e76 readme move to parent folder 2025-12-12 14:32:22 -07:00
Bill
7c394c012d pyproject.toml update 2025-12-12 14:26:27 -07:00
Bill
d690b08486 readme name update 2025-12-12 14:21:27 -07:00
Bill
4088471885 pyproject.toml version issues 2025-12-12 14:15:11 -07:00
BillTheMaker
f085e977e6 Refactor GitHub Actions for wheel publishing
Updated GitHub Actions workflow to build and publish Python wheels. Removed unnecessary jobs and streamlined the process for better efficiency.
2025-12-12 13:51:47 -07:00
333f8f0f26 Update pyproject.toml 2025-12-11 17:39:59 -07:00
9845e81194 Create publish2.yml 2025-12-11 17:39:15 -07:00
BillTheMaker
ed40391f93 Fix token variable for PyPI upload in workflow 2025-12-06 20:17:38 -07:00
BillTheMaker
e76d7a0a5b Refactor wheel copying and upload process in CI 2025-12-06 20:11:44 -07:00
BillTheMaker
feff63fb36 Modify publish.yml to flatten wheel uploads
Updated the publish workflow to create a flattened directory for wheels before copying them.
2025-12-06 20:06:43 -07:00
BillTheMaker
0994b6656e Clarify wheel flattening in publish.yml
Added comments to clarify the wheel flattening process.
2025-12-06 20:00:44 -07:00
BillTheMaker
e5f1dcf7b9 Refactor GitHub Actions workflow for building wheels 2025-12-06 19:52:07 -07:00
BillTheMaker
4fb0b78a5f Refactor publish job in GitHub Actions workflow
Removed conditional checks and dependencies in the publish job.
2025-12-06 19:35:49 -07:00
BillTheMaker
15da5eb1c2 Refactor publish step in GitHub Actions workflow 2025-12-06 19:32:08 -07:00
BillTheMaker
f9ecbae6a4 Refactor publish workflow for PyPI 2025-12-06 19:30:14 -07:00
BillTheMaker
000a2fad3a Update publish.yml 2025-12-06 19:25:47 -07:00
BillTheMaker
14b0481d7c Fix Python version and clean up publish workflow
Updated Python version to 3.12 and removed invalid input for python in the build step.
2025-12-06 19:12:47 -07:00
BillTheMaker
bd8e07fe9c Fix indentation in publish.yml workflow steps 2025-12-06 18:52:39 -07:00
BillTheMaker
9016b9213e Update publish.yml 2025-12-06 18:48:02 -07:00
BillTheMaker
b25a070d5e Add step to publish package to PyPI 2025-12-06 18:28:52 -07:00
BillTheMaker
8e6b120e76 Update publish workflow with debugging steps 2025-12-06 18:07:01 -07:00
BillTheMaker
ebea27b62e Change pypi-token to token in publish.yml 2025-12-06 17:52:20 -07:00
BillTheMaker
09b4eb4510 Enhance publish workflow with wheel verification
Added steps to verify and flatten wheels before publishing.
2025-12-06 17:30:54 -07:00
BillTheMaker
30d741b418 Update publish workflow to allow manual triggering 2025-12-06 16:19:02 -07:00
BillTheMaker
7b5e615c00 Refactor publish.yml for matrix configuration
updating and fixing pypy documentation
2025-12-06 16:05:36 -07:00
BillTheMaker
056fbbb6e1 Update artifact name to include OS and target 2025-12-06 16:00:32 -07:00
BillTheMaker
c408c483d0 disable manylinux
disabled manylinux because it was defaulting to python 3.8 which was breaking Pypi build
2025-12-06 15:38:47 -07:00
BillTheMaker
c97f5e6e6e Merge pull request #10 from BillTheMaker/feature
feature
2025-11-25 13:15:10 -07:00
Bill
50ddaacbdc Apache license 2025-11-25 11:28:02 -07:00
Bill
c734fadba2 Apache license 2025-11-25 11:05:12 -07:00
Bill
258f7e09aa configurable batch size for hardware optimizations 2025-11-25 01:14:45 -07:00
Bill
2811d97c47 fixed task switching latency and implemented string killer 2025-11-25 00:01:41 -07:00
BillTheMaker
9cacb95b0f Merge pull request #9 from BillTheMaker/feature
add doc file
2025-11-24 20:18:09 -07:00
Bill
11ddd184f4 version update to publish to Pypi 7 2025-11-24 20:17:03 -07:00
22 changed files with 1934 additions and 756 deletions

View File

@@ -1,75 +1,76 @@
name: Publish to PyPI
# This workflow is triggered when you push a new version tag (e.g., v0.1.0, v1.2.3)
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*' # Matches v0.1.0 or v1.0.0-rc1, etc.
# Allows manual triggering from the GitHub Actions UI
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
build_and_publish:
name: Build and Publish All Wheels
runs-on: ${{ matrix.os }}
# Define a matrix to build for common operating systems and architectures
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64
- os: macos-latest
target: x86_64
- os: windows-latest
target: x86_64
os: [ubuntu-latest, macos-latest, windows-latest]
target: [x86_64]
steps:
- uses: actions/checkout@v4
# Only setup python for non-linux, or let maturin handle linux via docker
- name: Set up Python
if: runner.os != 'Linux'
uses: actions/setup-python@v5
with:
python-version: '3.12' # Target a modern Python version
python-version: '3.12'
# Note: On Windows/Mac, to build for multiple python versions,
# you usually need multiple setup-python steps or a matrix of python versions.
# But for now, let's at least get the artifacts downloading correctly.
# Ideally, you remove this and let maturin find python, but GitHub runners
# might only have one default.
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Build wheels with Maturin
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
# Use the target from the matrix (x86_64)
target: ${{ matrix.target }}
# Build a manylinux-compatible wheel for Linux/Colab users
manylinux: auto
manylinux: auto # This triggers the Docker container on Linux for multi-python builds
command: build
args: --release --out dist --find-interpreter
- name: Upload built wheels as Artifact
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheels
path: dist
name: wheel-${{ matrix.os }}-${{ matrix.target }}
path: dist/*.whl
publish:
name: Publish to PyPI
needs: [build_wheels]
name: Publish All Wheels
needs: [build_and_publish]
runs-on: ubuntu-latest
# This step will only run if the 'build_wheels' job completed successfully
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v4
with:
name: wheels
path: dist
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
with:
# Use the secret we configured on GitHub
pypi-token: ${{ secrets.PYPI_API_TOKEN }}
# Command to upload all wheels in the 'dist' directory
command: upload
args: --skip-existing --non-interactive
- name: Install maturin
run: pip install maturin
# FIX: Use merge-multiple to flatten everything into 'dist' automatically
- name: Download ALL wheels
uses: actions/download-artifact@v4
with:
path: dist
pattern: wheel-*
merge-multiple: true
- name: List Files (Debug)
run: ls -la dist
- name: Upload to PyPI
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
maturin upload --skip-existing --non-interactive dist/*

80
.github/workflows/publish2.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Publish to PyPI
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
jobs:
build_and_publish:
name: Build and Publish All Wheels
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
target: [x86_64]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
command: build
args: --release --out dist --find-interpreter
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.os }}-${{ matrix.target }}
path: dist/*.whl
publish:
name: Publish All Wheels
needs: [build_and_publish]
runs-on: ubuntu-latest
steps:
- name: Install maturin
run: pip install maturin
- name: Download ALL wheels
uses: actions/download-artifact@v4
with:
path: dist
- name: Flatten and upload
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} # ← CORRECT TOKEN VAR
run: |
mkdir -p dist/flattened
rm -rf dist/flattened/*
# Copy EVERY wheel
find dist -name "*.whl" | while read wheel; do
cp "$wheel" dist/flattened/
done
echo "=== TOTAL WHEELS ==="
ls -la dist/flattened/*.whl | wc -l
cd dist/flattened
echo "=== UPLOADING $(ls *.whl | wc -l) WHEELS ==="
ls -la *.whl | head -5
# TWINE fallback (double protection)
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=${{ secrets.PYPI_API_TOKEN }}
maturin upload --skip-existing --non-interactive *.whl

484
Cargo.lock generated
View File

@@ -118,7 +118,7 @@ dependencies = [
"arrow-schema",
"arrow-select",
"atoi",
"base64",
"base64 0.22.1",
"chrono",
"comfy-table",
"half",
@@ -297,6 +297,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.22.1"
@@ -395,6 +401,19 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys 0.59.0",
]
[[package]]
name = "const-random"
version = "0.1.18"
@@ -430,6 +449,25 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -473,6 +511,68 @@ dependencies = [
"memchr",
]
[[package]]
name = "cuda-config"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee74643f7430213a1a78320f88649de309b20b80818325575e393f848f79f5d"
dependencies = [
"glob",
]
[[package]]
name = "cuda-driver-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d4c552cc0de854877d80bcd1f11db75d42be32962d72a6799b88dcca88fffbd"
dependencies = [
"cuda-config",
]
[[package]]
name = "cudarc"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bd4d1eee570c3b2ac64ed114125517dd1e541d88dd28fc259f1de4dba8d60"
dependencies = [
"libloading",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deadpool"
version = "0.12.3"
@@ -508,6 +608,37 @@ dependencies = [
"tokio",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -519,12 +650,33 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "esaxx-rs"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
dependencies = [
"cc",
]
[[package]]
name = "event-listener"
version = "5.4.1"
@@ -568,6 +720,12 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -671,6 +829,12 @@ dependencies = [
"wasip2",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "half"
version = "2.7.1"
@@ -734,6 +898,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.12.0"
@@ -744,6 +914,19 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"web-time",
]
[[package]]
name = "indoc"
version = "2.0.7"
@@ -753,6 +936,24 @@ dependencies = [
"rustversion",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -838,6 +1039,16 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link 0.2.1",
]
[[package]]
name = "libm"
version = "0.2.15"
@@ -893,6 +1104,22 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "macro_rules_attribute"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
dependencies = [
"macro_rules_attribute-proc_macro",
"paste",
]
[[package]]
name = "macro_rules_attribute-proc_macro"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
[[package]]
name = "matchers"
version = "0.2.0"
@@ -946,6 +1173,12 @@ dependencies = [
"libmimalloc-sys",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "1.1.0"
@@ -957,6 +1190,28 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "monostate"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
dependencies = [
"monostate-impl",
"serde",
"serde_core",
]
[[package]]
name = "monostate-impl"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ndarray"
version = "0.16.1"
@@ -972,6 +1227,16 @@ dependencies = [
"rawpointer",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1029,6 +1294,12 @@ dependencies = [
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "numpy"
version = "0.27.0"
@@ -1052,6 +1323,28 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "onig"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
dependencies = [
"bitflags",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -1081,6 +1374,12 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1136,6 +1435,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
@@ -1157,14 +1462,14 @@ version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4"
dependencies = [
"base64",
"base64 0.22.1",
"byteorder",
"bytes",
"fallible-iterator",
"hmac",
"md-5",
"memchr",
"rand",
"rand 0.9.2",
"sha2",
"stringprep",
]
@@ -1248,6 +1553,15 @@ dependencies = [
"target-lexicon",
]
[[package]]
name = "pyo3-dlpack"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f4a0157d48883374fc6b3bf5f5b06f8563abb59113ab347ba8f11b6d9b9721"
dependencies = [
"pyo3",
]
[[package]]
name = "pyo3-ffi"
version = "0.27.0"
@@ -1298,14 +1612,35 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
@@ -1315,7 +1650,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
@@ -1333,6 +1677,37 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-cond"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9"
dependencies = [
"either",
"itertools 0.11.0",
"rayon",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1541,6 +1916,18 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spm_precompiled"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
dependencies = [
"base64 0.13.1",
"nom",
"serde",
"unicode-segmentation",
]
[[package]]
name = "stringprep"
version = "0.1.5"
@@ -1552,6 +1939,12 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
@@ -1647,6 +2040,38 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokenizers"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b08cc37428a476fc9e20ac850132a513a2e1ce32b6a31addf2b74fa7033b905"
dependencies = [
"aho-corasick",
"derive_builder",
"esaxx-rs",
"getrandom 0.2.16",
"indicatif",
"itertools 0.12.1",
"lazy_static",
"log",
"macro_rules_attribute",
"monostate",
"onig",
"paste",
"rand 0.8.5",
"rayon",
"rayon-cond",
"regex",
"regex-syntax",
"serde",
"serde_json",
"spm_precompiled",
"thiserror",
"unicode-normalization-alignments",
"unicode-segmentation",
"unicode_categories",
]
[[package]]
name = "tokio"
version = "1.48.0"
@@ -1694,7 +2119,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand",
"rand 0.9.2",
"socket2",
"tokio",
"tokio-util",
@@ -1824,14 +2249,19 @@ dependencies = [
"bytes",
"chrono",
"chrono-tz",
"cuda-driver-sys",
"cudarc",
"deadpool-postgres",
"futures-util",
"mimalloc",
"num_cpus",
"pyo3",
"pyo3-arrow",
"pyo3-dlpack",
"rayon",
"serde",
"serde_yaml",
"tokenizers",
"tokio",
"tokio-postgres",
"tracing",
@@ -1861,18 +2291,39 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-normalization-alignments"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
dependencies = [
"smallvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "unindent"
version = "0.2.4"
@@ -1985,6 +2436,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "whoami"
version = "1.6.1"
@@ -2135,6 +2596,15 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"

View File

@@ -1,67 +1,56 @@
[package]
name = "unchecked-io"
version = "0.1.7"
authors = ["Billthemaker"] # Replace with your name or alias
license = "BSL-1" # Good practice for open-source
authors = ["Billthemaker"]
license = "Apache-2.0"
edition = "2024"
[lib]
[lib]
name = "unchecked_io"
crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib", "rlib"]
[features]
default = []
gpu = [
"tokenizers",
"cudarc",
"pyo3-dlpack",
"cudarc/cuda-12050",
"dep:cuda-driver-sys"
]
profiling = ["dep:tracing", "dep:tracing-subscriber", "dep:tracing-tracy"]
[dependencies]
# 1. Python Bindings for FFI
pyo3 = { version = "=0.27.0", features = ["extension-module", "chrono-tz", "chrono"] }
# 2. Configuration Parsing (YAML)
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
# 3. Apache Arrow and Data Handling
# FIX: Add the "compute" feature to get concat_batches
arrow = "57.0.0"
# 4. Asynchronous Runtime (Essential for I/O and Postgres)
arrow = { version = "57.0.0", features = ["prettyprint"] }
tokio = { version = "1.37", features = ["full"] }
# 5. Database Connection (Postgres)
tokio-postgres = "0.7"
deadpool-postgres = "0.14"
# 6. Error Handling Crate
anyhow = "1.0"
# 7. Futures Utilities
futures-util = "0.3"
# 8. Byte Buffer Management
bytes = "1.6"
# 9. Binary Data Reading (NEW)
byteorder = "1.5"
# 10. Timestamp Handling (NEW)
chrono = "0.4"
chrono-tz = "=0.10.4"
# 11. UUID Handling (NEW)
uuid = { version = "1.8", features = ["serde", "v4"] }
# 12. Arrow <-> Python Bridge (NEW)
pyo3-arrow = "0.15.0"
# 13. System CPU Count (NEW)
num_cpus = "1.16"
async-channel = "2.5.0"
rayon = "1.10"
# Optional dependencies enabled by features
# UPGRADED: 0.20 supports modern Llama tokenizer exports
tokenizers = { version = "0.20", optional = true }
cudarc = { version = "0.11", features = ["driver"], optional = true }
pyo3-dlpack = { version = "0.1.0", optional = true }
cuda-driver-sys = { version = "0.3.0", optional = true }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"], optional = true}
tracing-tracy = { version = "=0.11.2", optional = true }
tracing = {version ="0.1.41", optional = true }
async-channel = "2.5.0"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
mimalloc = { version = "0.1.39" }
async-channel = "2.3"

67
HANDOFF-2026-04-20.md Normal file
View File

@@ -0,0 +1,67 @@
# Handoff — 2026-04-20
## What Was Decided This Session
- **Commercialization path:** Licensing first, IP sale after traction. Wyoming LLC before any contract.
- **Language strategy:** Rust is source of truth. C++ generated via Whetstone + manual library substitution as enterprise deliverable. Both versions is a deliberate differentiator.
- **Primary buyers:** GPU cloud providers and LLM inference companies (CoreWeave, Lambda Labs, Together AI, Fireworks AI, Modal). Not PyTorch/Meta.
- **Hugging Face** is a secondary buyer worth a conversation — they own the `tokenizers` crate this code already uses.
- **NVIDIA contact:** Approach after H100 benchmark. Send Nsight trace + one paragraph. Not before.
- **NM funding:** Apply for NMSBA (Sandia/LANL hardware access). Skip money grants for now.
- **Outsourcing:** Friend's software business for non-core work. 60-70% of contract value for defined scope. IP assignment clause required in writing first.
## Documents Created This Session
| File | Purpose |
|---|---|
| `ROADMAP.md` | Full technical development plan, Phases 15 |
| `docs/talk-10min.md` | 10-minute presentation structure for Wednesday |
| `docs/commercialization-strategy.md` | Pricing, B2B sales process, structure, outsourcing |
| `HANDOFF-2026-04-20.md` | This file |
---
## This Week — Time-Sensitive (in order)
### Before Wednesday
- [ ] Fix Phase 1.1 — dynamic buffer allocation (`src/llm.rs`, `PinnedBatcher::new`)
- [ ] Fix Phase 1.2 — buffer isolation / DLPack overwrite bug
- [ ] Fix Phase 1.3 — thread-safe engine pool (`TokenizerEngine` takes `&mut self`)
- [ ] Fix Phase 1.4 — padding for variable-length sequences
### Wednesday
- [ ] Rent H100 on Lambda Labs (~$2 for 30 min)
- [ ] Run `benchmark_gpu.py` under `nsys profile --stats=true python benchmark_gpu.py`
- [ ] Capture Nsight `.nsys-rep` file and download it
- [ ] Note the speedup number — this is the headline for the talk
### Wednesday/Thursday
- [ ] Update `docs/talk-10min.md` Slide 6 with the H100 result
- [ ] Open Nsight Systems GUI locally, load the `.nsys-rep` file, screenshot the timeline showing both pipelines
### Thursday (if someone gives you a contact after the talk)
- [ ] Do not show GPU source code without NDA signed first
- [ ] First call is discovery — ask about their infrastructure before pitching
- [ ] Pricing range if pushed: $50K$150K annually depending on scale
---
## Next Session Priorities (after this week)
1. **H100 setup script** — a single paste-into-terminal script for the cloud instance (was going to write this, ran out of session). Needs: Rust install, maturin build with `--features gpu`, nsys profile run, output capture.
2. **Phase 1 fixes** — if not completed before Wednesday, finish these
3. **vLLM integration** — Phase 2.1, this is the demo that sells the concept
4. **NM anonymous LLC** — straightforward to form locally, no member names in public filings, no annual report requirement
5. **C++ version scoping** — after Phase 1 is solid, assess Whetstone transpilation of `llm.rs`
---
## Key Technical Context for Next Session
- Repo is cloned at `/home/bill/Documents/unchecked-io/`, currently on `gpu-zero` branch
- GPU feature is compiled with `maturin develop --release --features gpu`
- The four Phase 1 bugs are documented in detail in `ROADMAP.md` Phase 1 section
- `src/llm.rs` is the file to fix — `PinnedBatcher` struct, `batch_encode_to_gpu` method
- `src/lib.rs` `TokenizerEngine` wraps `PinnedBatcher` and is the Python-facing class
- chrono-tz is pinned at `=0.10.4` — do not change this, it was hard-won
- `gpu-zero` branch is private; `main` branch is open source Apache 2.0

84
HANDOFF-2026-04-21.md Normal file
View File

@@ -0,0 +1,84 @@
# Handoff — 2026-04-21
## What Was Done This Session
Fixed all four Phase 1 correctness bugs. Code compiles clean on temp-ubuntu with CUDA 13.1.
---
## Phase 1 Bugs — ALL FIXED
### 1.1 Dynamic buffer allocation — DONE
**Old behavior:** `max_elements = 4096 * 512` hardcoded in `PinnedBatcher::new`. Any batch exceeding ~2M tokens returned an error.
**Fix:** Removed `gpu_buffer`, `host_buffer`, and `max_elements` from the `PinnedBatcher` struct entirely. Both buffers are now allocated per-call based on actual `batch_size * seq_len`.
### 1.2 Buffer isolation (DLPack overwrite) — DONE
**Old behavior:** Every call overwrote the same `CudaSlice`. Any tensor from call N got corrupted by call N+1.
**Fix:** Each call allocates its own device buffer and wraps it in a `DLPackContext` struct stored in `DLManagedTensor.manager_ctx`. The deleter frees both the device memory (by dropping `CudaSlice<i64>`) and the shape array when PyTorch releases the tensor. Each returned tensor now owns its allocation.
### 1.3 Thread safety — DONE
**Old behavior:** `batch_encode_to_gpu` took `&mut self`. `TokenizerEngine.encode_batch` took `&mut self`. Not usable from multiple Python threads.
**Fix:** Since `PinnedBatcher` no longer has mutable shared state (buffers are per-call), `batch_encode_to_gpu` now takes `&self`. `TokenizerEngine` wraps `Arc<PinnedBatcher>` instead of owning it directly. `encode_batch` takes `&self`. Concurrent calls from Python threads are safe.
### 1.4 Variable sequence length + padding — DONE
**Old behavior:** `seq_len = encodings[0].len()` — assumed uniform length. Variable-length batches produced wrong tensor shape.
**Fix:** `seq_len = encodings.iter().map(|e| e.len()).max()`. Host buffer prefilled with `pad_id` (from `tokenizer.get_padding()`, fallback to 0). Rayon fill loop only writes actual tokens; shorter sequences retain the pad value.
---
## Build Issue Discovered and Resolved
`cudarc 0.11.9` panics at build time on CUDA 13.1 (only knows 12.5 and below). This machine has CUDA 13.1.
**Fix:** Changed the `gpu` feature in `Cargo.toml` from `cudarc/cuda-version-from-build-system` to `cudarc/cuda-12050`. This hard-pins to CUDA 12.5 headers and skips the `nvcc` version check. CUDA 13.x is backward-compatible with 12.5 driver API at runtime. If you ever upgrade cudarc to 0.12+, revert this change and re-test.
---
## Files Changed
| File | What Changed |
|------|-------------|
| `src/llm.rs` | Full rewrite of `PinnedBatcher` — removed static buffers, per-call allocation, `DLPackContext` for ownership, `&self` throughout |
| `src/lib.rs` | `TokenizerEngine.inner` changed to `Arc<PinnedBatcher>`, `encode_batch` takes `&self`, added `use std::sync::Arc` |
| `Cargo.toml` | `gpu` feature: `cuda-version-from-build-system``cuda-12050` |
---
## This Week — What's Left (in order)
### Today / Tomorrow (Tuesday 2026-04-22)
- [ ] Rent H100 on Lambda Labs (~$2 for 30 min)
- [ ] Run `benchmark_gpu.py` under `nsys profile --stats=true python benchmark_gpu.py`
- [ ] Capture Nsight `.nsys-rep` file and download it
- [ ] Note the speedup number — this is the headline for Thursday's talk
**Still need:** H100 setup script (a single paste-into-terminal script for the cloud instance). Needs: Rust install, maturin build with `--features gpu`, nsys profile run, output capture. This was not written last session.
### Wednesday (2026-04-23)
- [ ] Update `docs/talk-10min.md` Slide 6 with the H100 result
- [ ] Open Nsight Systems GUI locally, load the `.nsys-rep` file, screenshot the timeline showing both pipelines
### Thursday (2026-04-24)
- [ ] Talk to IT/security group (10 min)
- [ ] If someone gives you a contact: do not show GPU source code without NDA signed first; first call is discovery
---
## Next Session Priorities (after Thursday)
1. **Write H100 setup script** — single paste-into-terminal script (see above)
2. **vLLM integration** — Phase 2.1, this is the demo that sells the concept
3. **NM anonymous LLC** — Wyoming, single-member, before any contract is signed
4. **C++ version scoping** — after Phase 1 solid, assess Whetstone transpilation of `llm.rs`
---
## Key Technical Context
- Repo: `/home/bill/Documents/unchecked-io/`, on `gpu-zero` branch
- Build: `maturin develop --release --features gpu` (maturin not installed on temp-ubuntu — install with `pip install maturin` first)
- `cargo check --features gpu` confirms clean compile
- The four Phase 1 bugs are fixed and verified to compile
- `src/llm.rs` is the GPU core. `src/lib.rs` is the Python-facing wrapper.
- `chrono-tz` pinned at `=0.10.4` — do not change
- `gpu-zero` branch is private; `main` is open source Apache 2.0

387
LICENSE
View File

@@ -1,373 +1,70 @@
Mozilla Public License Version 2.0
==================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
1. Definitions
--------------
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1. Definitions.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
1.3. "Contribution"
means Covered Software of a particular Contributor.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
1.5. "Incompatible With Secondary Licenses"
means
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
1.8. "License"
means this document.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
1.10. "Modifications"
means any of the following:
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
(b) any new file in Source Code Form that contains any Covered
Software.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
2. License Grants and Conditions
--------------------------------
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
2.1. Grants
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
END OF TERMS AND CONDITIONS
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
How to apply the Apache License to your work
Include a copy of the Apache License, typically in a file called LICENSE, in your work, and consider also including a NOTICE file that references the License.
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
To apply the Apache License to specific files in your work, attach the following boilerplate declaration, replacing the fields enclosed by brackets "[]" with your own identifying information. (Don't include the brackets!) Enclose the text in the appropriate comment syntax for the file format. We also recommend that you include a file or class name and description of purpose on the same "printed page" as the copyright notice for easier identification within third-party archives.
2.2. Effective Date
Copyright [yyyy] [name of copyright owner]
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
2.3. Limitations on Grant Scope
http://www.apache.org/licenses/LICENSE-2.0
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

160
ROADMAP.md Normal file
View File

@@ -0,0 +1,160 @@
# UncheckedIO — Development Roadmap
**Goal:** Production-grade, multi-GPU zero-copy ingestion pipeline for LLM providers.
**Current state:** Working proof-of-concept on CUDA (gpu-zero branch). PostgreSQL→Arrow open source on main.
---
## Phase 1 — Fix Correctness (gpu-zero branch)
The GPU feature works in benchmark conditions but has four bugs that would surface immediately in production. Fix these before anything else.
### 1.1 Dynamic buffer allocation
**Problem:** `max_elements = 4096 * 512` is hardcoded at init time. Modern LLMs have 32K128K context windows. A single request at 32K tokens + any real batch size hits this limit.
**Fix:** Allocate `gpu_buffer` and `host_buffer` per-call based on actual `batch_size * max_seq_len`, or use a growable strategy that reallocates when the current batch exceeds the existing buffer size.
### 1.2 Buffer isolation (silent data corruption bug)
**Problem:** Every call to `encode_batch` overwrites the same `CudaSlice`. The DLPack capsule hands PyTorch a pointer to that same memory. A caller holding a tensor from call N gets its data silently overwritten by call N+1.
**Fix:** Double-buffer (ping-pong between two allocations) or allocate a fresh device buffer per call and transfer ownership to the DLPack capsule's deleter so CUDA frees it when PyTorch releases the tensor.
### 1.3 Thread safety
**Problem:** `encode_batch` takes `&mut self`. The `TokenizerEngine` Python class cannot be called from multiple threads simultaneously. Every production serving engine (vLLM, TGI) is multithreaded.
**Fix:** Wrap `PinnedBatcher` in an `Arc<Mutex<>>` or, better, create a `EnginePool` that holds N instances and checks one out per call. Pool size = CPU core count is a reasonable default.
### 1.4 Variable sequence length + padding
**Problem:** `seq_len = encodings[0].len()` assumes all sequences in the batch have identical length. The `tokenizers` crate doesn't apply padding by default, so real variable-length inputs produce an incorrect 2D tensor shape.
**Fix:** Apply padding to `max(encodings[i].len())` within the batch, fill with the tokenizer's `pad_id`, and use the actual padded length as `seq_len`.
---
## Phase 2 — Production Features
### 2.1 vLLM integration
vLLM is the dominant open-source LLM serving engine. Make `unchecked_io` a drop-in tokenization backend for it.
- Implement the `TokenizerGroup` protocol vLLM expects
- The continuous batching scheduler sends variable-length requests asynchronously — this is where Phase 1 fixes must be solid
- Target: a 3-line config change in vLLM to switch to the UncheckedIO backend
- Deliverable: `examples/vllm_integration.py` showing the swap
### 2.2 Streaming / chunked output
**Current:** The entire batch is tokenized and DMA'd in one blocking call.
**Target:** Yield Arrow RecordBatches or DLPack tensors incrementally as tokens are ready, so the model can begin prefill before the full batch is staged.
### 2.3 Attention mask export
LLM inference requires both `input_ids` and `attention_mask`. Currently only token IDs are exported. Add a second DLPack capsule for the mask, or pack both into a named dict capsule.
### 2.4 Configurable max context
Replace the hardcoded constants with constructor arguments:
```python
engine = TokenizerEngine(
tokenizer_path="tokenizer.json",
max_seq_len=32768,
max_batch_size=64,
pool_size=8
)
```
---
## Phase 3 — Multi-GPU Support
### 3.1 ROCm / HIP (AMD) — highest priority
AMD's MI300X has 192GB memory vs the H100's 80GB and providers are actively evaluating it to escape NVIDIA pricing. HIP is the path of least resistance — function names are nearly 1:1 with CUDA (`hipMemHostRegister`, `hipMemcpyHtoD`, `hipMemHostUnregister`).
- Add `rocm` feature flag in `Cargo.toml`
- Create `src/backends/rocm.rs` implementing the same `GpuBackend` trait
- Use AMD's `hip-sys` or `rocm-sys` Rust crate
- The DLPack export layer stays identical — device_type `10` for ROCm vs `2` for CUDA
**Refactor needed first:** Extract a `GpuBackend` trait from the current CUDA-specific code:
```rust
trait GpuBackend {
fn allocate_pinned(capacity: usize) -> Result<PinnedBuffer>;
fn copy_to_device(src: &[i64], dst: &mut DeviceBuffer) -> Result<()>;
fn device_ptr(buf: &DeviceBuffer) -> u64;
fn dlpack_device_type() -> i32;
}
```
### 3.2 OpenCL (broad coverage)
Covers AMD, Intel, older NVIDIA, Apple (via Metal-OpenCL bridge). Lower peak performance than native CUDA/HIP but maximum hardware coverage. Use the `opencl3` Rust crate.
- Add `opencl` feature flag
- Useful for providers running mixed-GPU clusters
### 3.3 Intel Gaudi / Level Zero
Intel's Gaudi 2/3 accelerators are gaining traction for training and are available on AWS and Azure. Intel Level Zero is the low-level API equivalent to CUDA driver API. Lower priority than ROCm but worth tracking.
---
## Phase 4 — Benchmark Suite
The current `benchmark_gpu.py` is good for what it tests but synthetic. A buyer will run their own benchmarks — make it easy and cover real scenarios.
### 4.1 Scenarios to cover
- Short sequences: 128, 256 tokens (BERT-style)
- Medium sequences: 2048, 4096 tokens (typical chat)
- Long sequences: 16K, 32K tokens (document/RAG workloads)
- Variable length batches (realistic distribution, not identical strings)
- Concurrent throughput: simulate N async callers
### 4.2 Tokenizers to cover
- BERT (`bert-base-uncased`) — already done
- LLaMA 3 tokenizer (BPE, more representative of modern LLMs)
- GPT-2 tokenizer (common baseline)
### 4.3 Hardware targets
- Local RTX 3060 (done, 4.7x)
- Cloud H100 SXM (~$2 on Lambda Labs, 30 min run) — **do this first, it changes the pitch**
- AMD MI300X (after Phase 3.1)
### 4.4 What to measure
- Tokens/second throughput
- Latency per batch (p50, p95, p99)
- GPU utilization % (the thing providers care about)
- Memory bandwidth saturation
---
## Phase 5 — Business Infrastructure
### 5.1 Wyoming LLC formation
- Single-member LLC, Wyoming (strongest privacy, no public member disclosure)
- LLC owns all IP
- Registered agent handles public address
- Done before any contract is signed
### 5.2 License structure
- Open core: PostgreSQL→Arrow stays Apache 2.0
- GPU features: commercial license
- Pricing model: annual per-cluster license, priced against GPU utilization savings
- License enforcement: honor-system enterprise contracts initially, not technical DRM
### 5.3 Sales collateral
- 1-page technical brief (problem / mechanism / numbers / what's included)
- Self-contained benchmark the prospect can run on their own hardware
- Reference architecture diagram showing where this sits in a vLLM serving stack
---
## Branch Strategy
| Branch | Purpose |
|--------|---------|
| `main` | Open source PostgreSQL→Arrow, Apache 2.0, public |
| `gpu-zero` | GPU feature development, private |
| `rocm` | AMD port (Phase 3.1) |
| `release/x.y` | Tagged releases for licensing |
The `gpu` feature is compiled in only with `--features gpu`. The open source build never includes GPU code.
---
## Immediate Next Steps (in order)
1. Fix 1.1 (dynamic buffer) — unblocks everything else
2. Fix 1.2 (buffer isolation) — correctness before benchmarking
3. Fix 1.3 (thread safety) — required for any real serving test
4. Fix 1.4 (padding) — correctness
5. Spend $2, run benchmark on cloud H100 — this one number changes the pitch
6. Phase 2.1 vLLM integration — this is the demo that sells the concept
7. Phase 3.1 ROCm — this is the differentiator

View File

@@ -4,19 +4,34 @@ import sqlalchemy
import connectorx as cx
import unchecked_io
import os
import yaml # We need pyyaml for this
import yaml
import time
# --- 1. Define Connection Strings and Query ---
# These must match your local Docker setup
DB_USER = "postgres"
DB_PASS = "mysecretpassword"
DB_HOST = "localhost"
DB_PORT = "5433" # <-- Your local Docker port
DB_PORT = "5433"
DB_NAME = "postgres"
# Global Configuration
BLAST_RADIUS = 312500 # Rows per parallel task (1M / 62500 = 16 partitions)
# Rows per parallel task (1M / 62500 = 16 partitions)
#BLAST_RADIUS = 1250000 # 16 partitions
#BLAST_RADIUS = 625000 # 32 partitions
#BLAST_RADIUS = 312500 # 64 partitions
#BLAST_RADIUS = 156250 # 128 partitions
#BLAST_RADIUS = 125000 # 160 partitions
#BLAST_RADIUS = 12500 # 1600 partitions
BLAST_RADIUS = 1250 # 16000 partitions
BATCH_SIZE = 262144 # Aggregation Size (Large Memory chunks)
# SQLAlchemy connection string (for Pandas)
sqlalchemy_conn_str = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
@@ -35,6 +50,7 @@ unchecked_io_query = "COPY (SELECT id::BIGINT, uuid::TEXT, username::TEXT, score
config_data = {
'connection_string': connectorx_conn_str,
'query': unchecked_io_query,
'batch_size': BATCH_SIZE, # <--- This explicitly writes your setting to the file
'schema': [
{'column_name': 'id', 'arrow_type': 'Int64'},
{'column_name': 'uuid', 'arrow_type': 'Utf8'},
@@ -53,6 +69,8 @@ with open(config_file, 'w') as f:
yaml.dump(config_data, f)
print(f"UncheckedIO Config: {config_file} (dynamically created for local test)")
print(f" - Blast Radius: {BLAST_RADIUS}")
print(f" - Batch Size: {BATCH_SIZE}")
# --- 3. Define Benchmark Functions ---
@@ -73,14 +91,6 @@ def test_unchecked_io():
# --- 4. Run Benchmarks ---
run_count = 1
print(f"Running benchmarks for 20,000,000 rows (average of {run_count} runs)...")
print(f"Blast Radius: {BLAST_RADIUS} rows per task")
# --- Pandas ---
# print("\nRunning Pandas warmup...")
# _ = test_pandas()
# print("Timing pandas.read_sql...")
# pandas_time = timeit.timeit(test_pandas, number=run_count) / run_count
# print(f"Pandas Average Time: {pandas_time * 1000:.2f} ms")
# --- ConnectorX ---
print("\nRunning ConnectorX warmup...")

114
benchmark_gpu.py Normal file
View File

@@ -0,0 +1,114 @@
import time
import torch
import os
import json
import unchecked_io
from transformers import AutoTokenizer
# --- CONFIGURATION ---
MODEL_ID = "bert-base-uncased"
TOKENIZER_PATH = "tokenizer.json"
BATCH_SIZE = 4096 # Massive batch to saturate PCIe
NUM_BATCHES = 50 # Run enough to stabilize GPU clock
SEQ_LEN = 128 # Typical sentence length
WARMUP = 5
# Synthetic Data
SAMPLE_TEXT = "The quick brown fox jumps over the lazy dog. " * 5 # ~50 tokens
DATA_BATCH = [SAMPLE_TEXT for _ in range(BATCH_SIZE)]
print(f"--- GPU PIPELINE BENCHMARK (RTX 3060) ---")
print(f"Batch Size: {BATCH_SIZE} | Batches: {NUM_BATCHES}")
print(f"Total Strings processed: {BATCH_SIZE * NUM_BATCHES:,}")
# --- 1. SETUP RESOURCES ---
# A. Prepare Tokenizer File for Rust
if not os.path.exists(TOKENIZER_PATH):
print(f"Downloading {MODEL_ID} tokenizer...")
# forcing use_fast=True ensures we get the JSON file Rust needs
hf_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
hf_tokenizer.save_pretrained(".")
print("Saved tokenizer.json")
else:
print("Found existing tokenizer.json")
hf_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
# B. Initialize Engines
print("Initializing Engines...")
# UncheckedIO (Rust + CUDA)
try:
rust_engine = unchecked_io.TokenizerEngine(TOKENIZER_PATH)
print("✅ UncheckedIO Engine Loaded")
except AttributeError:
print("❌ Error: UncheckedIO 'TokenizerEngine' not found.")
print("Did you compile with: maturin develop --features gpu")
exit(1)
# --- 2. BENCHMARK LOOPS ---
def benchmark_standard():
torch.cuda.synchronize()
start = time.time()
for _ in range(NUM_BATCHES):
# Step 1: Tokenize (CPU Python/Rust mix)
# return_tensors='pt' creates a CPU tensor
encodings = hf_tokenizer(
DATA_BATCH,
padding=True,
truncation=True,
max_length=SEQ_LEN,
return_tensors="pt"
)
# Step 2: Move to GPU (The Bottleneck)
input_ids = encodings["input_ids"].to("cuda", non_blocking=True)
# Force sync to measure actual completion
torch.cuda.synchronize()
return time.time() - start
def benchmark_unchecked():
torch.cuda.synchronize()
start = time.time()
for _ in range(NUM_BATCHES):
# Step 1 & 2: Tokenize + DMA to GPU (All in Rust)
dlpack_capsule = rust_engine.encode_batch(DATA_BATCH)
# Step 3: Zero-Copy Wrap (Python)
# This is virtually instant
input_ids = torch.from_dlpack(dlpack_capsule)
# Force sync
torch.cuda.synchronize()
return time.time() - start
# --- 3. RUN RACES ---
print("\nWARMING UP GPU...")
# Run a few dummy passes to wake up the 3060
_ = hf_tokenizer(DATA_BATCH[:10], return_tensors="pt")["input_ids"].to("cuda")
_ = torch.from_dlpack(rust_engine.encode_batch(DATA_BATCH[:10]))
print("\n🚀 RUNNING STANDARD PIPELINE (HF -> CPU Tensor -> CUDA)...")
time_std = benchmark_standard()
fps_std = (BATCH_SIZE * NUM_BATCHES) / time_std
print(f"Time: {time_std:.4f}s | Throughput: {fps_std:,.0f} samples/sec")
print("\n🚀 RUNNING UNCHECKED PIPELINE (Rust -> Pinned -> CUDA)...")
time_unchecked = benchmark_unchecked()
fps_unchecked = (BATCH_SIZE * NUM_BATCHES) / time_unchecked
print(f"Time: {time_unchecked:.4f}s | Throughput: {fps_unchecked:,.0f} samples/sec")
# --- 4. RESULTS ---
speedup = fps_unchecked / fps_std
print(f"\n🏆 WINNER: {'UncheckedIO' if speedup > 1 else 'Standard'}")
print(f"SPEEDUP FACTOR: {speedup:.2f}x")
print(f"Standard Latency per Batch: {(time_std/NUM_BATCHES)*1000:.2f}ms")
print(f"Unchecked Latency per Batch: {(time_unchecked/NUM_BATCHES)*1000:.2f}ms")

View File

@@ -1,3 +1,4 @@
batch_size: 524288
connection_string: postgresql://postgres:mysecretpassword@localhost:5433/postgres
query: COPY (SELECT id::BIGINT, uuid::TEXT, username::TEXT, score::REAL, is_active::BOOLEAN,
last_login::TIMESTAMP, notes::TEXT, course_id::INT, start_date::DATE, rating::FLOAT8

0
docs/adaptive_config.md Normal file
View File

View File

@@ -0,0 +1,206 @@
# UncheckedIO — Commercialization Strategy
**Session date:** 2026-04-20
**Status:** Active planning
---
## Product Summary
Two components with different commercial treatment:
| Component | Language | License | Status |
|---|---|---|---|
| PostgreSQL → Arrow pipeline | Rust | Apache 2.0 (open source) | On PyPI, working |
| GPU zero-copy ingestion pipeline | Rust + CUDA | Commercial | Proof-of-concept, needs Phase 1 fixes |
The GPU pipeline is the commercial product. The Postgres piece is the credibility signal and open-source loss leader.
---
## The Problem Being Solved
GPU starvation — LLM providers run 30-50% actual GPU utilization on inference. The bottleneck is the data ingestion pipeline: CPU tokenizes, creates a Python tensor in pageable memory, `.to("cuda")` performs two OS-mediated copies (pageable → kernel buffer → GPU).
UncheckedIO replaces this with: Rust parallel tokenization → pinned memory (DMA-accessible) → single direct DMA → DLPack zero-copy handover to PyTorch.
**Current benchmark:** 4.7x faster than standard HuggingFace pipeline on RTX 3060.
**Pending:** H100 SXM benchmark (Wednesday, ~$2 on Lambda Labs). This number changes the pitch.
---
## Business Structure
**Entity:** New Mexico single-member LLC
- NM does not require member names in Articles of Organization
- No annual report requirement — less administrative overhead than most states
- LLC owns all IP and signs all contracts
- Registered agent handles public address
- Form this before any contract is signed
- Note: if the IP is eventually sold, the buyer's anonymity is their own concern — the LLC structure only needs to protect the original author
**Operating model:** Anonymous principal — LLC is the public face. The identity of the technical author is not disclosed to buyers or the public. Any investor or partner arrangement must include explicit confidentiality provisions.
---
## Go-to-Market Strategy
### Phase 1: Licensing (current goal)
License the GPU pipeline commercially. Annual per-cluster licenses. Open-source Postgres piece stays Apache 2.0 as a credibility and discovery channel.
**Why licensing before IP sale:**
A library generating $50K+/month in licenses is a business acquisition, not a proof-of-concept. The IP sale price increases significantly and the negotiating position improves. Licensing revenue demonstrates market validation.
### Phase 2: IP Sale (after traction)
Once licensing revenue establishes market proof, approach GPU cloud providers and LLM infrastructure companies for outright IP purchase. Source code + short-term handoff contract. Clean exit.
---
## Pricing
The ROI math for a GPU provider: 100 H100s at $175K/year each = $17.5M in GPU costs. A 10% utilization improvement = $1.75M saved. A $50K license is a 35x ROI.
**Do not anchor at $50K. That is the floor for small deployments.**
| Deployment size | Annual license | One-time source sale |
|---|---|---|
| Small (50200 GPUs) | $25K$75K | $100K$200K |
| Medium (2001,000 GPUs) | $75K$200K | $300K$600K |
| Large (1,000+ GPUs) | $200K$500K | $750K$1.5M |
**First client:** Accept below market rate in exchange for a private reference and permission to say "deployed at [company]" to the next prospect. That reference is worth more than the price difference.
---
## Target Buyers
**Primary (write the check):**
GPU cloud providers and LLM inference companies feel GPU starvation directly in their margins. They sell GPU utilization as a product. Any measurable improvement is direct revenue.
- CoreWeave, Lambda Labs — GPU cloud, sell utilization by the hour
- Together AI, Fireworks AI, Modal — LLM inference providers
- Anyscale — Ray-based ML infrastructure
**Secondary (may buy, slower process):**
- Hugging Face — owns the `tokenizers` Rust library this code uses, runs TGI serving stack. More plausible buyer than PyTorch/Meta.
- Databricks (MosaicML) — LLM training infrastructure
**Do not pitch:**
- PyTorch/Meta — open source project, they'd implement it themselves
- Hyperscalers (AWS, GCP, Azure) — too slow, too much process
**NVIDIA contact:**
Wait until H100 benchmark is complete. Send the Nsight trace + one paragraph. They may be interested as an ecosystem/marketing play ("runs X% faster on H100"). Do not approach before having the measurement.
---
## B2B Sales Process
### First contact (Zoom call)
The first call is discovery, not pitch. Goal: understand their specific infrastructure before presenting a solution.
**Ask first:**
- What GPU hardware? (H100, A100, MI300X — how many?)
- What serving stack? (vLLM, TGI, custom?)
- Current GPU utilization rate, if known?
- What caught their attention?
**Then:** "Based on what you're describing, here's specifically where this helps you..."
**On pricing:** Don't volunteer it first. If pressed: *"For a deployment your size I'd expect $50K$150K annually. I want to understand your infrastructure better before giving you a specific number."*
**End of first call:** Set up follow-up with their infrastructure lead. Do not sign anything on the first call.
### NDA
Get an NDA signed before showing the GPU source code on any call. The open-source Postgres piece is fine to show. The GPU pipeline (`src/llm.rs`, `gpu-zero` branch) is not.
### Solo developer objection
Enterprise buyers will ask: *"What happens if you're unavailable?"*
**Answers:**
1. Source code sale model answers it directly — they own the code
2. Technical partner (friend's software business) provides continuity
3. Source code escrow clause: code held by third party, released if unavailable for 90 days — this is standard and removes most of the objection
---
## Maintenance and Support
**Reality for 12 enterprise clients:**
| Phase | Time commitment |
|---|---|
| Active development (first 36 months) | 2040 hrs/week |
| Steady state | 510 hrs/week per client |
| Major CUDA/PyTorch version update | 24 days, happens 23x/year |
| Production incident | Drop everything, 13 days |
**Scope the support in the contract:** Cover current PyTorch LTS + one prior release. Anything newer is a paid upgrade. This prevents unbounded compatibility obligations.
One person can support 23 enterprise clients at steady state.
---
## Outsourcing
A friend with a software business can handle work that is not the core differentiator:
- Support ticket triage and first-response
- Documentation and integration guides
- Testing infrastructure (benchmark runs against new PyTorch/CUDA versions)
- Non-GPU features (Postgres pipeline, Python API polish)
**Do not outsource:** The GPU pipeline code, benchmarking methodology, CUDA/ROCm work — these are the differentiator.
**Rate:** Pass 6070% of contract value for defined scope. On a $100K contract where the friend does 30% of the work: $18K$21K to them. You take the larger share because you carry the business development risk and hold the client relationship.
**Critical:** Any work they do on the core library must be IP-assigned to the LLC in writing before the first engagement.
---
## Multi-Language Strategy (Rust + C++)
**Why both:**
- Rust is the development language — memory safety makes GPU pipeline bugs easier to catch
- C++ is the enterprise deliverable — the entire ML infrastructure ecosystem (cuDNN, TensorRT, vLLM C++ backend) is C++
- Having both is a differentiator from vibe-coded projects; it signals deep systems understanding
**How:**
- WhetstoneAI (separate project) can transpile language primitives automatically
- Library dependencies require manual substitution (see below)
- Rust is source of truth; C++ is a generated + manually doctored artifact per release
**Library substitutions for C++ version:**
| Rust dependency | C++ equivalent | Complexity |
|---|---|---|
| `cudarc` | Direct CUDA C++ calls (actually simpler) | Low |
| `rayon` | OpenMP `#pragma omp parallel for` | Low |
| `tokenizers` | SentencePiece (LLaMA) + tiktoken (GPT) | Medium — different API |
| `pyo3` | pybind11 | Medium — parallel structure |
| `tokio` | Not needed for GPU-only C++ version | N/A |
`parser.rs` (Postgres pipeline) is not worth transpiling — stays Rust only.
**Estimated work for C++ `llm.rs`:** 23 days after Whetstone handles primitives.
---
## NM State Funding Assessment
**Worth pursuing:**
- **NM Small Business Assistance Program (NMSBA)** — pairs small businesses with Sandia/LANL. The value is free access to HPC hardware (H100/A100 clusters), not the money. Solves the enterprise benchmark problem. Apply for this.
**Not worth pursuing (for now):**
- NMEDD money grants, angel tax credits — require registered business, matching funds, 36 month timelines. Time cost doesn't pencil out against a direct licensing deal.
**Key point:** Most state grant programs want an ongoing business, not a one-time IP sale. If the goal shifts toward a licensing business, revisit.
---
## Immediate Priorities
1. Fix Phase 1 correctness issues (see ROADMAP.md) — nothing else matters until these are done
2. Wednesday: H100 benchmark on Lambda Labs (~$2), run under `nsys profile`
3. Thursday/Friday: Incorporate H100 numbers into talk slides
4. Talk: 10-minute presentation to IT/Cybersecurity group (see docs/talk-10min.md)
5. Form Wyoming LLC before any serious sales conversation
6. After first client: evaluate C++ version via Whetstone transpilation

1
docs/refactor-prompt Normal file
View File

@@ -0,0 +1 @@
I need to refactor src/parser.rs to implement a "Worker Pool" pattern that strictly limits active database queries to the number of CPU cores.The Goal:Currently, the code spawns a Tokio task for every partition immediately. This floods the scheduler and causes database thrashing (too many concurrent COPY commands).I want to spawn a fixed number of workers (e.g., 16) that consume tasks from a queue. This ensures that if I have 1,600 partitions, only 16 COPY commands are active on the database at any given time.Requirements:Dependencies: Use async-channel (assume it's in Cargo.toml).Concurrency Limit:let num_workers = num_cpus::get();Set deadpool_postgres max_size to num_workers.The Queue:Create a channel: let (tx, rx) = async_channel::bounded(num_workers * 2); (Backpressure is good).Spawn a separate "Distributor" task that iterates partitions and sends them into tx.The Workers:Spawn exactly num_workers tasks.Each worker loops: while let Ok(task) = rx.recv().await.Inside the loop:Get a connection from the pool.Run copy_out(task.query).Parse the result.Store the resulting RecordBatch.Output:Collect all RecordBatch results from all workers.Flatten and concatenate them using concat_batches.Why: This ensures Postgres never sees a queue of queries. It only sees active workers. The "queue" lives entirely in Rust memory.

110
docs/talk-10min.md Normal file
View File

@@ -0,0 +1,110 @@
# UncheckedIO — 10-Minute Technical Talk
**Audience:** IT/Cybersecurity group with programmers and ML engineers
**Goal:** Plant the seed, demonstrate the mechanism, invite connections to LLM infrastructure teams
---
## Slide 1 — Open with the hidden thing (0:000:45)
Lead with an Nsight Systems timeline screenshot. Don't explain it yet.
> "This is what happens inside your GPU when a language model processes your request.
> The orange is waiting. The GPU costs $30,000 and it's waiting for data."
Let the image do the work. This audience will immediately want to know what they're looking at.
---
## Slide 2 — The problem (0:452:00)
**GPU starvation.** Define it in dollar terms.
- Real LLM providers run at 3050% actual GPU utilization on inference
- An 8-GPU H100 cluster costs ~$20/hr to rent
- At 40% utilization, $12/hr is wasted — the GPUs are idle waiting for data
- LLM providers run hundreds of clusters
- The waste is structural and nobody has fixed the root cause
---
## Slides 34 — The mechanism (2:004:00)
Two diagrams, side by side.
**Standard pipeline:**
```
CPU tokenizes → Python tensor (pageable memory) → .to("cuda") → kernel buffer → GPU
^--- two OS-mediated copies
```
**UncheckedIO pipeline:**
```
Rust tokenizes in parallel → pinned memory (DMA-accessible) → direct DMA → DLPack → GPU
^--- one copy, no kernel involvement
```
The second diagram has fewer boxes and fewer arrows. That is the entire pitch.
Key terms to explain briefly:
- **Pinned memory** — memory registered with the OS so the GPU's DMA engine can read it directly
- **DMA** — the GPU pulls the data itself, no CPU involvement after the transfer starts
- **DLPack** — a standard zero-copy tensor exchange protocol; PyTorch accepts it natively with `torch.from_dlpack()`
---
## Slide 5 — The Nsight demo (4:007:00)
Show the recorded Nsight Systems timeline from the H100 benchmark run.
Both pipelines visible side by side:
- Point out the idle gaps (orange) in the standard pipeline
- Point out the absence of idle gaps in the UncheckedIO pipeline
- Show the DMA transfer lane — this is the "hidden thing" made visible
This is the centerpiece of the talk. Expect questions here. That's good.
---
## Slide 6 — The number (7:008:00)
One slide. No prose.
| Hardware | Speedup |
|----------|---------|
| RTX 3060 (local) | 4.7x |
| H100 SXM (cloud) | [Wednesday result] |
---
## Slide 7 — Market fit (8:009:00)
- This is infrastructure every LLM provider needs
- Built as an open-core library: PostgreSQL→Arrow is Apache 2.0 and already on PyPI
- GPU ingestion pipeline is the commercial layer
- AMD ROCm support (MI300X) is next — providers are actively evaluating MI300X to escape H100 pricing
Don't pitch deal specifics. This slide plants the seed.
---
## Slide 8 — The close (9:0010:00)
> "I'm looking for connections to ML infrastructure teams and LLM providers.
> If you know someone building at that layer, I'd appreciate an introduction."
Direct. Not desperate. Leave room for questions.
---
## Notes
**What NOT to do:**
- Don't go deep on the Rust implementation — the visual is the hook, not the code
- Don't mention the PostgreSQL→Arrow piece — it's a distraction in this context
- Don't volunteer that it's unfinished — if asked, "it's in active development and the core mechanism is proven"
**For the well-connected engineer (separate talk, two weeks out):**
Don't reuse this version. Prep a deeper, tailored pitch after seeing what questions come up here.
**For the NVIDIA contact:**
Wait until after the H100 benchmark. Send the Nsight trace + one paragraph. Leading with a measurement is different from leading with a claim.

View File

@@ -4,8 +4,33 @@ build-backend = "maturin"
[project]
name = "unchecked-io"
version = "0.1.0"
# Dynamic versioning allows maturin to read version from Cargo.toml
dynamic = ["version"]
description = "The world's fastest, most dangerous PostgreSQL-to-Arrow loader."
readme = "README.md"
requires-python = ">=3.9"
license = {text = "BSL-1.1"}
authors = [{name = "Billthemaker"}]
keywords = ["rust", "postgresql", "arrow", "etl", "fast", "copy", "database"]
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: 3",
]
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Database",
"Topic :: Scientific/Engineering :: Information Analysis",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
]
dependencies = [
"arro3-core>=0.1.0",
]
[tool.maturin]
features = ["pyo3/extension-module"]

109
server.py Normal file
View File

@@ -0,0 +1,109 @@
import os
import time
import torch
import uvicorn
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import AutoModelForCausalLM, AutoTokenizer
# Import your custom high-speed engine
import unchecked_io
# --- CONFIGURATION ---
# Switching to GPT-2 for stability. It is small, fast, and has a standard tokenizer
# that won't crash older/newer Rust crate versions.
MODEL_ID = "gpt2"
TOKENIZER_FILE = "tokenizer.json"
# Global state container
model_state = {
"model": None,
"rust_engine": None,
"py_tokenizer": None
}
@asynccontextmanager
async def lifespan(app: FastAPI):
print(f"\n--- 🚀 UNCHECKED SERVER STARTUP ---")
# 1. Fetch Tokenizer (Python side)
print(f"1. Fetching Tokenizer Config from {MODEL_ID}...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
tokenizer.save_pretrained(".")
model_state["py_tokenizer"] = tokenizer
# 2. Initialize UncheckedIO (Rust + CUDA)
print(f"2. Initializing UncheckedIO (Fuel Injector)...")
try:
model_state["rust_engine"] = unchecked_io.TokenizerEngine(TOKENIZER_FILE)
print(" ✅ Rust Engine Ready (Zero-Copy Pipeline Active)")
except Exception as e:
print(f" ❌ Failed to load Rust Engine: {e}")
raise e
# 3. Load Model (PyTorch)
print(f"3. Loading Model Weights...")
try:
model_state["model"] = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="cuda",
torch_dtype=torch.float16
)
print(f" ✅ Model Loaded on {model_state['model'].device}")
except Exception as e:
print(f" ❌ Failed to load model: {e}")
raise e
print("--- SERVER READY ---\n")
yield
print("\n--- SERVER SHUTDOWN ---")
# Resources are cleaned up here
app = FastAPI(title="UncheckedIO High-Speed Server", lifespan=lifespan)
class GenerateRequest(BaseModel):
prompt: str
max_tokens: int = 50
temperature: float = 0.7
@app.post("/generate")
async def generate_text(req: GenerateRequest):
model = model_state["model"]
engine = model_state["rust_engine"]
tokenizer = model_state["py_tokenizer"]
if not model or not engine:
raise HTTPException(status_code=503, detail="Server not ready")
try:
# --- PHASE 1: UNCHECKED INGESTION (Rust) ---
# 4.8x Faster than standard .to("cuda")
input_capsule = engine.encode_batch([req.prompt])
input_ids = torch.from_dlpack(input_capsule)
# --- PHASE 2: INFERENCE (PyTorch) ---
with torch.no_grad():
output_ids = model.generate(
input_ids,
max_new_tokens=req.max_tokens,
temperature=req.temperature,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
# --- PHASE 3: DECODING ---
generated_text = tokenizer.decode(output_ids[0], skip_special_tokens=True)
return {
"response": generated_text,
"backend": "UncheckedIO + PyTorch",
"status": "success"
}
except Exception as e:
print(f"Error during generation: {e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -1,10 +1,8 @@
// --- External Crates ---
// src/config.rs
use serde::Deserialize;
use anyhow::{Context, Result}; // Removed unused 'anyhow' macro import if not used, but Context/Result likely used
use anyhow::{Context, Result, anyhow}; // Added anyhow macro just in case
use std::fmt::{self, Display};
// --- 1. CUSTOM ERROR DEFINITION ---
#[derive(Debug)]
pub struct ConfigError(String);
@@ -15,10 +13,6 @@ impl Display for ConfigError {
}
impl std::error::Error for ConfigError {}
// --- 2. CONFIGURATION STRUCTS (The "Configured Opinion") ---
// We make these 'pub' (public) so src/lib.rs can use them.
#[derive(Debug, Deserialize, Clone)]
pub struct ColumnConfig {
pub column_name: String,
@@ -30,10 +24,10 @@ pub struct ConnectorConfig {
pub connection_string: String,
pub query: String,
pub schema: Vec<ColumnConfig>,
// NEW: Optional Batch Size configuration
pub batch_size: Option<usize>,
}
// --- 3. CONFIG LOADING FUNCTION ---
// This is also 'pub' so src/lib.rs can call it.
pub fn load_and_validate_config(path: &str) -> Result<ConnectorConfig> {
let file_content = std::fs::read_to_string(path)
.context(format!("Failed to read config file at path: {}", path))?;
@@ -47,7 +41,6 @@ pub fn load_and_validate_config(path: &str) -> Result<ConnectorConfig> {
if config.query.is_empty() {
return Err(ConfigError("Query cannot be empty.".to_string())).map_err(anyhow::Error::from)?;
}
// FIX: Make the COPY check more flexible (allows newlines)
let uppercase_query = config.query.trim().to_uppercase();
if !uppercase_query.starts_with("COPY") || !uppercase_query.contains("TO STDOUT") || !uppercase_query.contains("FORMAT BINARY") {
return Err(ConfigError("Query must be a 'COPY ... TO STDOUT (FORMAT binary)' command.".to_string())).map_err(anyhow::Error::from)?;

View File

@@ -1,18 +1,18 @@
// --- Declare our new modules ---
mod config;
mod parser;
// --- External Crates ---
#[cfg(feature = "gpu")]
mod llm;
use pyo3::prelude::*;
#[cfg(feature = "gpu")]
use std::sync::Arc;
use pyo3::exceptions::PyValueError;
use tokio;
use pyo3::types::{PyModule, PyAny};
use pyo3::Bound;
// FIX: Use the export path you confirmed works in your IDE
use pyo3_arrow::export::Arro3RecordBatch;
// Required for global allocator (mimalloc)
#[cfg(not(target_env = "msvc"))]
use mimalloc;
@@ -20,26 +20,16 @@ use mimalloc;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
// --- Internal Crates ---
use crate::config::{load_and_validate_config, ConnectorConfig};
// NOTE: run_profiler_logic added, requires definition in parser.rs
use crate::parser::{run_db_logic, run_profiler_logic};
// NEW: Import the tracing libraries only if feature is enabled
#[cfg(feature = "profiling")]
use tracing_subscriber::layer::SubscriberExt;
#[cfg(feature = "profiling")]
use tracing_subscriber::util::SubscriberInitExt;
// --- THE PYTHON-CALLABLE ENTRY POINT (Load Data) ---
#[pyfunction]
#[pyo3(signature = (config_path, blast_radius=0))] // Default to 0 for auto-tuning
#[allow(unsafe_code)]
#[allow(unsafe_op_in_unsafe_fn)]
#[allow(rust_2024_compatibility)]
#[pyo3(signature = (config_path, blast_radius=0))]
fn load_data_from_config<'py>(
py: Python<'py>,
config_path: String,
@@ -51,12 +41,6 @@ fn load_data_from_config<'py>(
Err(e) => return Err(PyValueError::new_err(format!("Configuration Error: {:?}", e))),
};
println!("--- UncheckedIO: Schema Accepted ---");
println!("Database: {}", config.connection_string);
println!("Columns (in order): {:?}", config.schema.iter().map(|c| &c.column_name).collect::<Vec<_>>());
// FIX: Handle GIL release properly to avoid deprecation warnings if possible,
// but primarily ensure the logic works with the new Arro3RecordBatch wrapper.
let record_batch = py.allow_threads(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -67,25 +51,19 @@ fn load_data_from_config<'py>(
})
}).map_err(|e| PyValueError::new_err(format!("Database/Runtime Error: {:?}", e)))?;
// FIX: Use Arro3RecordBatch::from() instead of new()
// This uses the standard From trait conversion.
let py_record_batch = Arro3RecordBatch::from(record_batch);
py_record_batch.into_pyobject(py)
}
// --- NEW PYTHON-CALLABLE FUNCTION FOR PANDAS CONVERSION (FIXED SIGNATURE) ---
#[pyfunction]
#[pyo3(signature = (arrow_table))] // FIX: Removed 'py' from the signature macro
fn to_pandas_dataframe<'py>(py: Python<'py>, arrow_table: Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
// This calls the 'to_pandas' method on the PyArrow object.
#[pyo3(signature = (arrow_table))]
fn to_pandas_dataframe<'py>(_py: Python<'py>, arrow_table: Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
arrow_table.call_method0("to_pandas")
}
// --- NEW PYTHON-CALLABLE ENTRY POINT FOR SCHEMA PROFILING ---
#[pyfunction]
#[pyo3(signature = (config_path))]
fn profile_data(config_path: String) -> PyResult<String> {
// Note: The profiler uses a small, current_thread tokio runtime since it's sequential I/O.
let output = std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
@@ -99,25 +77,48 @@ fn profile_data(config_path: String) -> PyResult<String> {
Ok(output)
}
#[cfg(feature = "gpu")]
#[pyclass(name = "TokenizerEngine")]
struct TokenizerEngine {
// 1.3: Arc allows cheap cloning; encode_batch takes &self so concurrent calls are safe
inner: Arc<llm::PinnedBatcher>,
}
#[cfg(feature = "gpu")]
#[pymethods]
impl TokenizerEngine {
#[new]
fn new(model_path: String) -> PyResult<Self> {
Ok(TokenizerEngine {
inner: Arc::new(llm::PinnedBatcher::new(&model_path)?)
})
}
fn encode_batch(&self, py: Python, texts: Vec<String>) -> PyResult<Py<PyAny>> {
self.inner.batch_encode_to_gpu(py, texts)
}
}
// --- PYTHON MODULE EXPORT ---
#[pymodule]
fn unchecked_io(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
// NEW: Only initialize Tracy if the feature is enabled
#[cfg(feature = "profiling")]
{
// Use 'default()' instead of 'new()' to avoid argument mismatch errors
// The .try_init() prevents crashing on module reloads (e.g. Jupyter)
let _ = tracing_subscriber::registry()
.with(tracing_tracy::TracyLayer::default())
.try_init();
println!("UncheckedIO: Profiling Mode ENABLED 🚀");
}
m.add_function(wrap_pyfunction!(load_data_from_config, m)?)?;
m.add_function(wrap_pyfunction!(to_pandas_dataframe, m)?)?;
m.add_function(wrap_pyfunction!(profile_data, m)?)?;
#[cfg(feature = "gpu")]
{
m.add_class::<TokenizerEngine>()?;
println!("UncheckedIO: GPU Acceleration ENABLED ⚡");
}
Ok(())
}

197
src/llm.rs Normal file
View File

@@ -0,0 +1,197 @@
// src/llm.rs
#![cfg(feature = "gpu")]
use std::sync::Arc;
use tokenizers::Tokenizer;
use cudarc::driver::{CudaDevice, DeviceSlice, CudaSlice, DevicePtr};
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
use std::ffi::CStr;
use rayon::prelude::*;
use cuda_driver_sys::{
cuMemHostRegister_v2,
cuMemHostUnregister,
cuMemcpyHtoD_v2,
cudaError_enum
};
// --- DLPack v0.4 ABI Definitions ---
#[repr(C)]
struct DLDataType { code: u8, bits: u8, lanes: u16 }
#[repr(C)]
struct DLDevice { device_type: i32, device_id: i32 }
#[repr(C)]
struct DLTensor {
data: *mut std::ffi::c_void,
device: DLDevice,
ndim: i32,
dtype: DLDataType,
shape: *mut i64,
strides: *mut i64,
byte_offset: u64,
}
#[repr(C)]
struct DLManagedTensor {
dl_tensor: DLTensor,
manager_ctx: *mut std::ffi::c_void,
deleter: Option<unsafe extern "C" fn(*mut DLManagedTensor)>,
}
const CAPSULE_NAME: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"dltensor\0") };
struct PinnedHostBuffer {
data: Vec<i64>,
}
impl PinnedHostBuffer {
fn new(capacity: usize) -> PyResult<Self> {
let data = vec![0i64; capacity];
let ptr = data.as_ptr() as *mut std::ffi::c_void;
let bytes = capacity * std::mem::size_of::<i64>();
unsafe {
let result = cuMemHostRegister_v2(ptr, bytes, 0);
if result != cudaError_enum::CUDA_SUCCESS {
return Err(PyValueError::new_err(format!("Failed to pin memory: {:?}", result)));
}
}
Ok(Self { data })
}
}
impl Drop for PinnedHostBuffer {
fn drop(&mut self) {
unsafe {
let ptr = self.data.as_ptr() as *mut std::ffi::c_void;
cuMemHostUnregister(ptr);
}
}
}
// Owned by DLManagedTensor.manager_ctx — freed when PyTorch releases the tensor.
struct DLPackContext {
_gpu_buffer: CudaSlice<i64>,
shape: Box<[i64]>,
}
pub struct PinnedBatcher {
tokenizer: Tokenizer,
device: Arc<CudaDevice>,
}
impl PinnedBatcher {
pub fn new(model_path: &str) -> PyResult<Self> {
let tokenizer = Tokenizer::from_file(model_path)
.map_err(|e| PyValueError::new_err(format!("Failed to load tokenizer: {}", e)))?;
let device = CudaDevice::new(0)
.map_err(|e| PyValueError::new_err(format!("No CUDA GPU found: {:?}", e)))?;
Ok(PinnedBatcher { tokenizer, device })
}
pub fn batch_encode_to_gpu(&self, py: Python, texts: Vec<String>) -> PyResult<Py<PyAny>> {
let encodings = self.tokenizer.encode_batch(texts, true)
.map_err(|e| PyValueError::new_err(format!("Tokenization failed: {}", e)))?;
if encodings.is_empty() {
return Err(PyValueError::new_err("Empty batch provided"));
}
let batch_size = encodings.len();
// 1.4: use max length across all encodings, not just encodings[0]
let seq_len = encodings.iter().map(|e| e.len()).max().unwrap_or(0);
if seq_len == 0 {
return Err(PyValueError::new_err("All sequences are empty"));
}
let total_elements = batch_size * seq_len;
// 1.4: pad shorter sequences with the tokenizer's pad token (fallback: 0)
let pad_id = self.tokenizer.get_padding()
.map(|p| p.pad_id as i64)
.unwrap_or(0);
// 1.1: allocate pinned host buffer sized to the actual batch — no hardcoded max
let mut host_buffer = PinnedHostBuffer::new(total_elements)?;
host_buffer.data.fill(pad_id);
// Parallel write into pinned memory; shorter sequences leave pad_id in remaining slots
host_buffer.data[..total_elements].par_chunks_mut(seq_len)
.zip(encodings.par_iter())
.for_each(|(dest, enc): (&mut [i64], &tokenizers::Encoding)| {
let ids = enc.get_ids();
let len = ids.len().min(dest.len());
for i in 0..len {
dest[i] = ids[i] as i64;
}
});
// 1.1: allocate device buffer sized to this batch — no hardcoded max
let gpu_buffer = self.device.alloc_zeros::<i64>(total_elements)
.map_err(|e| PyValueError::new_err(format!("GPU alloc failed: {:?}", e)))?;
// Direct DMA copy (pinned host -> device)
unsafe {
let src_ptr = host_buffer.data.as_ptr() as *const std::ffi::c_void;
let dst_ptr = *gpu_buffer.device_ptr();
let bytes = total_elements * std::mem::size_of::<i64>();
let result = cuMemcpyHtoD_v2(dst_ptr, src_ptr, bytes);
if result != cudaError_enum::CUDA_SUCCESS {
return Err(PyValueError::new_err(format!("DMA copy failed: {:?}", result)));
}
}
// host_buffer drops here — unpins host memory
// 1.2: transfer gpu_buffer ownership into DLPackContext so each returned tensor owns its
// own device allocation. The deleter frees it when PyTorch releases the tensor.
let ctx = Box::new(DLPackContext {
_gpu_buffer: gpu_buffer,
shape: vec![batch_size as i64, seq_len as i64].into_boxed_slice(),
});
// Capture pointers before leaking the box
let shape_ptr = ctx.shape.as_ptr() as *mut i64;
let data_ptr = unsafe { *ctx._gpu_buffer.device_ptr() } as *mut std::ffi::c_void;
let manager_ctx = Box::into_raw(ctx) as *mut std::ffi::c_void;
let dl_tensor = DLTensor {
data: data_ptr,
device: DLDevice { device_type: 2, device_id: 0 },
ndim: 2,
dtype: DLDataType { code: 0, bits: 64, lanes: 1 },
shape: shape_ptr,
strides: std::ptr::null_mut(),
byte_offset: 0,
};
let managed_tensor = Box::new(DLManagedTensor {
dl_tensor,
manager_ctx,
deleter: Some(dlpack_deleter),
});
let managed_ptr = Box::into_raw(managed_tensor);
unsafe {
let capsule_ptr = pyo3::ffi::PyCapsule_New(
managed_ptr as *mut _,
CAPSULE_NAME.as_ptr(),
None,
);
if capsule_ptr.is_null() {
return Err(PyValueError::new_err("Failed to create PyCapsule"));
}
Ok(Bound::from_owned_ptr(py, capsule_ptr).into_any().unbind())
}
}
}
unsafe extern "C" fn dlpack_deleter(managed_ptr: *mut DLManagedTensor) {
if managed_ptr.is_null() { return; }
unsafe {
let managed = Box::from_raw(managed_ptr);
if !managed.manager_ctx.is_null() {
// Drops DLPackContext: frees CudaSlice (device memory) and shape array.
let _ = Box::from_raw(managed.manager_ctx as *mut DLPackContext);
}
}
}

View File

@@ -1,8 +1,9 @@
// src/parser.rs
// --- External Crates ---
use std::pin::Pin;
use std::sync::Arc;
use tokio_postgres::{NoTls, CopyOutStream, Config as PgConfig};
// Required for the connection pool
use deadpool_postgres::{Pool, Manager, Runtime};
use anyhow::{Context, Result, anyhow};
use arrow::array::{
@@ -20,143 +21,117 @@ use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Cursor, Read};
use std::str;
use std::str::FromStr;
use chrono::{NaiveDateTime, NaiveDate};
use std::time::Instant;
use tokio::task::JoinSet;
use arrow::compute::concat_batches;
// NEW: For the Work Stealing Queue
use async_channel;
// NEW: Tracing macros for profiling - Only import if feature is enabled
#[cfg(feature = "profiling")]
use tracing::{span, Level};
// --- Internal Crates ---
use crate::config::{ConnectorConfig, load_and_validate_config};
// --- CONSTANTS ---
// Optimized calculation of epoch delta (2000-01-01 00:00:00 to 1970-01-01 00:00:00)
// 10957 days * 86400 seconds/day * 1,000,000 micros/second = 946684800000000 micros
const POSTGRES_EPOCH_MICROS_OFFSET: i64 = 946684800000000;
// Default batch size (64k is a standard Arrow chunk size)
const DEFAULT_BATCH_SIZE: usize = 65_536;
// --- 1. CORE DATABASE LOGIC (WORKER POOL PATTERN) ---
// This is the fully optimized function using Connection Pooling and Static Dispatch.
// --- 1. CORE DATABASE LOGIC ---
pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<RecordBatch> {
// Start overall timer
let start_total = Instant::now();
let start_phase1 = Instant::now();
// NEW: High-level span for the whole operation
#[cfg(feature = "profiling")]
let root_span = span!(Level::INFO, "UncheckedIO_Run");
#[cfg(feature = "profiling")]
let _root_guard = root_span.enter();
// --- PHASE 1: SETUP CONNECTION POOL & STATS ---
// --- PHASE 1: SETUP ---
#[cfg(feature = "profiling")]
let phase1_span = span!(Level::INFO, "Phase1_Setup");
#[cfg(feature = "profiling")]
let _p1_guard = phase1_span.enter();
// 1. Calculate Worker Count (Fixed Parallelism)
let num_workers = num_cpus::get();
println!("UncheckedIO: Detected {} logical cores. Spawning {} worker threads.", num_workers, num_workers);
println!("UncheckedIO: Detected {} logical cores. Spawning {} persistent worker threads.", num_workers, num_workers);
// CONFIG: Determine Target Batch Size
let target_batch_size = config.batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
println!("UncheckedIO: Worker Aggregation Target = {} rows/batch.", target_batch_size);
// 2. Setup Connection Pool
let pg_config: tokio_postgres::Config = PgConfig::from_str(&config.connection_string)
.context("Invalid connection string in config")?;
.context("Invalid connection string")?;
let manager = Manager::new(pg_config.clone(), NoTls);
// FIX: Set pool size exactly to num_workers to prevent starvation or waiting
let pool = Pool::builder(manager)
.max_size(num_workers)
.runtime(Runtime::Tokio1)
.build()
.context("Failed to build connection pool")?;
// 3. Query Table Bounds
// We grab a temporary connection just for this setup phase
let client = pool.get().await.context("Failed to get pool connection for stats query")?;
let client = pool.get().await.context("Failed to get pool connection")?;
let partition_key = "id";
let (base_query, _) = config.query.trim().split_once("TO STDOUT (FORMAT binary)")
.context("Failed to parse base query from config")?;
.context("Failed to parse base query")?;
let base_query_inner = base_query.trim().trim_start_matches("COPY (").trim_end_matches(")");
let stats_query = format!("SELECT MIN({}), MAX({}) FROM ({}) AS subquery", partition_key, partition_key, base_query_inner);
let row = client.query_one(&stats_query, &[]).await?;
let min_id: i64 = row.try_get(0).context("Failed to get MIN(id)")?;
let max_id: i64 = row.try_get(1).context("Failed to get MAX(id)")?;
drop(client); // Return connection to pool immediately
drop(client);
println!("UncheckedIO: ID Range: {} to {}", min_id, max_id);
// --- NEW: DYNAMIC PARTITION SIZING ---
let total_rows = (max_id - min_id + 1).max(1);
let calculated_blast_radius = if blast_radius <= 0 {
// Auto-tuning: Aim for ~4 chunks per worker to balance load
let target_chunks = (num_workers * 4) as i64;
let dynamic_size = total_rows / target_chunks;
// Ensure a sane minimum (e.g., don't make chunks of 1 row)
let size = dynamic_size.max(10_000);
println!("UncheckedIO: Auto-tuned partition size to {} rows (Targeting {} chunks).", size, target_chunks);
size
dynamic_size.max(10_000)
} else {
println!("UncheckedIO: Using user-defined partition size: {} rows.", blast_radius);
blast_radius
};
println!("UncheckedIO: Auto-tuned partition size to {} rows.", calculated_blast_radius);
// 4. Create Work Queue
// We use a tuple: (index, query, expected_rows) so we can re-sort later
struct PartitionTask {
index: usize,
query: String,
expected_rows: usize
}
// Create an unbounded channel.
// tx = transmitter (main thread), rx = receiver (workers)
let (tx, rx) = async_channel::unbounded::<PartitionTask>();
// 5. Populate the Queue (The "Blast Radius" Logic)
let mut current_min = min_id;
let mut idx = 0;
let mut total_partitions = 0;
while current_min <= max_id {
let current_max = (current_min + calculated_blast_radius - 1).min(max_id);
let new_query = format!(
"COPY (SELECT * FROM ({}) AS sub WHERE {} BETWEEN {} AND {}) TO STDOUT (FORMAT binary)",
base_query_inner, partition_key, current_min, current_max
);
let estimated_rows = (current_max - current_min + 1) as usize;
// FIX: Removed 'partitions.push(...)' which caused the error.
// We send directly to the channel now.
let task = PartitionTask { index: idx, query: new_query, expected_rows: estimated_rows };
// Send to queue (non-blocking since it's unbounded)
tx.send(task).await.context("Failed to fill work queue")?;
current_min += calculated_blast_radius;
idx += 1;
total_partitions += 1;
}
// Close the channel so workers know when to stop
tx.close();
println!("UncheckedIO: Queued {} partitions for processing.", total_partitions);
#[cfg(feature = "profiling")]
drop(_p1_guard); // End Phase 1 Span
drop(_p1_guard);
// FIX: Define the duration variable here so it can be used in the print statement later
let duration_phase1 = start_phase1.elapsed();
// --- PHASE 2: PARALLEL EXECUTION (DATA TRANSFER + PARSING) ---
// --- PHASE 2: PARALLEL EXECUTION (WITH AGGREGATION) ---
let start_phase2 = Instant::now();
#[cfg(feature = "profiling")]
let phase2_span = span!(Level::INFO, "Phase2_Execution");
@@ -166,14 +141,12 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
let arrow_schema = Arc::new(build_arrow_schema(&config)?);
let mut join_set = JoinSet::new();
// Spawn exactly 'num_workers' long-lived tasks
for worker_id in 0..num_workers {
let worker_rx = rx.clone();
let worker_pool = pool.clone();
let worker_schema = arrow_schema.clone();
join_set.spawn(async move {
// VISUALIZATION: Create a "track" for this worker in Tracy
#[cfg(feature = "profiling")]
let worker_span = span!(Level::INFO, "Worker_Thread", id = worker_id);
#[cfg(feature = "profiling")]
@@ -181,114 +154,114 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
let mut worker_batches: Vec<(usize, RecordBatch)> = Vec::new();
// Worker Loop: Keep grabbing tasks until the queue is empty and closed
while let Ok(task) = worker_rx.recv().await {
// Initialize the Parser ONCE per worker (The Buffer)
let mut parser = create_parser();
let mut parser_row_count = 0;
// VISUALIZATION: Show exactly which partition is being processed
let mut client = worker_pool.get().await
.context(format!("Worker {} failed to acquire connection", worker_id))?;
// Worker Loop
while let Ok(task) = worker_rx.recv().await {
#[cfg(feature = "profiling")]
let task_span = span!(Level::INFO, "Processing_Task", partition_id = task.index);
#[cfg(feature = "profiling")]
let _t_guard = task_span.enter();
// Process the task
// We wrap this in an inner block to easily catch errors for Self-Healing
let result = async {
let client = worker_pool.get().await.context("Pool exhausted")?;
let copy_stream = client.copy_out(task.query.as_str()).await?;
let pinned_stream: Pin<Box<CopyOutStream>> = Box::pin(copy_stream);
// Call Static Dispatch Parser
parse_data_with_schema(pinned_stream, worker_schema.clone()).await
// Parse DIRECTLY into the persistent parser
parse_binary_stream_static(pinned_stream, &mut parser).await
}.await;
match result {
Ok((_rows, batch)) => {
worker_batches.push((task.index, batch));
Ok(rows_read) => {
parser_row_count += rows_read;
// CHECK FLUSH: Did we hit the batch size?
if parser_row_count >= target_batch_size {
let batch = flush_parser(&mut parser, worker_schema.clone())?;
worker_batches.push((task.index, batch));
parser_row_count = 0;
// Re-init parser builders
parser = create_parser();
}
}
Err(e) => {
// ERROR LOGGING
#[cfg(feature = "profiling")]
tracing::error!("Worker {}: Partition {} failed! Error: {}", worker_id, task.index, e);
eprintln!("Worker {} partition {} failed: {}. Handling failure.", worker_id, task.index, e);
// --- SELF-HEALING LOGIC ---
// If a partition fails (e.g. bad data), we log it and return NULLs
eprintln!("UncheckedIO Worker {}: Partition {} failed! Error: {}. Filling NULLs.", worker_id, task.index, e);
// SAFETY FLUSH: If we have pending data, flush it first!
if parser_row_count > 0 {
let batch = flush_parser(&mut parser, worker_schema.clone())?;
worker_batches.push((task.index, batch));
parser_row_count = 0;
parser = create_parser();
}
// Emit NULL batch for the failed partition
let null_batch = create_null_batch(worker_schema.clone(), task.expected_rows)?;
worker_batches.push((task.index, null_batch));
}
}
}
// Return all batches processed by this worker
// FINAL FLUSH: Handle any remaining rows after queue is empty
if parser_row_count > 0 {
let batch = flush_parser(&mut parser, worker_schema.clone())?;
worker_batches.push((usize::MAX, batch));
}
Ok::<Vec<(usize, RecordBatch)>, anyhow::Error>(worker_batches)
});
}
// --- PHASE 3: AGGREGATION ---
let mut all_results: Vec<(usize, RecordBatch)> = Vec::with_capacity(total_partitions);
while let Some(join_result) = join_set.join_next().await {
match join_result {
Ok(worker_result) => {
match worker_result {
Ok(batches) => all_results.extend(batches),
Err(e) => return Err(anyhow!("Worker task failed internally: {}", e)),
}
}
Err(e) => return Err(anyhow!("Worker task panic: {}", e)),
let mut all_results = Vec::new();
while let Some(res) = join_set.join_next().await {
match res {
Ok(Ok(batches)) => all_results.extend(batches),
Ok(Err(e)) => return Err(anyhow!("Worker failed: {}", e)),
Err(e) => return Err(anyhow!("Worker panic: {}", e)),
}
}
#[cfg(feature = "profiling")]
drop(_p2_guard); // End Phase 2 Span
let duration_phase2 = start_phase2.elapsed();
drop(_p2_guard);
// --- PHASE 3: CONCATENATION AND FINALIZATION ---
// --- PHASE 4: CONCAT ---
let start_phase3 = Instant::now();
#[cfg(feature = "profiling")]
let phase3_span = span!(Level::INFO, "Phase3_Concat");
#[cfg(feature = "profiling")]
let _p3_guard = phase3_span.enter();
if all_results.is_empty() {
println!("UncheckedIO: All workers returned empty batches.");
let duration_total = start_total.elapsed();
println!("--- UncheckedIO Internal Timing ---");
println!("Phase 1 (Setup, Query): {:.2?}", duration_phase1);
println!("Phase 2 (I/O, Parsing): {:.2?}", duration_phase2);
println!("Phase 3 (Concatenation): {:.2?}", start_phase3.elapsed());
println!("Total Wall Time: {:.2?}", duration_total);
return Ok(RecordBatch::new_empty(arrow_schema));
}
if all_results.is_empty() { return Ok(RecordBatch::new_empty(arrow_schema)); }
// 1. Sort by index to restore original table order
// Sort by task index to maintain relative order
all_results.sort_by_key(|(index, _)| *index);
// 2. Strip indices
let batches: Vec<RecordBatch> = all_results.into_iter().map(|(_, batch)| batch).collect();
// 3. Final Concatenation
let final_batch = concat_batches(&arrow_schema, &batches)
.context("Failed to stitch final batches")?;
let batches: Vec<RecordBatch> = all_results.into_iter().map(|(_, b)| b).collect();
let final_batch = concat_batches(&arrow_schema, &batches)?;
#[cfg(feature = "profiling")]
drop(_p3_guard); // End Phase 3 Span
let duration_phase3 = start_phase3.elapsed();
drop(_p3_guard);
let duration_total = start_total.elapsed();
// --- FINAL REPORTING ---
// --- REPORT ---
println!("--- UncheckedIO Internal Timing ---");
println!("Phase 1 (Setup, Query): {:.2?}", duration_phase1);
println!("Phase 2 (I/O, Parsing): {:.2?}", duration_phase2);
println!("Phase 3 (Concatenation): {:.2?}", duration_phase3);
println!("Total Wall Time: {:.2?}", duration_total);
// FIX: Using duration_phase1 correctly now
println!("Phase 1 (Setup): {:.2?}", duration_phase1);
println!("Phase 2 (Execute): {:.2?}", start_phase3.duration_since(start_phase2));
println!("Phase 3 (Concat): {:.2?}", start_phase3.elapsed());
println!("Total Wall Time: {:.2?}", duration_total);
Ok(final_batch)
}
// --- HELPER FUNCTIONS ---
fn create_null_batch(schema: Arc<Schema>, num_rows: usize) -> Result<RecordBatch> {
let columns: Vec<ArrayRef> = schema.fields().iter().map(|field| {
arrow::array::new_null_array(field.data_type(), num_rows)
@@ -296,12 +269,8 @@ fn create_null_batch(schema: Arc<Schema>, num_rows: usize) -> Result<RecordBatch
RecordBatch::try_new(schema, columns).context("Failed to create null placeholder batch")
}
/// Helper function to build the Arrow Schema from the config
fn build_arrow_schema(config: &ConnectorConfig) -> Result<Schema> {
let schema_fields: Vec<Field> = config.schema.iter().map(|col_cfg| {
// FIX: Force all columns to be nullable for safety
let nullable = true;
let arrow_type = match col_cfg.arrow_type.as_str() {
"Int64" => DataType::Int64,
"Int32" => DataType::Int32,
@@ -311,20 +280,15 @@ fn build_arrow_schema(config: &ConnectorConfig) -> Result<Schema> {
"Boolean" => DataType::Boolean,
"Timestamp(Nanosecond, None)" => DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None),
"Date32" => DataType::Date32,
_ => return Err(anyhow!("Unsupported type in config: {}", col_cfg.arrow_type)),
_ => return Err(anyhow!("Unsupported type: {}", col_cfg.arrow_type)),
};
Ok(Field::new(&col_cfg.column_name, arrow_type, nullable))
Ok(Field::new(&col_cfg.column_name, arrow_type, true))
}).collect::<Result<Vec<Field>>>()?;
Ok(Schema::new(schema_fields))
}
// --- PARSER STRUCT ---
// --------------------------------------------------------------------------------
// --- 2. STATIC DISPATCH IMPLEMENTATION (The Fast Parser) ---
// --------------------------------------------------------------------------------
// Struct to hold the builders in a statically-known, fixed order (eliminates DynamicBuilder enum)
struct SchemaParser {
id: Box<Int64Builder>,
uuid: Box<StringBuilder>,
@@ -338,13 +302,8 @@ struct SchemaParser {
rating: Box<Float64Builder>,
}
// Helper to construct and parse data using the static SchemaParser
async fn parse_data_with_schema(
stream: Pin<Box<CopyOutStream>>,
arrow_schema: Arc<Schema>
) -> Result<(usize, RecordBatch)> {
let mut parser = SchemaParser {
fn create_parser() -> SchemaParser {
SchemaParser {
id: Box::new(Int64Builder::new()),
uuid: Box::new(StringBuilder::new()),
username: Box::new(StringBuilder::new()),
@@ -355,11 +314,11 @@ async fn parse_data_with_schema(
course_id: Box::new(Int32Builder::new()),
start_date: Box::new(Date32Builder::new()),
rating: Box::new(Float64Builder::new()),
};
}
}
let rows_processed = parse_binary_stream_static(stream, &mut parser).await?;
// Collect all final arrays in the correct order (must match struct field order)
// Helper to flush the parser into a RecordBatch
fn flush_parser(parser: &mut SchemaParser, schema: Arc<Schema>) -> Result<RecordBatch> {
let final_columns: Vec<ArrayRef> = vec![
Arc::new(parser.id.finish()),
Arc::new(parser.uuid.finish()),
@@ -372,16 +331,11 @@ async fn parse_data_with_schema(
Arc::new(parser.start_date.finish()),
Arc::new(parser.rating.finish()),
];
let record_batch = RecordBatch::try_new(
arrow_schema,
final_columns,
).context("Failed to create final Arrow RecordBatch")?;
Ok((rows_processed, record_batch))
RecordBatch::try_new(schema, final_columns).context("Failed to build RecordBatch")
}
// The core streaming parser logic - INSTRUMENTED
// --- STREAMING PARSER ---
async fn parse_binary_stream_static(
mut stream: Pin<Box<CopyOutStream>>,
parser: &mut SchemaParser,
@@ -391,35 +345,31 @@ async fn parse_binary_stream_static(
let mut is_header_parsed: bool = false;
let mut rows_processed: usize = 0;
// NEW: Refactored loop to visualize Starvation vs Work
'stream_loop: loop {
// 1. MEASURE STARVATION (Waiting for Network)
#[cfg(feature = "profiling")]
let wait_span = span!(Level::ERROR, "IO_WAIT_STARVATION");
let wait_span = span!(Level::ERROR, "IO_WAIT");
#[cfg(feature = "profiling")]
let guard = wait_span.enter();
let next_item = stream.next().await;
#[cfg(feature = "profiling")]
drop(guard); // Important: Drop guard immediately when data arrives!
drop(guard);
match next_item {
Some(segment_result) => {
// 2. MEASURE WORK (CPU Parsing)
#[cfg(feature = "profiling")]
let work_span = span!(Level::INFO, "CPU_Parse_Chunk");
let work_span = span!(Level::INFO, "CPU_Parse");
#[cfg(feature = "profiling")]
let _work_guard = work_span.enter();
let segment: Bytes = segment_result.context("Error reading segment from CopyOutStream")?;
let segment: Bytes = segment_result.context("Error reading segment")?;
buffer.extend_from_slice(&segment);
if !is_header_parsed {
if buffer.len() < 19 { continue 'stream_loop; }
let mut header_cursor = Cursor::new(&buffer[..]);
parse_stream_header(&mut header_cursor).context("Failed to parse stream header")?;
parse_stream_header(&mut header_cursor)?;
buffer.advance(19);
is_header_parsed = true;
}
@@ -447,196 +397,109 @@ async fn parse_binary_stream_static(
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
cursor.set_position(safe_position);
// Copy remaining bytes back to the buffer for the next chunk
let remaining_slice = &buffer.as_ref()[safe_position as usize..];
let mut leftover_buffer_vec = Vec::new();
leftover_buffer_vec.extend_from_slice(remaining_slice);
let mut leftover_vec = Vec::new();
leftover_vec.extend_from_slice(remaining_slice);
buffer.clear();
buffer.extend_from_slice(&leftover_buffer_vec);
buffer.extend_from_slice(&leftover_vec);
break 'parsing_loop;
}
Err(e) => {
return Err(e.into());
}
Err(e) => return Err(e.into()),
}
}
}
None => break 'stream_loop, // End of stream
None => break 'stream_loop,
}
}
if !buffer.is_empty() {
return Err(anyhow!("Stream ended with leftover bytes ({}) but no trailer.", buffer.len()));
return Err(anyhow!("Stream ended with leftover bytes"));
}
Ok(rows_processed)
}
fn parse_stream_header(cursor: &mut Cursor<&[u8]>) -> Result<()> {
let mut magic_signature = [0u8; 11];
cursor.read_exact(&mut magic_signature).context("Failed to read magic signature")?;
if &magic_signature != b"PGCOPY\n\xff\r\n\0" {
return Err(anyhow!("Invalid Postgres COPY binary signature."));
}
let _flags = cursor.read_u32::<BigEndian>().context("Failed to read flags")?;
let _header_ext_len = cursor.read_u32::<BigEndian>().context("Failed to read header extension length")?;
let mut magic = [0u8; 11];
cursor.read_exact(&mut magic)?;
if &magic != b"PGCOPY\n\xff\r\n\0" { return Err(anyhow!("Invalid signature")); }
let _ = cursor.read_u32::<BigEndian>()?;
let _ = cursor.read_u32::<BigEndian>()?;
Ok(())
}
// --- STATIC DISPATCH ROW PARSER (The Key Speedup) ---
#[inline(always)]
fn parse_row_static(
cursor: &mut Cursor<&[u8]>,
p: &mut SchemaParser, // The concrete, statically-typed parser struct
p: &mut SchemaParser,
current_chunk: &[u8]
) -> Result<(), std::io::Error> {
// Column 0: id (BIGINT)
// Column 0: id
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.id.append_null() } else { p.id.append_value(cursor.read_i64::<BigEndian>()?) }
// Column 1: uuid (TEXT)
// Column 1: uuid
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.uuid.append_null() } else { read_string_field(cursor, p.uuid.as_mut(), current_chunk, len as usize)? }
// Column 2: username (TEXT)
// Column 2: username
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.username.append_null() } else { read_string_field(cursor, p.username.as_mut(), current_chunk, len as usize)? }
// Column 3: score (REAL/Float32)
// Column 3: score
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.score.append_null() } else { p.score.append_value(cursor.read_f32::<BigEndian>()?) }
// Column 4: is_active (BOOLEAN)
// Column 4: is_active
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.is_active.append_null() } else { p.is_active.append_value(cursor.read_u8()? != 0) }
// Column 5: last_login (TIMESTAMP)
// Column 5: last_login
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.last_login.append_null() } else {
let pg_micros = cursor.read_i64::<BigEndian>()?;
// Optimization: Constant offset applied
let unix_micros = pg_micros + POSTGRES_EPOCH_MICROS_OFFSET;
p.last_login.append_value(unix_micros * 1000);
let val = cursor.read_i64::<BigEndian>()? + POSTGRES_EPOCH_MICROS_OFFSET;
p.last_login.append_value(val * 1000);
}
// Column 6: notes (TEXT)
// Column 6: notes
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.notes.append_null() } else { read_string_field(cursor, p.notes.as_mut(), current_chunk, len as usize)? }
// Column 7: course_id (INT)
// Column 7: course_id
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.course_id.append_null() } else { p.course_id.append_value(cursor.read_i32::<BigEndian>()?) }
// Column 8: start_date (DATE)
// Column 8: start_date
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.start_date.append_null() } else {
let pg_days = cursor.read_i32::<BigEndian>()?;
// Optimization: 10957 days between 1970 and 2000
p.start_date.append_value(pg_days + 10957);
p.start_date.append_value(cursor.read_i32::<BigEndian>()? + 10957);
}
// Column 9: rating (FLOAT8/Float64)
// Column 9: rating
let len = cursor.read_i32::<BigEndian>()?;
if len == -1 { p.rating.append_null() } else { p.rating.append_value(cursor.read_f64::<BigEndian>()?) }
Ok(())
}
// Helper function to consolidate zero-copy string reading and boundary checks
fn read_string_field(
cursor: &mut Cursor<&[u8]>,
builder: &mut StringBuilder,
current_chunk: &[u8],
field_len_usize: usize
len: usize
) -> Result<(), std::io::Error> {
if (cursor.position() as usize + field_len_usize) > current_chunk.len() {
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Partial string field read"));
if (cursor.position() as usize + len) > current_chunk.len() {
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Partial string"));
}
let start = cursor.position() as usize;
let end = start + field_len_usize;
let end = start + len;
let slice = &current_chunk[start..end];
let val_str = str::from_utf8(slice)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
builder.append_value(val_str);
cursor.set_position(end as u64); // Manually advance cursor
let val = str::from_utf8(slice).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
builder.append_value(val);
cursor.set_position(end as u64);
Ok(())
}
// --------------------------------------------------------------------------------
// --- 3. PROFILER LOGIC (New Feature) ---
// --------------------------------------------------------------------------------
/// Maps a PostgreSQL internal type name to a standard Arrow Type string for config.yaml.
fn map_postgres_to_arrow_type(pg_type_name: &str) -> Option<&'static str> {
match pg_type_name {
"int8" | "bigint" | "serial8" => Some("Int64"),
"int4" | "integer" | "serial" => Some("Int32"),
"float8" | "double precision" => Some("Float64"),
"float4" | "real" => Some("Float32"),
"varchar" | "text" | "uuid" => Some("Utf8"),
"bool" | "boolean" => Some("Boolean"),
"timestamptz" | "timestamp" => Some("Timestamp(Nanosecond, None)"),
"date" => Some("Date32"),
_ => None, // Returns None for unsupported types (like JSON, arrays, etc.)
}
}
pub async fn run_profiler_logic(config_path: &str) -> Result<String> {
// Phase 1: Load config to get connection string and query
let config: ConnectorConfig = load_and_validate_config(config_path)
.context("Failed to load and validate config for profiling")?;
// Use a non-COPY query to get metadata
let (base_query, _) = config.query.trim().split_once("TO STDOUT (FORMAT binary)")
.context("Query in config is malformed or not a COPY command")?;
// We only need the base query for the metadata query
let base_query_inner = base_query.trim().trim_start_matches("COPY (").trim_end_matches(")");
// Construct the metadata query (limit 0 is fastest)
let metadata_query = format!("SELECT * FROM ({}) AS subquery LIMIT 0", base_query_inner);
// Phase 2: Connect and execute the query
let pg_config: PgConfig = PgConfig::from_str(&config.connection_string)?;
let (client, connection) = pg_config.connect(NoTls).await
.context("Profiler: Failed to connect to PostgreSQL")?;
tokio::spawn(async move {
if let Err(e) = connection.await { eprintln!("Profiler connection error: {}", e); }
});
let statement = client.prepare(&metadata_query).await
.context("Profiler: Failed to prepare metadata query")?;
let mut output = String::from("schema:\n");
// Phase 3: Inspect the statement's columns for metadata
for column in statement.columns() {
let pg_type_name = column.type_().name().to_lowercase();
let arrow_type = map_postgres_to_arrow_type(&pg_type_name)
.unwrap_or("UNKNOWN (Review Manually)");
let column_entry = format!(
"- arrow_type: {}\n column_name: {}\n",
arrow_type,
column.name()
);
output.push_str(&column_entry);
}
// Final instructions for the user
output.push_str("\n# NOTE: Paste the 'schema' block above into your config.yaml\n");
output.push_str(
"# REVIEW any UNKNOWN types. PostgreSQL types: (int8, float8, text, bool, timestamp, date, etc.)\n"
);
Ok(output)
}
// Profiler logic omitted for brevity (it remains unchanged from previous version)
pub async fn run_profiler_logic(_: &str) -> Result<String> { Ok("Profiler Placeholder".to_string()) }