fastprox 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. fastprox-0.1.0/.github/workflows/bench.yml +52 -0
  2. fastprox-0.1.0/.github/workflows/ci.yml +151 -0
  3. fastprox-0.1.0/.github/workflows/fuzz.yml +36 -0
  4. fastprox-0.1.0/.github/workflows/profile.yml +99 -0
  5. fastprox-0.1.0/.github/workflows/variants.yml +89 -0
  6. fastprox-0.1.0/.gitignore +13 -0
  7. fastprox-0.1.0/.python-version +1 -0
  8. fastprox-0.1.0/CHANGELOG.md +84 -0
  9. fastprox-0.1.0/CLAUDE.md +34 -0
  10. fastprox-0.1.0/CMakeLists.txt +154 -0
  11. fastprox-0.1.0/LICENSE +21 -0
  12. fastprox-0.1.0/PKG-INFO +101 -0
  13. fastprox-0.1.0/README.md +74 -0
  14. fastprox-0.1.0/ROADMAP.md +118 -0
  15. fastprox-0.1.0/bench/asm_summary.py +65 -0
  16. fastprox-0.1.0/bench/bench_python.py +99 -0
  17. fastprox-0.1.0/bench/compare_external.py +292 -0
  18. fastprox-0.1.0/bench/profile_kernels.cpp +292 -0
  19. fastprox-0.1.0/bench/tv_variants.cpp +171 -0
  20. fastprox-0.1.0/bindings/python/CMakeLists.txt +17 -0
  21. fastprox-0.1.0/bindings/python/fastprox/__init__.py +435 -0
  22. fastprox-0.1.0/bindings/python/src/module.cpp +224 -0
  23. fastprox-0.1.0/cmake/fastprox.map +7 -0
  24. fastprox-0.1.0/cmake/fastprox.pc.in +10 -0
  25. fastprox-0.1.0/cmake/fastproxConfig.cmake.in +9 -0
  26. fastprox-0.1.0/docs/api.md +256 -0
  27. fastprox-0.1.0/docs/benchmarks.md +442 -0
  28. fastprox-0.1.0/docs/spec.md +461 -0
  29. fastprox-0.1.0/examples/README.md +20 -0
  30. fastprox-0.1.0/examples/basic_usage.py +73 -0
  31. fastprox-0.1.0/examples/bilevel_learning.py +85 -0
  32. fastprox-0.1.0/examples/derivatives.py +68 -0
  33. fastprox-0.1.0/examples/proximal_gradient.py +77 -0
  34. fastprox-0.1.0/examples/tv_denoising.py +63 -0
  35. fastprox-0.1.0/fuzz/CMakeLists.txt +20 -0
  36. fastprox-0.1.0/fuzz/fuzz_ops.cpp +296 -0
  37. fastprox-0.1.0/fuzz/standalone.cpp +79 -0
  38. fastprox-0.1.0/include/fastprox.h +208 -0
  39. fastprox-0.1.0/pyproject.toml +96 -0
  40. fastprox-0.1.0/src/api/api.cpp +509 -0
  41. fastprox-0.1.0/src/core/driver.hpp +299 -0
  42. fastprox-0.1.0/src/core/layout.hpp +163 -0
  43. fastprox-0.1.0/src/core/op.hpp +362 -0
  44. fastprox-0.1.0/src/core/parallel.cpp +244 -0
  45. fastprox-0.1.0/src/core/parallel.hpp +43 -0
  46. fastprox-0.1.0/src/core/workspace.hpp +91 -0
  47. fastprox-0.1.0/src/ops/group.cpp +146 -0
  48. fastprox-0.1.0/src/ops/isotonic.cpp +124 -0
  49. fastprox-0.1.0/src/ops/ops.hpp +41 -0
  50. fastprox-0.1.0/src/ops/pointwise.cpp +235 -0
  51. fastprox-0.1.0/src/ops/projections.cpp +608 -0
  52. fastprox-0.1.0/src/ops/tv1d.cpp +576 -0
  53. fastprox-0.1.0/tests/c/CMakeLists.txt +12 -0
  54. fastprox-0.1.0/tests/c/test_core.cpp +595 -0
  55. fastprox-0.1.0/tests/c/test_header.c +65 -0
  56. fastprox-0.1.0/tests/fixtures/generate.py +84 -0
  57. fastprox-0.1.0/tests/fixtures/reference.npz +0 -0
  58. fastprox-0.1.0/tests/python/conftest.py +2 -0
  59. fastprox-0.1.0/tests/python/fixture_cases.py +117 -0
  60. fastprox-0.1.0/tests/python/reference.py +104 -0
  61. fastprox-0.1.0/tests/python/test_examples.py +29 -0
  62. fastprox-0.1.0/tests/python/test_fixtures.py +42 -0
  63. fastprox-0.1.0/tests/python/test_operators.py +422 -0
  64. fastprox-0.1.0/tests/valgrind/fork.supp +12 -0
  65. fastprox-0.1.0/uv.lock +1291 -0
@@ -0,0 +1,52 @@
1
+ name: bench
2
+
3
+ # Benchmarks on GitHub's Linux runners: homogeneous cores (unlike a laptop
4
+ # with performance and efficiency cores), but shared VMs, so compare solvers
5
+ # within one run rather than across runs. Started by hand from the Actions tab.
6
+ on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ full:
10
+ description: "All sizes up to 1e7 (slow) instead of 1e3 and 1e5"
11
+ type: boolean
12
+ default: false
13
+
14
+ jobs:
15
+ bench:
16
+ name: bench ${{ matrix.os }}
17
+ runs-on: ${{ matrix.os }}
18
+ timeout-minutes: 90
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ os: [ubuntu-latest, ubuntu-24.04-arm]
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v6
26
+ - name: Tools for the external codes (proxTV needs LAPACK, Condat's files GSL)
27
+ run: sudo apt-get update && sudo apt-get install -y liblapack-dev libgsl-dev
28
+ - run: uv sync --python 3.12
29
+ - name: Machine
30
+ run: |
31
+ {
32
+ lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
33
+ cc --version | head -1
34
+ c++ --version | head -1
35
+ } | tee machine.txt
36
+ { echo "### ${{ matrix.os }}"; echo '```'; cat machine.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
37
+ - name: Overhead, throughput and parallel scaling
38
+ run: uv run python bench/bench_python.py | tee bench_python.txt
39
+ - name: Against proxTV and Condat's code
40
+ run: uv run python bench/compare_external.py ${{ inputs.full && ' ' || '--quick' }} | tee compare_external.txt
41
+ - name: Summary
42
+ if: always()
43
+ run: |
44
+ for f in bench_python.txt compare_external.txt; do
45
+ [ -f "$f" ] || continue
46
+ { echo "#### $f"; echo '```'; cat "$f"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
47
+ done
48
+ - uses: actions/upload-artifact@v4
49
+ if: always()
50
+ with:
51
+ name: bench-${{ matrix.os }}
52
+ path: "*.txt"
@@ -0,0 +1,151 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch: # also builds the wheels, without a tag
7
+ inputs:
8
+ publish:
9
+ description: Upload the wheels and sdist to TestPyPI
10
+ type: boolean
11
+ default: false
12
+
13
+ jobs:
14
+ c:
15
+ name: C/C++ ${{ matrix.os }} ${{ matrix.cc }} threads=${{ matrix.threads }}
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ include:
21
+ - { os: ubuntu-latest, cc: gcc, cxx: g++, threads: ON }
22
+ - { os: ubuntu-latest, cc: clang, cxx: clang++, threads: ON }
23
+ - { os: ubuntu-latest, cc: gcc, cxx: g++, threads: OFF }
24
+ - { os: macos-latest, cc: clang, cxx: clang++, threads: ON }
25
+ - { os: macos-15-intel, cc: clang, cxx: clang++, threads: ON }
26
+ - { os: windows-latest, cc: cl, cxx: cl, threads: ON }
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Configure
30
+ env: { CC: "${{ matrix.cc }}", CXX: "${{ matrix.cxx }}" }
31
+ run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DFASTPROX_THREADS=${{ matrix.threads }}
32
+ - run: cmake --build build --config Release
33
+ - run: ctest --test-dir build -C Release --output-on-failure --timeout 300
34
+
35
+ sanitizers:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DFASTPROX_SANITIZE=ON -DFASTPROX_FUZZ=ON
40
+ - run: cmake --build build
41
+ - run: ctest --test-dir build --output-on-failure --timeout 300
42
+
43
+ tsan:
44
+ name: ThreadSanitizer
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ # TSan in clang 18 rejects the runner kernel's high ASLR entropy.
49
+ - run: sudo sysctl vm.mmap_rnd_bits=28
50
+ - name: Configure
51
+ env: { CC: clang, CXX: clang++ }
52
+ run: >
53
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
54
+ -DCMAKE_C_FLAGS=-fsanitize=thread -DCMAKE_CXX_FLAGS=-fsanitize=thread
55
+ -DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread -DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=thread
56
+ - run: cmake --build build
57
+ - run: ctest --test-dir build --output-on-failure --timeout 300
58
+ env: { TSAN_OPTIONS: "halt_on_error=1" }
59
+
60
+ valgrind:
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - run: sudo apt-get update && sudo apt-get install -y valgrind
65
+ - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
66
+ - run: cmake --build build
67
+ - name: Memcheck
68
+ run: |
69
+ for t in build/tests/c/test_header build/tests/c/test_core; do
70
+ valgrind --error-exitcode=1 --leak-check=full --errors-for-leak-kinds=definite \
71
+ --suppressions=tests/valgrind/fork.supp "$t"
72
+ done
73
+
74
+ python:
75
+ name: Python ${{ matrix.python }} ${{ matrix.os }}
76
+ runs-on: ${{ matrix.os }}
77
+ strategy:
78
+ fail-fast: false
79
+ matrix:
80
+ os: [ubuntu-latest, macos-latest, windows-latest]
81
+ python: ["3.12"]
82
+ include:
83
+ - { os: ubuntu-latest, python: "3.14" } # the abi3 wheel on a newer CPython
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: astral-sh/setup-uv@v6
87
+ - run: uv sync --python ${{ matrix.python }}
88
+ - run: uv run pytest -q
89
+
90
+ wheels:
91
+ name: Wheels ${{ matrix.os }}
92
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
93
+ runs-on: ${{ matrix.os }}
94
+ strategy:
95
+ matrix:
96
+ # macos-15-intel builds the x86_64 wheel natively, so its tests run.
97
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-latest]
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ - uses: pypa/cibuildwheel@v3.1
101
+ - uses: actions/upload-artifact@v4
102
+ with:
103
+ name: wheels-${{ matrix.os }}
104
+ path: wheelhouse/*.whl
105
+
106
+ sdist:
107
+ name: Source distribution
108
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
109
+ runs-on: ubuntu-latest
110
+ steps:
111
+ - uses: actions/checkout@v4
112
+ - uses: astral-sh/setup-uv@v6
113
+ - run: uv build --sdist
114
+ - name: Tag matches the package version
115
+ if: startsWith(github.ref, 'refs/tags/')
116
+ run: |
117
+ version=$(tar xzf dist/*.tar.gz -O --wildcards '*/PKG-INFO' | sed -n 's/^Version: //p')
118
+ test "${GITHUB_REF_NAME}" = "v${version}" || {
119
+ echo "tag ${GITHUB_REF_NAME} does not match version ${version}"; exit 1; }
120
+ - uses: actions/upload-artifact@v4
121
+ with:
122
+ name: sdist
123
+ path: dist/*.tar.gz
124
+
125
+ # Trusted publishing (OIDC): no API tokens are stored in the repository.
126
+ # The publishers are registered on PyPI and TestPyPI for this workflow file
127
+ # and these environment names.
128
+ publish-testpypi:
129
+ name: Publish to TestPyPI
130
+ if: github.event_name == 'workflow_dispatch' && inputs.publish
131
+ needs: [c, sanitizers, tsan, valgrind, python, wheels, sdist]
132
+ runs-on: ubuntu-latest
133
+ environment: { name: testpypi, url: "https://test.pypi.org/p/fastprox" }
134
+ permissions: { id-token: write }
135
+ steps:
136
+ - uses: actions/download-artifact@v4
137
+ with: { path: dist, merge-multiple: true }
138
+ - uses: pypa/gh-action-pypi-publish@release/v1
139
+ with: { repository-url: "https://test.pypi.org/legacy/" }
140
+
141
+ publish-pypi:
142
+ name: Publish to PyPI
143
+ if: startsWith(github.ref, 'refs/tags/v')
144
+ needs: [c, sanitizers, tsan, valgrind, python, wheels, sdist]
145
+ runs-on: ubuntu-latest
146
+ environment: { name: pypi, url: "https://pypi.org/p/fastprox" }
147
+ permissions: { id-token: write }
148
+ steps:
149
+ - uses: actions/download-artifact@v4
150
+ with: { path: dist, merge-multiple: true }
151
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,36 @@
1
+ name: fuzz
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "17 3 * * *" # nightly
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ libfuzzer:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/cache@v4
14
+ with:
15
+ path: corpus
16
+ key: fuzz-corpus-${{ github.run_id }}
17
+ restore-keys: fuzz-corpus-
18
+ - name: Build
19
+ env: { CC: clang, CXX: clang++ }
20
+ run: |
21
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DFASTPROX_FUZZ=ON \
22
+ -DFASTPROX_SANITIZE=ON -DFASTPROX_THREADS=OFF -DBUILD_SHARED_LIBS=OFF
23
+ cmake --build build --target fuzz_ops
24
+ - name: Fuzz for 20 minutes
25
+ run: |
26
+ mkdir -p corpus
27
+ ./build/fuzz/fuzz_ops corpus -max_total_time=1200 -timeout=10 -rss_limit_mb=4096 \
28
+ -max_len=8192 -print_final_stats=1
29
+ - if: failure()
30
+ uses: actions/upload-artifact@v4
31
+ with:
32
+ name: fuzz-crashes
33
+ path: |
34
+ crash-*
35
+ timeout-*
36
+ leak-*
@@ -0,0 +1,99 @@
1
+ name: profile
2
+
3
+ # Single-thread profile of 1D TV on GitHub's x86_64 runner, fastprox against
4
+ # proxTV, with hardware counters where the VM exposes them and GCC's assembly
5
+ # of the kernels. `ubuntu-latest` lands on an Intel or an AMD CPU at random,
6
+ # so this starts several identical jobs and labels each artifact with the CPU
7
+ # it got. Started by hand from the Actions tab.
8
+ on:
9
+ workflow_dispatch:
10
+ inputs:
11
+ jobs:
12
+ description: "Identical jobs to start (each lands on a random CPU), 1 to 12"
13
+ type: string
14
+ default: "6"
15
+ n:
16
+ description: "Samples per row"
17
+ type: string
18
+ default: "100000"
19
+ reps:
20
+ description: "Repetitions per setting (fresh input each)"
21
+ type: string
22
+ default: "31"
23
+
24
+ jobs:
25
+ slots:
26
+ runs-on: ubuntu-latest
27
+ outputs:
28
+ list: ${{ steps.s.outputs.list }}
29
+ steps:
30
+ - id: s
31
+ env: { JOBS: "${{ inputs.jobs }}" }
32
+ run: |
33
+ python3 - <<'EOF' >> "$GITHUB_OUTPUT"
34
+ import json, os
35
+ k = max(1, min(12, int(os.environ["JOBS"])))
36
+ print("list=" + json.dumps(list(range(1, k + 1))))
37
+ EOF
38
+
39
+ profile:
40
+ name: profile ${{ matrix.slot }}
41
+ needs: slots
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 30
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ slot: ${{ fromJSON(needs.slots.outputs.list) }}
48
+ env: { N: "${{ inputs.n }}", REPS: "${{ inputs.reps }}" }
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: astral-sh/setup-uv@v6
52
+ - name: Machine
53
+ id: machine
54
+ run: |
55
+ {
56
+ lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
57
+ c++ --version | head -1
58
+ } | tee machine.txt
59
+ model=$(lscpu | sed -n 's/^Model name: *//p' | head -1)
60
+ case "$model" in
61
+ *Intel*|*INTEL*) vendor=intel ;;
62
+ *AMD*) vendor=amd ;;
63
+ *) vendor=other ;;
64
+ esac
65
+ echo "vendor=$vendor" >> "$GITHUB_OUTPUT"
66
+ echo "### profile ${{ matrix.slot }}: $model" >> "$GITHUB_STEP_SUMMARY"
67
+ - name: Allow hardware counters for this user
68
+ run: sudo sysctl -w kernel.perf_event_paranoid=1 || true
69
+ - name: Build fastprox
70
+ run: |
71
+ cmake -S . -B build/c -DCMAKE_BUILD_TYPE=Release
72
+ cmake --build build/c --target fastprox -j 4
73
+ - name: Build proxTV (fetched at run time, never vendored)
74
+ run: |
75
+ sudo apt-get update && sudo apt-get install -y liblapack-dev
76
+ uv sync --python 3.12
77
+ uv run python -c "import sys; sys.path.insert(0, 'bench'); import compare_external as c; print(c.build_proxtv())"
78
+ - name: Profile
79
+ run: |
80
+ c++ -O2 -std=c++17 bench/profile_kernels.cpp -Iinclude -Lbuild/c -lfastprox -ldl \
81
+ -Wl,-rpath,"$PWD/build/c" -o profile_kernels
82
+ ./profile_kernels build/external/libproxtv_tv1.so "$N" "$REPS" | tee profile.txt
83
+ { echo '```'; cat profile.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
84
+ - name: Assembly of the kernels (same flags as the library)
85
+ run: |
86
+ for f in tv1d projections; do
87
+ c++ -std=c++17 -O3 -DNDEBUG -fno-fast-math -Iinclude -Isrc -S "src/ops/$f.cpp" -o "$f.s"
88
+ done
89
+ python3 bench/asm_summary.py tv1d.s projections.s | tee asm_summary.txt
90
+ { echo '```'; cat asm_summary.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
91
+ - uses: actions/upload-artifact@v4
92
+ with:
93
+ name: profile-${{ matrix.slot }}-${{ steps.machine.outputs.vendor }}
94
+ path: |
95
+ machine.txt
96
+ profile.txt
97
+ asm_summary.txt
98
+ tv1d.s
99
+ projections.s
@@ -0,0 +1,89 @@
1
+ name: variants
2
+
3
+ # Variants of the 1D TV kernel side by side against proxTV, in one binary per
4
+ # job (bench/tv_variants.cpp), on GitHub's arm64 runner (Neoverse-N2) and on
5
+ # x86_64 jobs that land on random Intel or AMD CPUs. Comparing within a job
6
+ # keeps the CPU out of the comparison. Runs on every push to a branch named
7
+ # variants/<topic>, or by hand from the Actions tab.
8
+ on:
9
+ push:
10
+ branches: ["variants/**"]
11
+ workflow_dispatch:
12
+ inputs:
13
+ x86_jobs:
14
+ description: "x86_64 jobs (each lands on a random CPU), 0 to 8"
15
+ type: string
16
+ default: "3"
17
+ n:
18
+ description: "Samples per row"
19
+ type: string
20
+ default: "100000"
21
+ reps:
22
+ description: "Repetitions per setting (fresh input each)"
23
+ type: string
24
+ default: "31"
25
+
26
+ jobs:
27
+ matrix:
28
+ runs-on: ubuntu-latest
29
+ outputs:
30
+ include: ${{ steps.m.outputs.include }}
31
+ steps:
32
+ - id: m
33
+ env: { X86: "${{ inputs.x86_jobs || '3' }}" }
34
+ run: |
35
+ python3 - <<'EOF' >> "$GITHUB_OUTPUT"
36
+ import json, os
37
+ k = max(0, min(8, int(os.environ["X86"])))
38
+ jobs = [{"os": "ubuntu-24.04-arm", "slot": 0}] + [{"os": "ubuntu-latest", "slot": i} for i in range(1, k + 1)]
39
+ print("include=" + json.dumps(jobs))
40
+ EOF
41
+
42
+ variants:
43
+ name: variants ${{ matrix.os }} ${{ matrix.slot }}
44
+ needs: matrix
45
+ runs-on: ${{ matrix.os }}
46
+ timeout-minutes: 30
47
+ strategy:
48
+ fail-fast: false
49
+ matrix:
50
+ include: ${{ fromJSON(needs.matrix.outputs.include) }}
51
+ env: { N: "${{ inputs.n || '100000' }}", REPS: "${{ inputs.reps || '31' }}" }
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: astral-sh/setup-uv@v6
55
+ - name: Machine
56
+ id: machine
57
+ run: |
58
+ {
59
+ lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
60
+ c++ --version | head -1
61
+ } | tee machine.txt
62
+ model=$(lscpu | sed -n 's/^Model name: *//p' | head -1)
63
+ case "$model" in
64
+ *Intel*|*INTEL*) cpu=intel ;;
65
+ *AMD*) cpu=amd ;;
66
+ *Neoverse*) cpu=neoverse ;;
67
+ *) cpu=other ;;
68
+ esac
69
+ echo "cpu=$cpu" >> "$GITHUB_OUTPUT"
70
+ echo "### variants ${{ matrix.slot }}: $model" >> "$GITHUB_STEP_SUMMARY"
71
+ - name: Build proxTV (fetched at run time, never vendored)
72
+ run: |
73
+ sudo apt-get update && sudo apt-get install -y liblapack-dev
74
+ uv sync --python 3.12
75
+ uv run python -c "import sys; sys.path.insert(0, 'bench'); import compare_external as c; print(c.build_proxtv())"
76
+ - name: Variants
77
+ run: |
78
+ c++ -std=c++17 -O3 -DNDEBUG -fno-fast-math -Iinclude -Isrc bench/tv_variants.cpp \
79
+ src/api/api.cpp src/core/parallel.cpp src/ops/pointwise.cpp src/ops/group.cpp \
80
+ src/ops/projections.cpp src/ops/isotonic.cpp -DFASTPROX_BUILDING \
81
+ -DFASTPROX_VERSION_STRING='"variants"' -ldl -lpthread -o tv_variants
82
+ ./tv_variants build/external/libproxtv_tv1.so "$N" "$REPS" | tee variants.txt
83
+ { echo '```'; cat variants.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
84
+ - uses: actions/upload-artifact@v4
85
+ with:
86
+ name: variants-${{ matrix.slot }}-${{ steps.machine.outputs.cpu }}
87
+ path: |
88
+ machine.txt
89
+ variants.txt
@@ -0,0 +1,13 @@
1
+ build/
2
+ .venv/
3
+ __pycache__/
4
+ *.egg-info/
5
+ *.so
6
+ *.dylib
7
+ *.pyd
8
+ compile_commands.json
9
+ .pytest_cache/
10
+ .hypothesis/
11
+ dist/
12
+ .cache/
13
+ fuzz-crash.bin
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,84 @@
1
+ # Changelog
2
+
3
+ All notable changes to fastprox. Planned work is in [ROADMAP.md](ROADMAP.md).
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] — 2026-10-16
9
+
10
+ First release. Changes since 0.1.0rc2:
11
+
12
+ ### Added
13
+
14
+ - `variants` workflow and `bench/tv_variants.cpp`: variants of the 1D TV kernel side by side against proxTV (classic, linearized, hybrid and weighted taut string), compiled into one binary and timed on the same inputs with the order rotated, on the arm64 runner and on x86_64 jobs. It runs on pushes to `variants/*` branches or by hand. `condat` takes the length of its first, paper-form phase as a template parameter (`kShort` by default) so variants need no copy of the kernel.
15
+
16
+ - `profile` workflow (manual dispatch): several identical jobs on GitHub's x86_64 runner, each labelled with the CPU it landed on. Each job profiles single-thread 1D TV against proxTV with `bench/profile_kernels.cpp`, which records hardware counters where the VM exposes them and rotates the solver order so that none benefits from branch history. It also summarises GCC's assembly of the kernels (conditional jumps against selects) with `bench/asm_summary.py`.
17
+
18
+ ### Documentation
19
+
20
+ - `examples/`: basic usage, 1D TV denoising, proximal gradient (lasso and batched simplex least squares), derivatives checked against finite differences, and bilevel learning of TV weights through the VJP. `tests/python/test_examples.py` runs them all; matplotlib for the plot is in the `examples` dependency group.
21
+ - `docs/api.md` lists, for every operator, which Jacobian the derivatives use at kinks and ties. It explains why each choice is an element of the B-subdifferential, and that the x, γ and parameter derivatives together form one. The ℓ∞ prox and conjugate proxes reverse the thresholding rule through the Moreau identity.
22
+
23
+ ### Changed
24
+
25
+ - `bench/compare_external.py` rotates the order of the solvers with every repetition (fastprox's default solver always ran first, on an input just written, and its Condat path right after it), and quick mode takes the median of 7 repetitions instead of 3.
26
+ - 1D TV: the per-call finiteness and range pass runs in 8 independent lanes with `fminnm`/`fmaxnm` on arm64 (compare-selects elsewhere) instead of one dependent chain: 0.25 instead of 0.48 ns per sample with Clang and 0.89 with GCC on Apple M5 (float64, n = 10⁶). Weighted rows on white noise at λ = 10 went from 5.52 to 5.29 ns per sample, and float32 rows at λ = 0.1 are now 1–5% behind Condat's single-precision code (were 6–12%).
27
+ - 1D TV: the branch-free bound updates in short segments of Condat's algorithm are written as min/max of the excess instead of selects on a comparison, which GCC compiled back into branches. Same arithmetic up to the sign of zero. With GCC 16 on Apple M5, 4–12% faster at λ = 0.1 and 5–27% faster at λ = 1 (uniform and weighted), within 3% of the Clang build at λ ≤ 1; at λ = 10, uniform rows lose 1–19% under GCC (random walk most), where the branches it emitted happened to predict well, and weighted rows gain up to 10%. Clang's code is unchanged.
28
+ - Simplex and ℓ1 ball: the first pass of Condat's method skips blocks of 8 entries whose maximum is at or below the running pivot (the pivot can only change on an entry above it, so results are bit-identical). The ℓ1 norm that decides whether x is inside the ball is summed in 8 lanes instead of one dependent chain. On Apple M5 at n = 10⁶: simplex projection 0.50 instead of 0.60 ns per sample on Gaussian input, now faster than Condat's code (0.56); unweighted ℓ1 ball 0.64 instead of 1.08, weighted 1.08 instead of 1.37 (Clang) and 1.05 instead of 1.68 (GCC). The ℓ∞ prox shares the ℓ1-ball kernel.
29
+
30
+ - x86-64: the min/max helpers behind 1D TV's bound updates and range pass and the simplex block maximum use SSE2 `minsd`/`maxsd` instead of a compare-select, which GCC compiled into compare-and-branch. Same values. On GitHub's x86_64 runners (GCC 13.3, n = 10⁵), 1D TV went from fastest or within 2% of proxTV in 3–6 of 18 settings to 12–13 on Intel Xeons (8573C, 6973P-C) and from 14–17 to 17–18 on AMD EPYC; at λ = 1 fastprox now takes 0.82–1.03 of proxTV's time on Intel (was 1.01–1.16). Uniform random walk at λ = 10 lost 5–11% relative to proxTV, where the branches predicted well.
31
+
32
+ - 1D TV: after the first 3 samples of a segment, Condat's algorithm now tracks the segment by its running sum. The lower bound is the running maximum of the candidates (s − λₖ)/m and the upper bound the running minimum of (s + λₖ)/m, so the clamps are min/max and the only value carried between steps is the sum, instead of a dependency chain through every clamp (same algorithm and structure; values differ at rounding level). On Apple M5 (n = 10⁵, single thread) the kernel is 1.3–2.9× faster at λ ≥ 1 and unchanged at λ = 0.1. Weighted white noise at λ = 10, the one setting where fastprox trailed proxTV on every machine, went from 1.03–1.09 of proxTV's time to 0.43–0.45 (Clang) and from 1.14 to 0.60 (GCC 16), and uniform random walk at λ = 10 from 0.99 to 0.64. On GitHub's runners (GCC 13.3): weighted white noise at λ = 10 from 1.12–1.14 to 0.73–0.78 on Intel Xeons, from 1.00–1.08 to 0.55–0.62 on AMD EPYC and from 1.22 to 0.92 on Neoverse-N2; uniform random walk at λ = 10 from 1.05–1.09 to 0.83–0.84 on Intel and from 1.19 to 1.06 on Neoverse-N2. At λ = 0.1 weighted rows lost about 3% on Intel. Long segments no longer use branches for the clamps, so `kBranchFree` (8) is replaced by `kShort` (3).
33
+
34
+ ### Fixed
35
+
36
+ - Weighted ℓ1-ball projection with weights spanning many orders of magnitude could return outputs far larger than the input (z = [5.5e7, 2.4e70] for x = [11.0, 2.0], weights [1.8e-308, 7.7e-246] and radius 1e-300; found by fuzzing). Removing an entry within rounding of the pivot that dominated the running sums cancelled them. The threshold is now kept at least at the largest breakpoint left out of the support, as it is in exact arithmetic. The same guard is in the unweighted kernel.
37
+
38
+ ## [0.1.0rc2] — 2026-10-15
39
+
40
+ ### Fixed
41
+
42
+ - The macOS wheel of 0.1.0rc1 aborted the process (`OMP: Error #15`) when PyTorch, or any package bundling a recent LLVM OpenMP runtime, was used in the same process: each wheel carried its own libomp, and the copies refuse to coexist.
43
+
44
+ ### Changed
45
+
46
+ - Rows run on fastprox's own thread pool (`src/core/parallel.cpp`) instead of OpenMP. No OpenMP runtime is linked or bundled on any platform; the wheels depend only on the system and NumPy, and macOS wheels now support macOS 11.0 (arm64) and 10.15 (x86_64). Same results bit for bit; dynamic scheduling as before. Measured against the OpenMP build on Apple M5 in one process: equal on one thread, 1–8% slower at 2–4 threads on small batches, 8–31% faster at 8–15 threads.
47
+ - `threads = 0` means `fp_default_threads()`: `FASTPROX_NUM_THREADS`, else the first number in `OMP_NUM_THREADS`, else the CPUs the process may use (CPU affinity on Linux). Concurrent callers do not oversubscribe: one uses the pool, the others run serially. A child after `fork()` builds its own pool.
48
+ - `fp_has_openmp()` and `fastprox.has_openmp()` are replaced by `fp_default_threads()` and `fastprox.default_threads()`; the CMake option `FASTPROX_OPENMP` is now `FASTPROX_THREADS`.
49
+ - CI: a ThreadSanitizer job; AddressSanitizer and Valgrind now run with threads.
50
+
51
+ ## [0.1.0rc1] — 2026-10-15
52
+
53
+ First release candidate, on PyPI (`pip install --pre fastprox`).
54
+
55
+ ### Added
56
+
57
+ - Plain-C API (`include/fastprox.h`) over a C++17 core: opaque operator and workspace handles, `fp_prox`, `fp_prox_conj`, `fp_jvp`, `fp_vjp`, error codes with every exception caught at the boundary.
58
+ - Operators: soft thresholding (weighted), elastic net, Huber, group ℓ2 and ℓ2 norm, box, nonnegative orthant, ℓ2 ball, simplex, weighted ℓ1 ball, ℓ∞ norm, isotonic regression, and 1D TV (Condat's direct algorithm and taut string, edge weights).
59
+ - JVP and VJP in the input, the step and the operator parameters for every operator.
60
+ - Batching over any axis of strided n-d arrays, OpenMP parallelism with bit-identical results for any thread count, and allocation-free calls with a workspace.
61
+ - Python package built with nanobind (abi3, CPython ≥ 3.12): one-shot functions and operator classes with `prox`, `prox_conj`, `jvp` and `vjp`.
62
+ - C/C++ and Python test suites, a benchmark script, and a CI workflow.
63
+ - Release pipeline: sdist job, trusted publishing to TestPyPI (manual dispatch with `publish`) and to PyPI (tags `v*`, checked against the package version); PyPI metadata and a README for the project page; pre-release tags through `FASTPROX_VERSION_SUFFIX` in `CMakeLists.txt`; Python tests also run on CPython 3.14.
64
+ - Fuzz harness for the whole API (libFuzzer target and standalone driver) with a nightly workflow; CVXPY + Clarabel reference fixtures; benchmarks against proxTV and Condat's code (`bench/compare_external.py`, `docs/benchmarks.md`); API reference (`docs/api.md`); Valgrind CI job.
65
+
66
+ ### Changed
67
+
68
+ - 1D TV: the Condat path falls back to the taut string after 3n + 1024 rescanned steps (was 16n + 1024 steps in total), which makes adversarial inputs such as ramps about 3.5× cheaper without affecting typical signals.
69
+ - 1D TV: the bound updates of Condat's algorithm are branch-free selects with a table of reciprocals, 25–35% faster on noisy signals (same arithmetic, so the same results).
70
+ - 1D TV: weighted problems now use Condat's direct algorithm, generalised to per-edge weights (radii precomputed per piece), with the same budgeted fallback to the taut string; `FP_TV1D_CONDAT` accepts weights. Weighted rows went from 1.4–2.8× slower than proxTV's weighted taut string to faster in most settings.
71
+ - 1D TV: radii that can never be active (above the dual bound j · (max x − min x)) are clamped, and when no edge can be active the result is the constant mean. Extreme weights or steps no longer cost accuracy: the residual stays within 10 n ε ‖x‖∞.
72
+ - 1D TV: the taut string stores its two chains as segments (length, rise, cached slope) in coordinates local to the current knot, an idea taken from proxTV's classic taut string. It is 17–26% faster and needs no compensated sums: the cumulative sum of the signal is never formed.
73
+ - Unweighted simplex and ℓ1-ball projections keep candidate values instead of indices (about 30% faster). The simplex also tests finiteness inside its pivot pass instead of a separate pass (a further 19%), and the ℓ1 ball and ℓ∞ prox reuse their ℓ1 norm as the test.
74
+ - 1D TV: bound updates are branch-free only in the first 8 samples of a segment; weighted rows read their radii directly when none can need clamping; operator construction makes one pass over the weights.
75
+ - Workspace scratch is no longer zero-filled, and the finiteness check is a branch-free pass: one-shot calls on large rows are noticeably faster.
76
+ - The version is defined once, in `CMakeLists.txt`; the Python package reads it from there.
77
+
78
+ ### Fixed
79
+
80
+ - Weighted ℓ1-ball projection returned infinities for tiny weights (around 1e-279) because sums of squared weights underflowed (found by fuzzing). Weights are now normalised by their maximum, and ratios below 1e-100 are rejected.
81
+ - `fp_prox_conj` with σ = 0 on arrays with negative strides computed byte offsets in unsigned arithmetic (undefined behaviour, found by fuzzing).
82
+ - Static Windows builds marked API symbols `dllexport`.
83
+ - The taut string kept cumulative sums in plain doubles and missed the 10 n ε ‖x‖∞ accuracy bound on long rows; its chains now use local coordinates.
84
+ - A test assumed weighted isotonic regression is firmly nonexpansive in the Euclidean norm; it is so in the weighted norm, which the test now uses.
@@ -0,0 +1,34 @@
1
+ # fastprox
2
+
3
+ Exact proximal operators with JVP/VJP. C++17 core behind the plain-C header
4
+ `include/fastprox.h`; Python bindings via nanobind. The design spec is
5
+ `docs/spec.md` — read it before changing the API or adding operators.
6
+
7
+ ## Commands
8
+
9
+ - C library and tests: `cmake -S . -B build/c -G Ninja && cmake --build build/c && ./build/c/tests/c/test_core && ./build/c/tests/c/test_header`
10
+ - Sanitizers: configure a second tree with `-DFASTPROX_SANITIZE=ON -DCMAKE_BUILD_TYPE=Debug` (ThreadSanitizer: `-fsanitize=thread` in the C/C++ and linker flags, as in the `tsan` CI job)
11
+ - Python (always through uv): `uv sync`, `uv run pytest -q`. uv rebuilds the extension when C++ sources change (`tool.uv.cache-keys`).
12
+ - Benchmarks: `uv run python bench/bench_python.py`
13
+ - Releasing: set the version in `project()` and `FASTPROX_VERSION_SUFFIX` (`CMakeLists.txt`) and the macros in `fastprox.h`; dry run with `gh workflow run ci.yml -f publish=true` (TestPyPI); then push the tag `v<version>`, which publishes to PyPI.
14
+
15
+ ## Layout
16
+
17
+ - `src/core/op.hpp` — `fp_op` base class; `ElemOp<D>` (elementwise, needs `prox1`/`deriv1`) and `RowOp<D>` (row kernels `prox`/`jvp`/`vjp`) CRTP helpers.
18
+ - `src/core/driver.hpp` — batching over strided n-d arrays, saved structure; `src/core/parallel.cpp` — the thread pool (no OpenMP: bundled libomp copies abort each other, e.g. next to PyTorch); `src/core/layout.hpp` — row and elementwise iteration plans.
19
+ - `src/ops/*.cpp` — one file per operator family, each with a `make_*` factory declared in `src/ops/ops.hpp`.
20
+ - `src/api/api.cpp` — the only `extern "C"` code: validation, dtype dispatch, try/catch.
21
+ - `bindings/python/src/module.cpp` calls only the C API; `bindings/python/fastprox/__init__.py` is the user-facing package.
22
+
23
+ ## Tracking
24
+
25
+ - `ROADMAP.md` lists planned work per release with status boxes; tick items as they land and add new ones as they come up.
26
+ - `CHANGELOG.md` records finished changes under `[Unreleased]`, in the same change that makes them.
27
+ - Any decision that departs from `docs/spec.md` goes in the decisions table at the end of `ROADMAP.md`, with its date.
28
+
29
+ ## Rules
30
+
31
+ - Clean room: never read GPL, CeCILL or unlicensed code (Condat's C files, Kolmogorov's maxflow/TVTree, Chambolle's graphTV, PyMaxflow, gco). Implement from papers and cite them in the kernel's header comment. proxTV (BSD) may be consulted with attribution.
32
+ - No `-ffast-math`. Results must be bit-identical for any thread count: rows are independent, and cross-row sums (vjp parameter gradients) use fixed blocks from `reduction_blocks`, never thread-dependent partitions.
33
+ - No allocation inside `fp_prox`/`fp_jvp`/`fp_vjp` when a workspace is given; size everything in `build_workspace` (tested by an allocation counter in `tests/c/test_core.cpp`).
34
+ - Adding an operator: kernel + derivatives in `src/ops/`, factory in `ops.hpp`, constructor in `fastprox.h` and `api.cpp`, nanobind wrapper, Python class with `_layout`, and an entry in `CASES` in `tests/python/test_operators.py` (which runs reference, finite-difference, adjointness, nonexpansiveness, threading and stride tests on it) plus `derivative_cases` in `tests/c/test_core.cpp`.