backcountry 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.
- backcountry-0.1.0/.gitignore +12 -0
- backcountry-0.1.0/LICENSE +21 -0
- backcountry-0.1.0/PKG-INFO +302 -0
- backcountry-0.1.0/README.md +275 -0
- backcountry-0.1.0/RUNNING.md +64 -0
- backcountry-0.1.0/docs/architecture.md +135 -0
- backcountry-0.1.0/docs/artifacts.md +276 -0
- backcountry-0.1.0/docs/judge-protocol.md +242 -0
- backcountry-0.1.0/docs/limitations.md +80 -0
- backcountry-0.1.0/docs/manifest.md +552 -0
- backcountry-0.1.0/docs/measurement.md +430 -0
- backcountry-0.1.0/docs/operating.md +229 -0
- backcountry-0.1.0/docs/usage.md +267 -0
- backcountry-0.1.0/pyproject.toml +59 -0
- backcountry-0.1.0/src/backcountry/__init__.py +54 -0
- backcountry-0.1.0/src/backcountry/__main__.py +5 -0
- backcountry-0.1.0/src/backcountry/harness/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/benchmark.py +155 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/bundle.py +790 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/emit.py +897 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/loader.py +173 -0
- backcountry-0.1.0/src/backcountry/harness/artifact/validate.py +418 -0
- backcountry-0.1.0/src/backcountry/harness/bind/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/bind/certify.py +278 -0
- backcountry-0.1.0/src/backcountry/harness/bind/emit.py +786 -0
- backcountry-0.1.0/src/backcountry/harness/bind/fp32_region.py +158 -0
- backcountry-0.1.0/src/backcountry/harness/bind/graph.py +214 -0
- backcountry-0.1.0/src/backcountry/harness/bind/sized.py +225 -0
- backcountry-0.1.0/src/backcountry/harness/bind/swap.py +6 -0
- backcountry-0.1.0/src/backcountry/harness/bind/verify.py +122 -0
- backcountry-0.1.0/src/backcountry/harness/cli.py +563 -0
- backcountry-0.1.0/src/backcountry/harness/control.py +224 -0
- backcountry-0.1.0/src/backcountry/harness/e2e.py +225 -0
- backcountry-0.1.0/src/backcountry/harness/front_door.py +272 -0
- backcountry-0.1.0/src/backcountry/harness/guide.py +62 -0
- backcountry-0.1.0/src/backcountry/harness/inputs/SOURCES.md +38 -0
- backcountry-0.1.0/src/backcountry/harness/inputs/text.jsonl.gz +0 -0
- backcountry-0.1.0/src/backcountry/harness/intent.py +686 -0
- backcountry-0.1.0/src/backcountry/harness/judge/__init__.py +34 -0
- backcountry-0.1.0/src/backcountry/harness/judge/agent.py +360 -0
- backcountry-0.1.0/src/backcountry/harness/judge/briefing.py +74 -0
- backcountry-0.1.0/src/backcountry/harness/judge/client.py +344 -0
- backcountry-0.1.0/src/backcountry/harness/judge/directions.py +54 -0
- backcountry-0.1.0/src/backcountry/harness/judge/examples.py +222 -0
- backcountry-0.1.0/src/backcountry/harness/judge/experience.py +120 -0
- backcountry-0.1.0/src/backcountry/harness/judge/method_level.py +107 -0
- backcountry-0.1.0/src/backcountry/harness/judge/model_level.py +289 -0
- backcountry-0.1.0/src/backcountry/harness/judge/principles.py +485 -0
- backcountry-0.1.0/src/backcountry/harness/judge/prompts.py +314 -0
- backcountry-0.1.0/src/backcountry/harness/judge/queue.py +182 -0
- backcountry-0.1.0/src/backcountry/harness/judge/schema.py +374 -0
- backcountry-0.1.0/src/backcountry/harness/judge/scripted.py +65 -0
- backcountry-0.1.0/src/backcountry/harness/judge/source_context.py +153 -0
- backcountry-0.1.0/src/backcountry/harness/knowledge.py +116 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/child.py +657 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/gates.py +212 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/golden.py +266 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/numeric.py +78 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/sizes.py +287 -0
- backcountry-0.1.0/src/backcountry/harness/ladder/static_checks.py +298 -0
- backcountry-0.1.0/src/backcountry/harness/log.py +54 -0
- backcountry-0.1.0/src/backcountry/harness/loop.py +5765 -0
- backcountry-0.1.0/src/backcountry/harness/manifest.py +575 -0
- backcountry-0.1.0/src/backcountry/harness/measure/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/measure/clocks.py +461 -0
- backcountry-0.1.0/src/backcountry/harness/measure/controls.py +71 -0
- backcountry-0.1.0/src/backcountry/harness/measure/heat.py +236 -0
- backcountry-0.1.0/src/backcountry/harness/measure/peaks.py +205 -0
- backcountry-0.1.0/src/backcountry/harness/measure/probe.py +128 -0
- backcountry-0.1.0/src/backcountry/harness/measure/sequences.py +4 -0
- backcountry-0.1.0/src/backcountry/harness/measure/session.py +449 -0
- backcountry-0.1.0/src/backcountry/harness/model_search.py +1375 -0
- backcountry-0.1.0/src/backcountry/harness/regions/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/regions/build.py +196 -0
- backcountry-0.1.0/src/backcountry/harness/regions/fingerprint.py +131 -0
- backcountry-0.1.0/src/backcountry/harness/regions/price.py +407 -0
- backcountry-0.1.0/src/backcountry/harness/regions/rank.py +256 -0
- backcountry-0.1.0/src/backcountry/harness/regions/roofline.py +222 -0
- backcountry-0.1.0/src/backcountry/harness/regions/store.py +155 -0
- backcountry-0.1.0/src/backcountry/harness/regions/sweep.py +46 -0
- backcountry-0.1.0/src/backcountry/harness/regions/types.py +109 -0
- backcountry-0.1.0/src/backcountry/harness/report.py +174 -0
- backcountry-0.1.0/src/backcountry/harness/resume.py +194 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/poison.py +32 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/protocol.py +379 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/recovery.py +74 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/watchdog.py +31 -0
- backcountry-0.1.0/src/backcountry/harness/sandbox/worker.py +35 -0
- backcountry-0.1.0/src/backcountry/harness/scaffold/__init__.py +65 -0
- backcountry-0.1.0/src/backcountry/harness/scaffold/lower.py +1552 -0
- backcountry-0.1.0/src/backcountry/harness/scaffold/native.py +118 -0
- backcountry-0.1.0/src/backcountry/harness/scaffold/stitch.py +458 -0
- backcountry-0.1.0/src/backcountry/harness/scaffold/symshape.py +173 -0
- backcountry-0.1.0/src/backcountry/harness/serve.py +426 -0
- backcountry-0.1.0/src/backcountry/harness/size_questions.py +216 -0
- backcountry-0.1.0/src/backcountry/harness/trace/__init__.py +59 -0
- backcountry-0.1.0/src/backcountry/harness/trace/freeze.py +110 -0
- backcountry-0.1.0/src/backcountry/harness/trace/optable.py +128 -0
- backcountry-0.1.0/src/backcountry/harness/trace/patches.py +565 -0
- backcountry-0.1.0/src/backcountry/harness/trace/recorder.py +565 -0
- backcountry-0.1.0/src/backcountry/harness/trace/replay.py +186 -0
- backcountry-0.1.0/src/backcountry/harness/trace/serialize.py +95 -0
- backcountry-0.1.0/src/backcountry/harness/trace/types.py +107 -0
- backcountry-0.1.0/src/backcountry/harness/trace/walk.py +96 -0
- backcountry-0.1.0/src/backcountry/harness/watch.py +374 -0
- backcountry-0.1.0/src/backcountry/harness/workload.py +206 -0
- backcountry-0.1.0/src/backcountry_runtime/LICENSE-omlx +190 -0
- backcountry-0.1.0/src/backcountry_runtime/__init__.py +0 -0
- backcountry-0.1.0/src/backcountry_runtime/apply.py +313 -0
- backcountry-0.1.0/src/backcountry_runtime/captured_kernels.py +86 -0
- backcountry-0.1.0/src/backcountry_runtime/checkpoints.py +53 -0
- backcountry-0.1.0/src/backcountry_runtime/compare.py +161 -0
- backcountry-0.1.0/src/backcountry_runtime/exact.py +10 -0
- backcountry-0.1.0/src/backcountry_runtime/exact_rows.py +228 -0
- backcountry-0.1.0/src/backcountry_runtime/functions.py +323 -0
- backcountry-0.1.0/src/backcountry_runtime/generation.py +334 -0
- backcountry-0.1.0/src/backcountry_runtime/grammar.py +150 -0
- backcountry-0.1.0/src/backcountry_runtime/graph.py +512 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/CMakeLists.txt +12 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/__init__.py +167 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/graph_native.cpp +331 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-312/_graph_native.cpython-312-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-312/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-313/_graph_native.cpython-313-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-313/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-314/_graph_native.cpython-314-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_14_0_arm64/cpython-314/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-312/_graph_native.cpython-312-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-312/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-313/_graph_native.cpython-313-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-313/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-314/_graph_native.cpython-314-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_15_0_arm64/cpython-314/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-312/_graph_native.cpython-312-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-312/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-313/_graph_native.cpython-313-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-313/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-314/_graph_native.cpython-314-darwin.so +0 -0
- backcountry-0.1.0/src/backcountry_runtime/graph_native/prebuilt/py3-none-macosx_26_0_arm64/cpython-314/build.json +1 -0
- backcountry-0.1.0/src/backcountry_runtime/inference.py +193 -0
- backcountry-0.1.0/src/backcountry_runtime/kernels.py +293 -0
- backcountry-0.1.0/src/backcountry_runtime/model_run.py +279 -0
- backcountry-0.1.0/src/backcountry_runtime/numeric.py +153 -0
- backcountry-0.1.0/src/backcountry_runtime/omlx_row_exact.py +595 -0
- backcountry-0.1.0/src/backcountry_runtime/original_sequence.py +93 -0
- backcountry-0.1.0/src/backcountry_runtime/sequence.py +188 -0
- backcountry-0.1.0/src/backcountry_runtime/sizes.py +229 -0
- backcountry-0.1.0/src/backcountry_runtime/stages.py +53 -0
- backcountry-0.1.0/src/backcountry_runtime/state.py +329 -0
- backcountry-0.1.0/src/backcountry_runtime/stats.py +127 -0
- backcountry-0.1.0/src/backcountry_runtime/swap.py +229 -0
- backcountry-0.1.0/src/backcountry_runtime/tune.py +1004 -0
- backcountry-0.1.0/src/backcountry_runtime/watch.py +256 -0
- backcountry-0.1.0/tests/__init__.py +0 -0
- backcountry-0.1.0/tests/cheats/__init__.py +247 -0
- backcountry-0.1.0/tests/cli/__init__.py +0 -0
- backcountry-0.1.0/tests/cli/apps.py +324 -0
- backcountry-0.1.0/tests/cli/conftest.py +71 -0
- backcountry-0.1.0/tests/cli/fake_judge.py +15 -0
- backcountry-0.1.0/tests/cli/fake_loop_judge.py +25 -0
- backcountry-0.1.0/tests/cli/test_check.py +72 -0
- backcountry-0.1.0/tests/cli/test_guide.py +40 -0
- backcountry-0.1.0/tests/cli/test_init.py +174 -0
- backcountry-0.1.0/tests/cli/test_job.py +71 -0
- backcountry-0.1.0/tests/cli/test_loop.py +92 -0
- backcountry-0.1.0/tests/conftest.py +138 -0
- backcountry-0.1.0/tests/drift.py +92 -0
- backcountry-0.1.0/tests/fixtures/__init__.py +0 -0
- backcountry-0.1.0/tests/fixtures/app_run_model.py +37 -0
- backcountry-0.1.0/tests/fixtures/bf16_state.py +33 -0
- backcountry-0.1.0/tests/fixtures/cache_retention.py +25 -0
- backcountry-0.1.0/tests/fixtures/compiled_submodule.py +37 -0
- backcountry-0.1.0/tests/fixtures/concat_join.py +17 -0
- backcountry-0.1.0/tests/fixtures/const_scale.py +32 -0
- backcountry-0.1.0/tests/fixtures/context_model.py +71 -0
- backcountry-0.1.0/tests/fixtures/data_branch.py +22 -0
- backcountry-0.1.0/tests/fixtures/eager_item.py +21 -0
- backcountry-0.1.0/tests/fixtures/engine_held_model.py +21 -0
- backcountry-0.1.0/tests/fixtures/engine_held_no_release.py +15 -0
- backcountry-0.1.0/tests/fixtures/engine_kit_model.py +33 -0
- backcountry-0.1.0/tests/fixtures/examples_model.py +27 -0
- backcountry-0.1.0/tests/fixtures/flux2_4b.py +272 -0
- backcountry-0.1.0/tests/fixtures/hand_norm.py +30 -0
- backcountry-0.1.0/tests/fixtures/history_model.py +71 -0
- backcountry-0.1.0/tests/fixtures/kernel_closure.py +32 -0
- backcountry-0.1.0/tests/fixtures/kernel_submodule.py +49 -0
- backcountry-0.1.0/tests/fixtures/kv_step.py +28 -0
- backcountry-0.1.0/tests/fixtures/layer_norm_affine.py +20 -0
- backcountry-0.1.0/tests/fixtures/llama_cache_model.py +16 -0
- backcountry-0.1.0/tests/fixtures/llama_engine.py +23 -0
- backcountry-0.1.0/tests/fixtures/llama_ish.py +59 -0
- backcountry-0.1.0/tests/fixtures/native_fusion.py +35 -0
- backcountry-0.1.0/tests/fixtures/native_recurrence.py +28 -0
- backcountry-0.1.0/tests/fixtures/nested_scopes.py +39 -0
- backcountry-0.1.0/tests/fixtures/norm_three_proj.py +24 -0
- backcountry-0.1.0/tests/fixtures/opaque_submodule.py +23 -0
- backcountry-0.1.0/tests/fixtures/operator_soup.py +28 -0
- backcountry-0.1.0/tests/fixtures/plain_function.py +15 -0
- backcountry-0.1.0/tests/fixtures/planted_win.py +38 -0
- backcountry-0.1.0/tests/fixtures/quantized_linear.py +27 -0
- backcountry-0.1.0/tests/fixtures/qwen_cache_model.py +18 -0
- backcountry-0.1.0/tests/fixtures/repeated_chain.py +27 -0
- backcountry-0.1.0/tests/fixtures/repeated_layers.py +32 -0
- backcountry-0.1.0/tests/fixtures/rope_context_model.py +74 -0
- backcountry-0.1.0/tests/fixtures/sibling_children.py +48 -0
- backcountry-0.1.0/tests/fixtures/size_branch.py +42 -0
- backcountry-0.1.0/tests/fixtures/sized_reshape.py +25 -0
- backcountry-0.1.0/tests/fixtures/slice_read.py +15 -0
- backcountry-0.1.0/tests/fixtures/split_heads.py +15 -0
- backcountry-0.1.0/tests/fixtures/square_rows.py +23 -0
- backcountry-0.1.0/tests/fixtures/state_call.py +50 -0
- backcountry-0.1.0/tests/fixtures/step_output_region.py +21 -0
- backcountry-0.1.0/tests/fixtures/tiny_engine.py +34 -0
- backcountry-0.1.0/tests/fixtures/two_partial_children.py +30 -0
- backcountry-0.1.0/tests/fixtures/two_stage_model.py +20 -0
- backcountry-0.1.0/tests/fixtures/vendor_parity.py +19 -0
- backcountry-0.1.0/tests/fixtures/views_only.py +22 -0
- backcountry-0.1.0/tests/fixtures/weight_view_in_module.py +21 -0
- backcountry-0.1.0/tests/fixtures/window_context_model.py +74 -0
- backcountry-0.1.0/tests/fixtures/workload_win.py +30 -0
- backcountry-0.1.0/tests/judges.py +55 -0
- backcountry-0.1.0/tests/test_any_function.py +144 -0
- backcountry-0.1.0/tests/test_app_run.py +110 -0
- backcountry-0.1.0/tests/test_apply_check.py +179 -0
- backcountry-0.1.0/tests/test_artifact.py +703 -0
- backcountry-0.1.0/tests/test_artifact_benchmark_defaults.py +85 -0
- backcountry-0.1.0/tests/test_artifact_generalization.py +170 -0
- backcountry-0.1.0/tests/test_bind.py +389 -0
- backcountry-0.1.0/tests/test_capture_versions.py +141 -0
- backcountry-0.1.0/tests/test_captured_kernels.py +141 -0
- backcountry-0.1.0/tests/test_checkpoint_packaging.py +94 -0
- backcountry-0.1.0/tests/test_compare.py +48 -0
- backcountry-0.1.0/tests/test_compiled_region_replay.py +126 -0
- backcountry-0.1.0/tests/test_context_workload.py +603 -0
- backcountry-0.1.0/tests/test_control.py +130 -0
- backcountry-0.1.0/tests/test_cooling_handoff.py +171 -0
- backcountry-0.1.0/tests/test_cross_model_audit.py +144 -0
- backcountry-0.1.0/tests/test_dead_work.py +132 -0
- backcountry-0.1.0/tests/test_delivery_accounting.py +459 -0
- backcountry-0.1.0/tests/test_drift.py +147 -0
- backcountry-0.1.0/tests/test_e2e.py +388 -0
- backcountry-0.1.0/tests/test_exact_rows.py +139 -0
- backcountry-0.1.0/tests/test_finish_control.py +184 -0
- backcountry-0.1.0/tests/test_fp32_reference.py +306 -0
- backcountry-0.1.0/tests/test_fp32_region_reference.py +87 -0
- backcountry-0.1.0/tests/test_freeze.py +65 -0
- backcountry-0.1.0/tests/test_front_door.py +175 -0
- backcountry-0.1.0/tests/test_generated_workload.py +158 -0
- backcountry-0.1.0/tests/test_graph_install.py +676 -0
- backcountry-0.1.0/tests/test_graph_model_matrix.py +132 -0
- backcountry-0.1.0/tests/test_graph_native.py +327 -0
- backcountry-0.1.0/tests/test_heat.py +246 -0
- backcountry-0.1.0/tests/test_identical_outputs.py +1382 -0
- backcountry-0.1.0/tests/test_inference_flow.py +319 -0
- backcountry-0.1.0/tests/test_install.py +518 -0
- backcountry-0.1.0/tests/test_intent.py +179 -0
- backcountry-0.1.0/tests/test_judge.py +1000 -0
- backcountry-0.1.0/tests/test_judge_agent.py +434 -0
- backcountry-0.1.0/tests/test_judge_briefing.py +143 -0
- backcountry-0.1.0/tests/test_judge_tries.py +51 -0
- backcountry-0.1.0/tests/test_knowledge.py +122 -0
- backcountry-0.1.0/tests/test_ladder_gates.py +579 -0
- backcountry-0.1.0/tests/test_ladder_numeric.py +478 -0
- backcountry-0.1.0/tests/test_launch_guard.py +153 -0
- backcountry-0.1.0/tests/test_library_inference.py +246 -0
- backcountry-0.1.0/tests/test_live_method.py +708 -0
- backcountry-0.1.0/tests/test_llama_ish.py +57 -0
- backcountry-0.1.0/tests/test_log.py +23 -0
- backcountry-0.1.0/tests/test_loop.py +789 -0
- backcountry-0.1.0/tests/test_manifest.py +381 -0
- backcountry-0.1.0/tests/test_measure.py +454 -0
- backcountry-0.1.0/tests/test_mixed_workloads.py +140 -0
- backcountry-0.1.0/tests/test_model_loading.py +158 -0
- backcountry-0.1.0/tests/test_multi_shape_bind.py +160 -0
- backcountry-0.1.0/tests/test_native_fusion.py +190 -0
- backcountry-0.1.0/tests/test_native_search.py +289 -0
- backcountry-0.1.0/tests/test_nested_bind.py +251 -0
- backcountry-0.1.0/tests/test_numeric_contract.py +185 -0
- backcountry-0.1.0/tests/test_output_diagnostics.py +65 -0
- backcountry-0.1.0/tests/test_output_layout.py +64 -0
- backcountry-0.1.0/tests/test_platform_env_and_compile.py +461 -0
- backcountry-0.1.0/tests/test_platform_metal_kernel.py +195 -0
- backcountry-0.1.0/tests/test_platform_msl_tree.py +112 -0
- backcountry-0.1.0/tests/test_platform_patching.py +270 -0
- backcountry-0.1.0/tests/test_platform_retention.py +122 -0
- backcountry-0.1.0/tests/test_platform_swap.py +214 -0
- backcountry-0.1.0/tests/test_prepared_replay.py +50 -0
- backcountry-0.1.0/tests/test_recurrent_cache.py +228 -0
- backcountry-0.1.0/tests/test_reference_starter.py +239 -0
- backcountry-0.1.0/tests/test_region_boundaries.py +235 -0
- backcountry-0.1.0/tests/test_region_selection.py +445 -0
- backcountry-0.1.0/tests/test_regions.py +699 -0
- backcountry-0.1.0/tests/test_regressions.py +530 -0
- backcountry-0.1.0/tests/test_report.py +60 -0
- backcountry-0.1.0/tests/test_resume.py +37 -0
- backcountry-0.1.0/tests/test_rope_offset.py +154 -0
- backcountry-0.1.0/tests/test_run_efficiency.py +301 -0
- backcountry-0.1.0/tests/test_run_recovery.py +561 -0
- backcountry-0.1.0/tests/test_runtime.py +136 -0
- backcountry-0.1.0/tests/test_sandbox.py +202 -0
- backcountry-0.1.0/tests/test_scaffold.py +936 -0
- backcountry-0.1.0/tests/test_scaffold_model_fallback.py +130 -0
- backcountry-0.1.0/tests/test_scaffold_reductions.py +136 -0
- backcountry-0.1.0/tests/test_search_experience.py +209 -0
- backcountry-0.1.0/tests/test_search_feedback.py +149 -0
- backcountry-0.1.0/tests/test_selection_flow.py +161 -0
- backcountry-0.1.0/tests/test_sequences.py +178 -0
- backcountry-0.1.0/tests/test_serve.py +868 -0
- backcountry-0.1.0/tests/test_sibling_bind.py +104 -0
- backcountry-0.1.0/tests/test_size_stretch.py +116 -0
- backcountry-0.1.0/tests/test_sizes.py +163 -0
- backcountry-0.1.0/tests/test_source_context.py +156 -0
- backcountry-0.1.0/tests/test_stages.py +308 -0
- backcountry-0.1.0/tests/test_state_calls.py +334 -0
- backcountry-0.1.0/tests/test_state_correctness.py +88 -0
- backcountry-0.1.0/tests/test_static_checks.py +74 -0
- backcountry-0.1.0/tests/test_stitch.py +551 -0
- backcountry-0.1.0/tests/test_through_the_run.py +144 -0
- backcountry-0.1.0/tests/test_tracer.py +523 -0
- backcountry-0.1.0/tests/test_tune.py +384 -0
- backcountry-0.1.0/tests/test_watch.py +197 -0
- backcountry-0.1.0/tests/test_widening.py +300 -0
- backcountry-0.1.0/tests/test_worker_signature_guard.py +51 -0
- backcountry-0.1.0/tests/test_worker_watchdog.py +256 -0
- backcountry-0.1.0/tests/test_workload.py +200 -0
- backcountry-0.1.0/tests/test_workload_acceptance.py +277 -0
- backcountry-0.1.0/tests/test_workload_scoring_gpu.py +115 -0
- backcountry-0.1.0/tests/test_workload_targeting.py +211 -0
- backcountry-0.1.0/tools/build_graph_native.py +100 -0
- backcountry-0.1.0/tools/build_text_pool.py +90 -0
- backcountry-0.1.0/uv.lock +1658 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shivam Garg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: backcountry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Makes MLX models faster on Apple Silicon without changing their answers: an AI proposes Metal kernels and faster loops, and a harness keeps only verified wins
|
|
5
|
+
Project-URL: Homepage, https://github.com/shivamg05/backcountry
|
|
6
|
+
Project-URL: Repository, https://github.com/shivamg05/backcountry
|
|
7
|
+
Project-URL: Issues, https://github.com/shivamg05/backcountry/issues
|
|
8
|
+
Author: Shivam Garg
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: apple-silicon,inference,kernels,llm,metal,mlx,optimization
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: <3.15,>=3.12
|
|
20
|
+
Requires-Dist: anthropic>=0.40
|
|
21
|
+
Requires-Dist: cmake>=3.27
|
|
22
|
+
Requires-Dist: mlx==0.32.4
|
|
23
|
+
Requires-Dist: nanobind==3.1.0
|
|
24
|
+
Requires-Dist: numpy>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# backcountry
|
|
29
|
+
|
|
30
|
+
Make an MLX model faster on Apple Silicon without changing its answers.
|
|
31
|
+
|
|
32
|
+
You mark the function your app calls for one request. An AI proposes faster
|
|
33
|
+
versions: custom Metal GPU code (kernels) for parts of the model and, if you
|
|
34
|
+
allow it, a faster way to use the model (a reply drafted several tokens at a
|
|
35
|
+
time and checked). A separate harness tests every one, and keeps it only if
|
|
36
|
+
the outputs still match the original (bit for bit, or within a tolerance you
|
|
37
|
+
control when the math is reordered) and the **whole request** gets measurably
|
|
38
|
+
faster. Whatever survives is saved beside your code, and your function uses it
|
|
39
|
+
from its next call.
|
|
40
|
+
|
|
41
|
+
Sometimes nothing survives. That's a real answer, not a crash: no candidate
|
|
42
|
+
beat your code by a margin the tool could confirm within its attempt budget.
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
You need an Apple Silicon Mac and Python 3.12, 3.13 or 3.14.
|
|
47
|
+
|
|
48
|
+
1. **Install [Claude Code](https://code.claude.com/docs/en/setup)**, run
|
|
49
|
+
`claude` once, sign in, and exit. Claude Code is the AI that writes the
|
|
50
|
+
kernels, so your account needs Claude Code access. A signed-in Codex or
|
|
51
|
+
Gemini CLI works too, or the Anthropic API with `ANTHROPIC_API_KEY`.
|
|
52
|
+
2. **Install backcountry** in the environment your app runs in:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install backcountry # or: uv add backcountry
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. **Mark the function** your app calls for one request, the one that turns
|
|
59
|
+
an input into an output (writes a reply, makes an image, classifies a
|
|
60
|
+
batch), with two lines:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import backcountry
|
|
64
|
+
|
|
65
|
+
@backcountry.tune
|
|
66
|
+
def generate_text(model, tokenizer, prompt, max_tokens=150): # your function, unchanged
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
4. **Run four commands** from your project's folder:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
backcountry init app.py # runs app.py once; asks the sizes to optimize
|
|
74
|
+
backcountry check backcountry.yaml # minutes, no attempts: does it run, fit, have room?
|
|
75
|
+
backcountry start backcountry.yaml # the search, in the background
|
|
76
|
+
backcountry status --wait # returns at each milestone
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A few things to know while it runs:
|
|
80
|
+
|
|
81
|
+
- **Expect it to take hours.** Each attempt is checked and timed, and the
|
|
82
|
+
budget counts attempts, not minutes. `start` runs in the background, so you
|
|
83
|
+
can close the terminal.
|
|
84
|
+
- **Keep other GPU work quiet.** Background load makes the timings noisy.
|
|
85
|
+
- **Kernel requests use your AI provider's allowance** (subscription or API
|
|
86
|
+
credits).
|
|
87
|
+
|
|
88
|
+
When it finishes, the result is a folder beside the file that defines your
|
|
89
|
+
function, `backcountry-results/<function>/`. Commit it, or ship it with your
|
|
90
|
+
code: the function uses it from its next call, and says so once.
|
|
91
|
+
|
|
92
|
+
## Run it through an AI agent
|
|
93
|
+
|
|
94
|
+
Don't want to answer questions and watch logs? Install backcountry, then paste
|
|
95
|
+
this into a coding agent (Claude Code, Codex, …) opened on your project:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
Run `backcountry guide` and follow it to make my app faster.
|
|
99
|
+
Ask me what you need, and update me only at milestones.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`backcountry guide` prints the steps for the installed version: where to put
|
|
103
|
+
the mark, which questions to ask you, how to start and watch the run. There
|
|
104
|
+
are two AIs here, and they can come from different providers: the agent you're
|
|
105
|
+
chatting with runs and watches the job; the one named by `--judge` writes the
|
|
106
|
+
kernels.
|
|
107
|
+
|
|
108
|
+
## What init asks
|
|
109
|
+
|
|
110
|
+
The function can do whatever one request does in your app: tokenize, run
|
|
111
|
+
your own loop with your own cache, sample, stop at an end token, decode,
|
|
112
|
+
post-process, print. `init` runs `app.py` as you would (`python app.py`) and
|
|
113
|
+
records each call of the marked function and the model it uses (passed in,
|
|
114
|
+
held by an object it is given, or a global, even one a library keeps for
|
|
115
|
+
itself). Then it tries each argument of the first call at another size and
|
|
116
|
+
watches what the model does, so it asks only about the ones that change the
|
|
117
|
+
work:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
These change how much work the model does:
|
|
121
|
+
1. width and height (together)
|
|
122
|
+
These run the model more or fewer times (one value, used for every size):
|
|
123
|
+
2. steps
|
|
124
|
+
These don't change the work:
|
|
125
|
+
prompt, seed
|
|
126
|
+
Sizes for width and height (together): a list like 512x512, 1024x768 [default: 1024x1024]: 512x512, 1024x1024
|
|
127
|
+
steps: the value to use for every size [default: 4]:
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The same questions come for any model: a prompt's length in tokens, a batch's
|
|
131
|
+
count, an array's axis, an image's width and height, any number argument.
|
|
132
|
+
Each question's default is the sizes your app called it with (here the
|
|
133
|
+
script made one 1024x1024 image), so pressing Enter optimizes what the app
|
|
134
|
+
actually runs. Answer with a list (`1, 8, 32`), a range (`32-4096`, spread into five sizes)
|
|
135
|
+
or pairs (`512x512, 1024x768`); at most eight sizes in all, since each is
|
|
136
|
+
checked and timed. `init` makes the call at every size once before writing
|
|
137
|
+
anything; agents pass the answers by number (`--sizes "1=512x512, 1024x1024; 2=4"`).
|
|
138
|
+
The search replays your function at each size, from the arguments your app
|
|
139
|
+
passed, repeated or cut to the size (never made-up data); it compares what the
|
|
140
|
+
function returns (arrays, numbers, text by its bytes, images) and times the
|
|
141
|
+
whole call, so the speedup it reports is your request's. Random draws are
|
|
142
|
+
seeded the same for every replay, and what the function prints is muted.
|
|
143
|
+
|
|
144
|
+
`init` also asks whether the search may rewrite the function itself
|
|
145
|
+
(`same-output`, the default). The AI then first proposes new versions of it,
|
|
146
|
+
with the same signature, that use the model differently: a loop that does
|
|
147
|
+
less work, a reply drafted several tokens at a time and checked. One ships
|
|
148
|
+
only if everything it returns matches the original's on fresh inputs and the
|
|
149
|
+
whole call is faster. The kernel search follows. `same-math` keeps your
|
|
150
|
+
function exactly as written and only makes the model's math faster.
|
|
151
|
+
|
|
152
|
+
A serving engine is marked the same way, whatever engine it is: mark the
|
|
153
|
+
function that handles one request (an engine's generate call, a server's route
|
|
154
|
+
handler). The engine may run the model on worker threads of its own, on a
|
|
155
|
+
stream of its own, and keep the model on its own objects (`engine.model`
|
|
156
|
+
beside your global): the search replays the request the same way, puts its
|
|
157
|
+
copies of the model everywhere the app holds it, and may rewrite the handler
|
|
158
|
+
and the code it calls.
|
|
159
|
+
|
|
160
|
+
An app that makes no request itself is asked for one. When the script ends
|
|
161
|
+
without calling the marked function, `init` asks for each of its arguments
|
|
162
|
+
(text, a number, a list, or a file beside the app: an image, a `.json`
|
|
163
|
+
conversation, a `.txt`); an argument named like one of the app's globals (its
|
|
164
|
+
`model`, its `tokenizer`) is taken from there. A server never ends: put its
|
|
165
|
+
serving loop under `if __name__ == "__main__":` and run
|
|
166
|
+
`backcountry init app.py --ask`, which loads the app without that block and
|
|
167
|
+
asks. One typed request is enough: the size questions repeat or cut it to
|
|
168
|
+
each size. An agent reads the questions with `--questions` and answers with
|
|
169
|
+
`--request "question=What is MLX?; history=chat.json"`.
|
|
170
|
+
|
|
171
|
+
What the app must do: run an MLX model (a PyTorch script is told so before it
|
|
172
|
+
runs), make the same calls each time it runs (`init` and the search rerun it),
|
|
173
|
+
and give the same output, doing the same work, for the same input (seeded randomness is fine; a serving engine's prompt cache, which skips reading a request it has just read, is not: `check` says so). The
|
|
174
|
+
app's own arguments go after `--` (`backcountry init app.py -- --image cat.png`).
|
|
175
|
+
|
|
176
|
+
`backcountry.yaml` holds what you chose: the sizes (one line each), a request
|
|
177
|
+
you typed, whether the function may be rewritten, the attempts. Edit it and
|
|
178
|
+
the next `check` or `start` uses it. Its paths are read from its own folder, so
|
|
179
|
+
a project copied or cloned to another Mac runs as it is: commit
|
|
180
|
+
`backcountry.yaml` and `.backcountry/backcountry_model.py` with your code (the
|
|
181
|
+
rest of `.backcountry/`, this machine's recording, is git-ignored). `check`
|
|
182
|
+
says how much headroom the hardware leaves at each size ("up to 14%
|
|
183
|
+
headroom"; under 5%: "may not be worth a run"), what fits in memory, and that
|
|
184
|
+
the AI is signed in.
|
|
185
|
+
|
|
186
|
+
## While it runs
|
|
187
|
+
|
|
188
|
+
The commands below act on the newest run under `runs/` in your project;
|
|
189
|
+
`--work-dir` names another.
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
backcountry status # one screen; --json for scripts
|
|
193
|
+
backcountry status --wait # returns at the next milestone: a kernel shipped, a region
|
|
194
|
+
# opened or closed, a pause or crash, the final result
|
|
195
|
+
backcountry finish # stop new attempts; still runs the final checks and saves the result
|
|
196
|
+
backcountry skip # leave a part of the model that isn't going anywhere
|
|
197
|
+
backcountry pause # need the Mac back (sleep, restart, other GPU work)?
|
|
198
|
+
backcountry resume # pick it up where it stopped
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`--attempts N` (at `init`, or in `backcountry.yaml`) is how many proposals the
|
|
202
|
+
search may try (up to 25 on any one part of the model). There is no time
|
|
203
|
+
limit: an attempt takes minutes on a small model and up to half an hour on a
|
|
204
|
+
large one, and the run ends when its attempts are spent (or at
|
|
205
|
+
`backcountry finish`). After the search, each kernel is checked at every size
|
|
206
|
+
in the range, then timed and saved: that is extra time. The result says how
|
|
207
|
+
many attempts it rests on and how long each part took.
|
|
208
|
+
|
|
209
|
+
Each run's folder holds `report.json` (every measurement, attempt and failure,
|
|
210
|
+
plus the final outcome), `run.jsonl` and `candidates.log` (progress and a
|
|
211
|
+
result for each attempt). The final summary says which of three things
|
|
212
|
+
happened: a verified result, no confirmed improvement, or a failure. A kernel
|
|
213
|
+
that is fast on its own doesn't count; only a confirmed whole-request speedup
|
|
214
|
+
ships.
|
|
215
|
+
|
|
216
|
+
## Using the result
|
|
217
|
+
|
|
218
|
+
A marked function needs nothing more: its first call installs the kernels
|
|
219
|
+
into the model it uses (and, when the search found a faster version of the
|
|
220
|
+
function, calls that from then on). A faster version is made for the
|
|
221
|
+
function's code as the run saw it: once you edit the function, the app runs
|
|
222
|
+
your edited code (it says so; the kernels still apply) until a new run. To run
|
|
223
|
+
it untouched, set `BACKCOUNTRY_OFF=1`, or keep the mark with
|
|
224
|
+
`@backcountry.tune(apply=False)`; to remove the result, delete its folder.
|
|
225
|
+
|
|
226
|
+
Elsewhere, install a result into a model you hold:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from backcountry import apply
|
|
230
|
+
model = apply(model) # always use the returned model
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Everything after that line (generation, batching, caches) stays as it was.
|
|
234
|
+
A finished run also copies its result to `~/.backcountry/artifacts/` (set
|
|
235
|
+
`BACKCOUNTRY_HOME` to move it), and `apply(model)` uses the newest one there
|
|
236
|
+
whose layers your model has; a model none fits runs as it is, with a warning.
|
|
237
|
+
To use a particular result, name its folder: `apply(model, "path/to/artifact")`.
|
|
238
|
+
|
|
239
|
+
**See it yourself, through your own code.** `compare` calls your function with
|
|
240
|
+
the model as loaded and with the optimization switched in, alternately, on one
|
|
241
|
+
copy of the weights, and tells you whether the outputs are identical and how
|
|
242
|
+
much faster it is:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from backcountry import compare
|
|
246
|
+
print(compare(lambda m: generate(m, tokenizer, prompt=my_prompt, max_tokens=100), model))
|
|
247
|
+
# 48 layers replaced; outputs identical; 1.124x faster (as loaded 1.503 s, optimized 1.337 s, ...)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Keep in mind:
|
|
251
|
+
|
|
252
|
+
- **Weights aren't included.** You load them yourself as usual. The model's
|
|
253
|
+
architecture and quantization have to match what was optimized, but the
|
|
254
|
+
weight values can differ.
|
|
255
|
+
- **Only tested input sizes get the new kernels.** Anything else quietly runs
|
|
256
|
+
the original code, so it's still correct, just not faster.
|
|
257
|
+
- **Each layer is checked first.** A layer that isn't the one the kernels were
|
|
258
|
+
made for (another model, quantization or MLX version) keeps its own
|
|
259
|
+
code, with a warning.
|
|
260
|
+
- **Speed is measured on the run's chip.** On another Mac the outputs are the
|
|
261
|
+
same, and `apply()` warns that the speed there is unmeasured.
|
|
262
|
+
|
|
263
|
+
More in [the result guide](https://github.com/shivamg05/backcountry/blob/main/docs/artifacts.md).
|
|
264
|
+
|
|
265
|
+
## Other ways to run it
|
|
266
|
+
|
|
267
|
+
A language model MLX-LM loads can be named directly, with no app:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
backcountry init mlx-community/Qwen3-8B-4bit
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`init` then asks whether prompts, replies or both should get faster, the
|
|
274
|
+
sizes, whether tokens may come from a faster generation loop (`same-output`)
|
|
275
|
+
or only faster kernels (`same-math`), and how many attempts to make. Use the
|
|
276
|
+
result through MLX-LM's own functions, imported from backcountry: they take
|
|
277
|
+
the same arguments and give the same results, and use the loop stored for the
|
|
278
|
+
model you pass (any other model runs MLX-LM's own):
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from backcountry import apply, generate # or stream_generate, generate_step
|
|
282
|
+
model = apply(model)
|
|
283
|
+
text = generate(model, tokenizer, prompt, max_tokens=256)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
For full control (several kinds of call, your own limits, a size range), write
|
|
287
|
+
a model file and a manifest yourself: see the [usage guide](https://github.com/shivamg05/backcountry/blob/main/docs/usage.md) and
|
|
288
|
+
[write a manifest](https://github.com/shivamg05/backcountry/blob/main/docs/manifest.md). Any supported AI can write the kernels:
|
|
289
|
+
`--judge codex --model YOUR_MODEL_ID`, for example.
|
|
290
|
+
|
|
291
|
+
## Further reading
|
|
292
|
+
|
|
293
|
+
- [Limitations](https://github.com/shivamg05/backcountry/blob/main/docs/limitations.md): what a result does and doesn't promise.
|
|
294
|
+
Worth reading before you quote a number.
|
|
295
|
+
- [RUNNING.md](https://github.com/shivamg05/backcountry/blob/main/RUNNING.md): where an agent running a job for you should start.
|
|
296
|
+
- [Architecture](https://github.com/shivamg05/backcountry/blob/main/docs/architecture.md): how the harness works and the rules it never breaks.
|
|
297
|
+
- [Judge protocol](https://github.com/shivamg05/backcountry/blob/main/docs/judge-protocol.md): how to plug in another AI provider,
|
|
298
|
+
or answer kernel requests by hand.
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# backcountry
|
|
2
|
+
|
|
3
|
+
Make an MLX model faster on Apple Silicon without changing its answers.
|
|
4
|
+
|
|
5
|
+
You mark the function your app calls for one request. An AI proposes faster
|
|
6
|
+
versions: custom Metal GPU code (kernels) for parts of the model and, if you
|
|
7
|
+
allow it, a faster way to use the model (a reply drafted several tokens at a
|
|
8
|
+
time and checked). A separate harness tests every one, and keeps it only if
|
|
9
|
+
the outputs still match the original (bit for bit, or within a tolerance you
|
|
10
|
+
control when the math is reordered) and the **whole request** gets measurably
|
|
11
|
+
faster. Whatever survives is saved beside your code, and your function uses it
|
|
12
|
+
from its next call.
|
|
13
|
+
|
|
14
|
+
Sometimes nothing survives. That's a real answer, not a crash: no candidate
|
|
15
|
+
beat your code by a margin the tool could confirm within its attempt budget.
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
You need an Apple Silicon Mac and Python 3.12, 3.13 or 3.14.
|
|
20
|
+
|
|
21
|
+
1. **Install [Claude Code](https://code.claude.com/docs/en/setup)**, run
|
|
22
|
+
`claude` once, sign in, and exit. Claude Code is the AI that writes the
|
|
23
|
+
kernels, so your account needs Claude Code access. A signed-in Codex or
|
|
24
|
+
Gemini CLI works too, or the Anthropic API with `ANTHROPIC_API_KEY`.
|
|
25
|
+
2. **Install backcountry** in the environment your app runs in:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install backcountry # or: uv add backcountry
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. **Mark the function** your app calls for one request, the one that turns
|
|
32
|
+
an input into an output (writes a reply, makes an image, classifies a
|
|
33
|
+
batch), with two lines:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import backcountry
|
|
37
|
+
|
|
38
|
+
@backcountry.tune
|
|
39
|
+
def generate_text(model, tokenizer, prompt, max_tokens=150): # your function, unchanged
|
|
40
|
+
...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
4. **Run four commands** from your project's folder:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
backcountry init app.py # runs app.py once; asks the sizes to optimize
|
|
47
|
+
backcountry check backcountry.yaml # minutes, no attempts: does it run, fit, have room?
|
|
48
|
+
backcountry start backcountry.yaml # the search, in the background
|
|
49
|
+
backcountry status --wait # returns at each milestone
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A few things to know while it runs:
|
|
53
|
+
|
|
54
|
+
- **Expect it to take hours.** Each attempt is checked and timed, and the
|
|
55
|
+
budget counts attempts, not minutes. `start` runs in the background, so you
|
|
56
|
+
can close the terminal.
|
|
57
|
+
- **Keep other GPU work quiet.** Background load makes the timings noisy.
|
|
58
|
+
- **Kernel requests use your AI provider's allowance** (subscription or API
|
|
59
|
+
credits).
|
|
60
|
+
|
|
61
|
+
When it finishes, the result is a folder beside the file that defines your
|
|
62
|
+
function, `backcountry-results/<function>/`. Commit it, or ship it with your
|
|
63
|
+
code: the function uses it from its next call, and says so once.
|
|
64
|
+
|
|
65
|
+
## Run it through an AI agent
|
|
66
|
+
|
|
67
|
+
Don't want to answer questions and watch logs? Install backcountry, then paste
|
|
68
|
+
this into a coding agent (Claude Code, Codex, …) opened on your project:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Run `backcountry guide` and follow it to make my app faster.
|
|
72
|
+
Ask me what you need, and update me only at milestones.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`backcountry guide` prints the steps for the installed version: where to put
|
|
76
|
+
the mark, which questions to ask you, how to start and watch the run. There
|
|
77
|
+
are two AIs here, and they can come from different providers: the agent you're
|
|
78
|
+
chatting with runs and watches the job; the one named by `--judge` writes the
|
|
79
|
+
kernels.
|
|
80
|
+
|
|
81
|
+
## What init asks
|
|
82
|
+
|
|
83
|
+
The function can do whatever one request does in your app: tokenize, run
|
|
84
|
+
your own loop with your own cache, sample, stop at an end token, decode,
|
|
85
|
+
post-process, print. `init` runs `app.py` as you would (`python app.py`) and
|
|
86
|
+
records each call of the marked function and the model it uses (passed in,
|
|
87
|
+
held by an object it is given, or a global, even one a library keeps for
|
|
88
|
+
itself). Then it tries each argument of the first call at another size and
|
|
89
|
+
watches what the model does, so it asks only about the ones that change the
|
|
90
|
+
work:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
These change how much work the model does:
|
|
94
|
+
1. width and height (together)
|
|
95
|
+
These run the model more or fewer times (one value, used for every size):
|
|
96
|
+
2. steps
|
|
97
|
+
These don't change the work:
|
|
98
|
+
prompt, seed
|
|
99
|
+
Sizes for width and height (together): a list like 512x512, 1024x768 [default: 1024x1024]: 512x512, 1024x1024
|
|
100
|
+
steps: the value to use for every size [default: 4]:
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The same questions come for any model: a prompt's length in tokens, a batch's
|
|
104
|
+
count, an array's axis, an image's width and height, any number argument.
|
|
105
|
+
Each question's default is the sizes your app called it with (here the
|
|
106
|
+
script made one 1024x1024 image), so pressing Enter optimizes what the app
|
|
107
|
+
actually runs. Answer with a list (`1, 8, 32`), a range (`32-4096`, spread into five sizes)
|
|
108
|
+
or pairs (`512x512, 1024x768`); at most eight sizes in all, since each is
|
|
109
|
+
checked and timed. `init` makes the call at every size once before writing
|
|
110
|
+
anything; agents pass the answers by number (`--sizes "1=512x512, 1024x1024; 2=4"`).
|
|
111
|
+
The search replays your function at each size, from the arguments your app
|
|
112
|
+
passed, repeated or cut to the size (never made-up data); it compares what the
|
|
113
|
+
function returns (arrays, numbers, text by its bytes, images) and times the
|
|
114
|
+
whole call, so the speedup it reports is your request's. Random draws are
|
|
115
|
+
seeded the same for every replay, and what the function prints is muted.
|
|
116
|
+
|
|
117
|
+
`init` also asks whether the search may rewrite the function itself
|
|
118
|
+
(`same-output`, the default). The AI then first proposes new versions of it,
|
|
119
|
+
with the same signature, that use the model differently: a loop that does
|
|
120
|
+
less work, a reply drafted several tokens at a time and checked. One ships
|
|
121
|
+
only if everything it returns matches the original's on fresh inputs and the
|
|
122
|
+
whole call is faster. The kernel search follows. `same-math` keeps your
|
|
123
|
+
function exactly as written and only makes the model's math faster.
|
|
124
|
+
|
|
125
|
+
A serving engine is marked the same way, whatever engine it is: mark the
|
|
126
|
+
function that handles one request (an engine's generate call, a server's route
|
|
127
|
+
handler). The engine may run the model on worker threads of its own, on a
|
|
128
|
+
stream of its own, and keep the model on its own objects (`engine.model`
|
|
129
|
+
beside your global): the search replays the request the same way, puts its
|
|
130
|
+
copies of the model everywhere the app holds it, and may rewrite the handler
|
|
131
|
+
and the code it calls.
|
|
132
|
+
|
|
133
|
+
An app that makes no request itself is asked for one. When the script ends
|
|
134
|
+
without calling the marked function, `init` asks for each of its arguments
|
|
135
|
+
(text, a number, a list, or a file beside the app: an image, a `.json`
|
|
136
|
+
conversation, a `.txt`); an argument named like one of the app's globals (its
|
|
137
|
+
`model`, its `tokenizer`) is taken from there. A server never ends: put its
|
|
138
|
+
serving loop under `if __name__ == "__main__":` and run
|
|
139
|
+
`backcountry init app.py --ask`, which loads the app without that block and
|
|
140
|
+
asks. One typed request is enough: the size questions repeat or cut it to
|
|
141
|
+
each size. An agent reads the questions with `--questions` and answers with
|
|
142
|
+
`--request "question=What is MLX?; history=chat.json"`.
|
|
143
|
+
|
|
144
|
+
What the app must do: run an MLX model (a PyTorch script is told so before it
|
|
145
|
+
runs), make the same calls each time it runs (`init` and the search rerun it),
|
|
146
|
+
and give the same output, doing the same work, for the same input (seeded randomness is fine; a serving engine's prompt cache, which skips reading a request it has just read, is not: `check` says so). The
|
|
147
|
+
app's own arguments go after `--` (`backcountry init app.py -- --image cat.png`).
|
|
148
|
+
|
|
149
|
+
`backcountry.yaml` holds what you chose: the sizes (one line each), a request
|
|
150
|
+
you typed, whether the function may be rewritten, the attempts. Edit it and
|
|
151
|
+
the next `check` or `start` uses it. Its paths are read from its own folder, so
|
|
152
|
+
a project copied or cloned to another Mac runs as it is: commit
|
|
153
|
+
`backcountry.yaml` and `.backcountry/backcountry_model.py` with your code (the
|
|
154
|
+
rest of `.backcountry/`, this machine's recording, is git-ignored). `check`
|
|
155
|
+
says how much headroom the hardware leaves at each size ("up to 14%
|
|
156
|
+
headroom"; under 5%: "may not be worth a run"), what fits in memory, and that
|
|
157
|
+
the AI is signed in.
|
|
158
|
+
|
|
159
|
+
## While it runs
|
|
160
|
+
|
|
161
|
+
The commands below act on the newest run under `runs/` in your project;
|
|
162
|
+
`--work-dir` names another.
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
backcountry status # one screen; --json for scripts
|
|
166
|
+
backcountry status --wait # returns at the next milestone: a kernel shipped, a region
|
|
167
|
+
# opened or closed, a pause or crash, the final result
|
|
168
|
+
backcountry finish # stop new attempts; still runs the final checks and saves the result
|
|
169
|
+
backcountry skip # leave a part of the model that isn't going anywhere
|
|
170
|
+
backcountry pause # need the Mac back (sleep, restart, other GPU work)?
|
|
171
|
+
backcountry resume # pick it up where it stopped
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--attempts N` (at `init`, or in `backcountry.yaml`) is how many proposals the
|
|
175
|
+
search may try (up to 25 on any one part of the model). There is no time
|
|
176
|
+
limit: an attempt takes minutes on a small model and up to half an hour on a
|
|
177
|
+
large one, and the run ends when its attempts are spent (or at
|
|
178
|
+
`backcountry finish`). After the search, each kernel is checked at every size
|
|
179
|
+
in the range, then timed and saved: that is extra time. The result says how
|
|
180
|
+
many attempts it rests on and how long each part took.
|
|
181
|
+
|
|
182
|
+
Each run's folder holds `report.json` (every measurement, attempt and failure,
|
|
183
|
+
plus the final outcome), `run.jsonl` and `candidates.log` (progress and a
|
|
184
|
+
result for each attempt). The final summary says which of three things
|
|
185
|
+
happened: a verified result, no confirmed improvement, or a failure. A kernel
|
|
186
|
+
that is fast on its own doesn't count; only a confirmed whole-request speedup
|
|
187
|
+
ships.
|
|
188
|
+
|
|
189
|
+
## Using the result
|
|
190
|
+
|
|
191
|
+
A marked function needs nothing more: its first call installs the kernels
|
|
192
|
+
into the model it uses (and, when the search found a faster version of the
|
|
193
|
+
function, calls that from then on). A faster version is made for the
|
|
194
|
+
function's code as the run saw it: once you edit the function, the app runs
|
|
195
|
+
your edited code (it says so; the kernels still apply) until a new run. To run
|
|
196
|
+
it untouched, set `BACKCOUNTRY_OFF=1`, or keep the mark with
|
|
197
|
+
`@backcountry.tune(apply=False)`; to remove the result, delete its folder.
|
|
198
|
+
|
|
199
|
+
Elsewhere, install a result into a model you hold:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from backcountry import apply
|
|
203
|
+
model = apply(model) # always use the returned model
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Everything after that line (generation, batching, caches) stays as it was.
|
|
207
|
+
A finished run also copies its result to `~/.backcountry/artifacts/` (set
|
|
208
|
+
`BACKCOUNTRY_HOME` to move it), and `apply(model)` uses the newest one there
|
|
209
|
+
whose layers your model has; a model none fits runs as it is, with a warning.
|
|
210
|
+
To use a particular result, name its folder: `apply(model, "path/to/artifact")`.
|
|
211
|
+
|
|
212
|
+
**See it yourself, through your own code.** `compare` calls your function with
|
|
213
|
+
the model as loaded and with the optimization switched in, alternately, on one
|
|
214
|
+
copy of the weights, and tells you whether the outputs are identical and how
|
|
215
|
+
much faster it is:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from backcountry import compare
|
|
219
|
+
print(compare(lambda m: generate(m, tokenizer, prompt=my_prompt, max_tokens=100), model))
|
|
220
|
+
# 48 layers replaced; outputs identical; 1.124x faster (as loaded 1.503 s, optimized 1.337 s, ...)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Keep in mind:
|
|
224
|
+
|
|
225
|
+
- **Weights aren't included.** You load them yourself as usual. The model's
|
|
226
|
+
architecture and quantization have to match what was optimized, but the
|
|
227
|
+
weight values can differ.
|
|
228
|
+
- **Only tested input sizes get the new kernels.** Anything else quietly runs
|
|
229
|
+
the original code, so it's still correct, just not faster.
|
|
230
|
+
- **Each layer is checked first.** A layer that isn't the one the kernels were
|
|
231
|
+
made for (another model, quantization or MLX version) keeps its own
|
|
232
|
+
code, with a warning.
|
|
233
|
+
- **Speed is measured on the run's chip.** On another Mac the outputs are the
|
|
234
|
+
same, and `apply()` warns that the speed there is unmeasured.
|
|
235
|
+
|
|
236
|
+
More in [the result guide](https://github.com/shivamg05/backcountry/blob/main/docs/artifacts.md).
|
|
237
|
+
|
|
238
|
+
## Other ways to run it
|
|
239
|
+
|
|
240
|
+
A language model MLX-LM loads can be named directly, with no app:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
backcountry init mlx-community/Qwen3-8B-4bit
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`init` then asks whether prompts, replies or both should get faster, the
|
|
247
|
+
sizes, whether tokens may come from a faster generation loop (`same-output`)
|
|
248
|
+
or only faster kernels (`same-math`), and how many attempts to make. Use the
|
|
249
|
+
result through MLX-LM's own functions, imported from backcountry: they take
|
|
250
|
+
the same arguments and give the same results, and use the loop stored for the
|
|
251
|
+
model you pass (any other model runs MLX-LM's own):
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from backcountry import apply, generate # or stream_generate, generate_step
|
|
255
|
+
model = apply(model)
|
|
256
|
+
text = generate(model, tokenizer, prompt, max_tokens=256)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
For full control (several kinds of call, your own limits, a size range), write
|
|
260
|
+
a model file and a manifest yourself: see the [usage guide](https://github.com/shivamg05/backcountry/blob/main/docs/usage.md) and
|
|
261
|
+
[write a manifest](https://github.com/shivamg05/backcountry/blob/main/docs/manifest.md). Any supported AI can write the kernels:
|
|
262
|
+
`--judge codex --model YOUR_MODEL_ID`, for example.
|
|
263
|
+
|
|
264
|
+
## Further reading
|
|
265
|
+
|
|
266
|
+
- [Limitations](https://github.com/shivamg05/backcountry/blob/main/docs/limitations.md): what a result does and doesn't promise.
|
|
267
|
+
Worth reading before you quote a number.
|
|
268
|
+
- [RUNNING.md](https://github.com/shivamg05/backcountry/blob/main/RUNNING.md): where an agent running a job for you should start.
|
|
269
|
+
- [Architecture](https://github.com/shivamg05/backcountry/blob/main/docs/architecture.md): how the harness works and the rules it never breaks.
|
|
270
|
+
- [Judge protocol](https://github.com/shivamg05/backcountry/blob/main/docs/judge-protocol.md): how to plug in another AI provider,
|
|
271
|
+
or answer kernel requests by hand.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Run an optimization
|
|
2
|
+
|
|
3
|
+
Start with [README.md](README.md) for installation and the four commands.
|
|
4
|
+
Detailed model and manifest options are in [docs/usage.md](docs/usage.md).
|
|
5
|
+
|
|
6
|
+
If you are an agent operating a run for someone, read
|
|
7
|
+
[docs/operating.md](docs/operating.md) before starting. It defines preflight,
|
|
8
|
+
event-driven progress updates, failure reporting, and how to finish early
|
|
9
|
+
without losing final validation. Follow the requested manifest and judge;
|
|
10
|
+
do not inspect other runs unless asked.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
backcountry start path/to/manifest.yaml --judge claude-cli # in the background
|
|
14
|
+
backcountry status --wait # repeat: each call returns at the next milestone; relay it
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`start` keeps running if the chat session or terminal closes; `status` (add
|
|
18
|
+
`--json` for fields) works from any later session. `run` does the same in the
|
|
19
|
+
foreground.
|
|
20
|
+
|
|
21
|
+
The default output is a fresh timestamped folder under `runs/`. You can specify
|
|
22
|
+
`--work-dir runs/YOUR_LABEL` or another location. Never reuse a
|
|
23
|
+
nonempty directory or run competing GPU benchmarks alongside an optimization.
|
|
24
|
+
|
|
25
|
+
## Pause and resume
|
|
26
|
+
|
|
27
|
+
A run can stop and continue later, in a new process, where it left off:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
backcountry pause # the newest run; --work-dir names another
|
|
31
|
+
backcountry resume # later, from the same folder
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **Pause before the Mac sleeps, restarts or needs its GPU.** The job stops at
|
|
35
|
+
its next safe point, after the attempt it is on, with nothing left running
|
|
36
|
+
on the GPU, and exits with status `paused`. An attempt can take a while (on
|
|
37
|
+
FLUX, half take over 10 minutes and the longest took an hour). If the
|
|
38
|
+
operator can't wait, press Ctrl-C instead and resume later: only the attempt
|
|
39
|
+
in progress is lost, and it is tried again. A sleeping Mac can also drop the
|
|
40
|
+
job's network; don't rely on the job surviving a sleep.
|
|
41
|
+
- **Resume** reuses the manifest and judge settings the job started with
|
|
42
|
+
(`--judge`, `--model`, `--judge-effort`, `--judge-cmd` override them). It
|
|
43
|
+
records the model again (seconds), restores everything setup measured,
|
|
44
|
+
reinstalls the accepted kernels, checks the model's outputs still match the
|
|
45
|
+
original, and continues the same region with the same history and budget. It
|
|
46
|
+
refuses if the manifest changed, and warns if the tool's code changed.
|
|
47
|
+
- **The job pauses itself** when the judge doesn't answer three times in a
|
|
48
|
+
row (no network, an expired login, a reply over 30 minutes). Those failed calls don't use attempts.
|
|
49
|
+
Fix the cause, then resume.
|
|
50
|
+
- **Ctrl-C, a crash or a killed process** can be resumed too, from the last
|
|
51
|
+
safe point: at most the one attempt in progress is repeated. A win accepted
|
|
52
|
+
in that attempt is found again; its checkpoint folder stays, and the repeat
|
|
53
|
+
is saved beside it.
|
|
54
|
+
- **A finished job deletes its `boundaries/`** (the captured region inputs
|
|
55
|
+
only a search reads, often tens of GB). A paused, failed or killed job keeps
|
|
56
|
+
them so it can resume.
|
|
57
|
+
- **An export that fails after a passed final check** (a crash, a bug, out of
|
|
58
|
+
memory) resumes at the export: the search and the final check are not run
|
|
59
|
+
again.
|
|
60
|
+
- Runs started before pause and resume existed have no saved state and can't
|
|
61
|
+
be resumed.
|
|
62
|
+
|
|
63
|
+
After completion, report the confirmed whole-workload result and artifact path,
|
|
64
|
+
or explain why nothing shipped. See [artifact usage](docs/artifacts.md).
|