tensoracle 1.5.0__tar.gz → 1.6.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 (95) hide show
  1. {tensoracle-1.5.0 → tensoracle-1.6.0}/PKG-INFO +8 -4
  2. {tensoracle-1.5.0 → tensoracle-1.6.0}/README.md +7 -3
  3. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/index.md +4 -2
  4. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_build.py +11 -6
  5. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_cache.py +10 -3
  6. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_cli.py +1 -0
  7. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_version.py +2 -2
  8. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_build.py +31 -6
  9. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cache.py +2 -2
  10. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cache_cli.py +26 -2
  11. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cache_management.py +43 -2
  12. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_weight_hash.py +2 -1
  13. {tensoracle-1.5.0 → tensoracle-1.6.0}/.gitignore +0 -0
  14. {tensoracle-1.5.0 → tensoracle-1.6.0}/LICENSE +0 -0
  15. {tensoracle-1.5.0 → tensoracle-1.6.0}/build_backend.py +0 -0
  16. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/CMakeLists.txt +0 -0
  17. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/cmake/TensoracleCubinPlugin.cmake +0 -0
  18. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/cmake/TensoracleCuda.cmake +0 -0
  19. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/include/tensoracle/cubin_plugin.hpp +0 -0
  20. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/src/cubin_plugin.cpp +0 -0
  21. {tensoracle-1.5.0 → tensoracle-1.6.0}/cpp/tests/core.cpp +0 -0
  22. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/api/bundle.md +0 -0
  23. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/api/index.md +0 -0
  24. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/api/producer.md +0 -0
  25. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/cpp-python-repositories.md +0 -0
  26. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/optimization.md +0 -0
  27. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/precision.md +0 -0
  28. {tensoracle-1.5.0 → tensoracle-1.6.0}/docs/tensorrt-sdk.md +0 -0
  29. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/CMakeLists.txt +0 -0
  30. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/README.md +0 -0
  31. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/bundle.cpp +0 -0
  32. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/bundle.json +0 -0
  33. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm120/bias.cubin +0 -0
  34. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm120/dot.specialized.cubin +0 -0
  35. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm120/dot.tf32-control.cubin +0 -0
  36. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm120/dot.unspecialized.cubin +0 -0
  37. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm120/scale.cubin +0 -0
  38. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm86/bias.cubin +0 -0
  39. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm86/dot.specialized.cubin +0 -0
  40. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm86/dot.tf32-control.cubin +0 -0
  41. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm86/dot.unspecialized.cubin +0 -0
  42. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm86/scale.cubin +0 -0
  43. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm89/bias.cubin +0 -0
  44. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm89/dot.specialized.cubin +0 -0
  45. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm89/dot.tf32-control.cubin +0 -0
  46. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm89/dot.unspecialized.cubin +0 -0
  47. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/bundle/cubins/sm89/scale.cubin +0 -0
  48. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/example_cubin_plugin/__init__.py +0 -0
  49. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/example_cubin_plugin/weights.safetensors +0 -0
  50. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/kernels.py +0 -0
  51. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/plugin.cpp +0 -0
  52. {tensoracle-1.5.0 → tensoracle-1.6.0}/examples/cubin_plugin/pyproject.toml +0 -0
  53. {tensoracle-1.5.0 → tensoracle-1.6.0}/mkdocs.yml +0 -0
  54. {tensoracle-1.5.0 → tensoracle-1.6.0}/pyproject.toml +0 -0
  55. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/__init__.py +0 -0
  56. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_cache_format.py +0 -0
  57. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_comparison.py +0 -0
  58. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_execution.py +0 -0
  59. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_memory.py +0 -0
  60. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/_plugins.py +0 -0
  61. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/bundle.py +0 -0
  62. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/convert.py +0 -0
  63. {tensoracle-1.5.0 → tensoracle-1.6.0}/src/tensoracle/producer.py +0 -0
  64. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/build_memory_runtime.py +0 -0
  65. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubin_io/check.cpp +0 -0
  66. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubin_io/kernels.cu +0 -0
  67. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubin_io/plugin.cpp +0 -0
  68. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubins/ptxas-12.9.86/sm120.cubin +0 -0
  69. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubins/ptxas-12.9.86/sm86.cubin +0 -0
  70. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/cubins/ptxas-12.9.86/sm89.cubin +0 -0
  71. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/example_runtime.py +0 -0
  72. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/native/CMakeLists.txt +0 -0
  73. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/native/export.h +0 -0
  74. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/native_build.py +0 -0
  75. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/plugin_io_runtime.py +0 -0
  76. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/precision_runtime.py +0 -0
  77. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_build_memory.py +0 -0
  78. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_bundle.py +0 -0
  79. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cmake_cuda.py +0 -0
  80. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cmake_plugin.py +0 -0
  81. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_comparison.py +0 -0
  82. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_convert.py +0 -0
  83. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cpp_core.py +0 -0
  84. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_cuda_graphs.py +0 -0
  85. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_distribution.py +0 -0
  86. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_engine_io.py +0 -0
  87. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_example_plugin.py +0 -0
  88. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_execution.py +0 -0
  89. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_gpu_cache.py +0 -0
  90. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_plugin_io.py +0 -0
  91. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_plugins.py +0 -0
  92. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_precision.py +0 -0
  93. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_producer.py +0 -0
  94. {tensoracle-1.5.0 → tensoracle-1.6.0}/tests/test_shape_tensor_outputs.py +0 -0
  95. {tensoracle-1.5.0 → tensoracle-1.6.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tensoracle
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Build, save, reuse, and execute TensorRT engines from safetensors weights
5
5
  Project-URL: documentation, https://sync-dev-org.github.io/tensoracle/
6
6
  Project-URL: repository, https://github.com/sync-dev-org/tensoracle
@@ -110,7 +110,7 @@ def graph(network: trt.INetworkDefinition) -> None:
110
110
 
111
111
  declaration = EngineBuild(
112
112
  inputs={"features": InputShape.ranged((1, 1, 2), (1, 64, 2), (1, 448, 2))},
113
- settings=BuilderSettings(workspace_limit=1 << 30, optimization_level=3),
113
+ settings=BuilderSettings(workspace_limit=None, optimization_level=3),
114
114
  )
115
115
  plan: bytes = build_serialized_engine(graph, declaration)
116
116
  ```
@@ -119,6 +119,8 @@ Use `InputShape.fixed((1, 64, 2))` for one input size; the graph may declare tha
119
119
 
120
120
  The engine's precision is determined by the tensor types that the graph function places in the network. For an fp16 graph, use `trt.float16` tensors and `np.float16` weights where needed; the graph can also contain fp32 tensors. `EngineBuild` declares input shapes and builder settings. TF32 is always disabled; `BuilderSettings(tf32=True)` raises `ValueError`. The function returns serialized plan bytes. Invalid declarations raise `ValueError`, and a failed TensorRT build raises `RuntimeError`.
121
121
 
122
+ `BuilderSettings.workspace_limit` defaults to `None`, which leaves the workspace limit at TensorRT's default without calling `set_memory_pool_limit`. To impose a limit, pass a positive integer in bytes, such as `workspace_limit=4 << 30` for 4 GiB. Zero, negative values, booleans, and noninteger values raise `ValueError`.
123
+
122
124
  The same API builds bf16, int8, and fp8 graphs. Cast tensors to `trt.bfloat16` for bf16; for int8/fp8, place `add_quantize` / `add_dequantize` layers and load their scales from safetensors. The application determines scales with an external quantization library and chooses output quality limits with `compare_outputs`. fp8 targets sm89 and sm120; bf16 and int8 also target sm86. Follow [Build bf16, int8, and fp8 engines](https://github.com/sync-dev-org/tensoracle/blob/main/docs/precision.md) for a complete example, NumPy dtype constraints, and comparison against an fp32 engine. Use different engine names or graph versions for different precisions.
123
125
 
124
126
  ## Save and reopen an engine
@@ -144,7 +146,9 @@ handle = open_engine(
144
146
  engine = handle.engine # Keep handle.runtime alive while using the engine.
145
147
  ```
146
148
 
147
- Saved files live in the `tensoracle` directory under `XDG_CACHE_HOME`, or under the home directory's `.cache` when that variable is unset. The same rule applies on every OS; on Windows the home directory is `%USERPROFILE%`, so the default is `%USERPROFILE%\.cache\tensoracle`. The cache identity includes the contents of all declared safetensors weight files and optional `plugins` files, the engine name, graph version, input shapes, builder settings, the GPU's compatibility properties, OS, CPU architecture, and TensorRT version. The GPU is the device of the current CUDA context, or device 0 when no context is current. Its properties are the product name, compute capability, memory bus width, L2 cache size, shared memory limits, texture alignment, multiprocessor count, whether the GPU is integrated, ECC mode, and total memory; another device with the same properties reuses the saved engine. Raise `graph_version` when graph code changes; without a version change, the previous engine is reused. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. Include in `graph_version` any other caller value that changes the engine contents but is not part of the cache identity, such as a plugin manifest or a build recipe. Pass `force_rebuild=True` to replace a cached engine even when it is intact. tensoracle closes a saved engine file after reading it, so another process using an opened engine does not block replacement. On Windows, a process that keeps the saved file itself open prevents replacement or removal with `PermissionError`; after that process closes the file, a file left empty by the failed rebuild is rejected as `CorruptEngineError` and can be rebuilt with `force_rebuild=True`.
149
+ Saved files live in the `tensoracle` directory under `XDG_CACHE_HOME`, or under the home directory's `.cache` when that variable is unset. The same rule applies on every OS; on Windows the home directory is `%USERPROFILE%`, so the default is `%USERPROFILE%\.cache\tensoracle`. The cache identity includes the contents of all declared safetensors weight files and optional `plugins` files, the engine name, graph version, input shapes, the builder's `optimization_level` and `tf32` settings, the GPU's compatibility properties, OS, CPU architecture, and TensorRT version. The GPU is the device of the current CUDA context, or device 0 when no context is current. Its properties are the product name, compute capability, memory bus width, L2 cache size, shared memory limits, texture alignment, multiprocessor count, whether the GPU is integrated, ECC mode, and total memory; another device with the same properties reuses the saved engine. Raise `graph_version` when graph code changes; without a version change, the previous engine is reused. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. Include in `graph_version` any other caller value that changes the engine contents but is not part of the cache identity, such as a plugin manifest or a build recipe. Pass `force_rebuild=True` to replace a cached engine even when it is intact. tensoracle closes a saved engine file after reading it, so another process using an opened engine does not block replacement. On Windows, a process that keeps the saved file itself open prevents replacement or removal with `PermissionError`; after that process closes the file, a file left empty by the failed rebuild is rejected as `CorruptEngineError` and can be rebuilt with `force_rebuild=True`.
150
+
151
+ `workspace_limit` is excluded from the cache identity. Changing it reuses a matching saved engine; pass `force_rebuild=True` to `open_engine` to build again with the new limit. The saved header records the workspace limit used for that build, including `None`, as a reference outside the conditions used for matching. Cache reads leave this reference unchanged. `tensoracle cache list` shows the recorded limit.
148
152
 
149
153
  `plugins` names native TensorRT plugin libraries. Both `open_engine` and `build_serialized_engine` accept it. Each function checks that every library exports `getCreators`, loads it through TensorRT's registry, and retains it for the process before building the graph or deserializing an engine. Repeating the same library does not register it twice. `open_engine` also hashes the loaded library bytes into the engine identity; a changed library selects a different saved engine in a new process. A missing, unreadable, or incompatible library, or a library changed after loading in the same process, raises `PluginLibraryError` before a cached plan is opened. Keep the library and its cubin bundle together in the caller's wheel.
150
154
 
@@ -170,7 +174,7 @@ tensoracle cache prune --max-size 10GiB
170
174
 
171
175
  The commands use the same `XDG_CACHE_HOME` / home `.cache` directory as `open_engine`. They read only filesystem metadata, engine headers, and saved weight digest records. They do not read or checksum the plan body, load the original weights, deserialize engines, import TensorRT, or query a GPU. Listing recognizes the header format; it does not establish that an engine can be loaded.
172
176
 
173
- `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
177
+ `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. Each recognized entry also contains `workspace_limit`: a positive integer in bytes for an explicit build limit, JSON `null` when the build left the limit at TensorRT's default, or `"unknown"` when the header has no reference value. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
174
178
 
175
179
  An engine's `.engine` modification time records its initial save or the last successful `open_engine` cache read. Failed loads and these management commands leave that time unchanged. A timestamp write failure, including a read-only cache, does not prevent a successful load, and a disappeared file is never recreated to record its time. The record describes the time of `open_engine`, not whether inference continues afterward. Files saved before timestamp recording was introduced, and files whose timestamp could not be written, may not reflect the actual last successful read.
176
180
 
@@ -77,7 +77,7 @@ def graph(network: trt.INetworkDefinition) -> None:
77
77
 
78
78
  declaration = EngineBuild(
79
79
  inputs={"features": InputShape.ranged((1, 1, 2), (1, 64, 2), (1, 448, 2))},
80
- settings=BuilderSettings(workspace_limit=1 << 30, optimization_level=3),
80
+ settings=BuilderSettings(workspace_limit=None, optimization_level=3),
81
81
  )
82
82
  plan: bytes = build_serialized_engine(graph, declaration)
83
83
  ```
@@ -86,6 +86,8 @@ Use `InputShape.fixed((1, 64, 2))` for one input size; the graph may declare tha
86
86
 
87
87
  The engine's precision is determined by the tensor types that the graph function places in the network. For an fp16 graph, use `trt.float16` tensors and `np.float16` weights where needed; the graph can also contain fp32 tensors. `EngineBuild` declares input shapes and builder settings. TF32 is always disabled; `BuilderSettings(tf32=True)` raises `ValueError`. The function returns serialized plan bytes. Invalid declarations raise `ValueError`, and a failed TensorRT build raises `RuntimeError`.
88
88
 
89
+ `BuilderSettings.workspace_limit` defaults to `None`, which leaves the workspace limit at TensorRT's default without calling `set_memory_pool_limit`. To impose a limit, pass a positive integer in bytes, such as `workspace_limit=4 << 30` for 4 GiB. Zero, negative values, booleans, and noninteger values raise `ValueError`.
90
+
89
91
  The same API builds bf16, int8, and fp8 graphs. Cast tensors to `trt.bfloat16` for bf16; for int8/fp8, place `add_quantize` / `add_dequantize` layers and load their scales from safetensors. The application determines scales with an external quantization library and chooses output quality limits with `compare_outputs`. fp8 targets sm89 and sm120; bf16 and int8 also target sm86. Follow [Build bf16, int8, and fp8 engines](https://github.com/sync-dev-org/tensoracle/blob/main/docs/precision.md) for a complete example, NumPy dtype constraints, and comparison against an fp32 engine. Use different engine names or graph versions for different precisions.
90
92
 
91
93
  ## Save and reopen an engine
@@ -111,7 +113,9 @@ handle = open_engine(
111
113
  engine = handle.engine # Keep handle.runtime alive while using the engine.
112
114
  ```
113
115
 
114
- Saved files live in the `tensoracle` directory under `XDG_CACHE_HOME`, or under the home directory's `.cache` when that variable is unset. The same rule applies on every OS; on Windows the home directory is `%USERPROFILE%`, so the default is `%USERPROFILE%\.cache\tensoracle`. The cache identity includes the contents of all declared safetensors weight files and optional `plugins` files, the engine name, graph version, input shapes, builder settings, the GPU's compatibility properties, OS, CPU architecture, and TensorRT version. The GPU is the device of the current CUDA context, or device 0 when no context is current. Its properties are the product name, compute capability, memory bus width, L2 cache size, shared memory limits, texture alignment, multiprocessor count, whether the GPU is integrated, ECC mode, and total memory; another device with the same properties reuses the saved engine. Raise `graph_version` when graph code changes; without a version change, the previous engine is reused. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. Include in `graph_version` any other caller value that changes the engine contents but is not part of the cache identity, such as a plugin manifest or a build recipe. Pass `force_rebuild=True` to replace a cached engine even when it is intact. tensoracle closes a saved engine file after reading it, so another process using an opened engine does not block replacement. On Windows, a process that keeps the saved file itself open prevents replacement or removal with `PermissionError`; after that process closes the file, a file left empty by the failed rebuild is rejected as `CorruptEngineError` and can be rebuilt with `force_rebuild=True`.
116
+ Saved files live in the `tensoracle` directory under `XDG_CACHE_HOME`, or under the home directory's `.cache` when that variable is unset. The same rule applies on every OS; on Windows the home directory is `%USERPROFILE%`, so the default is `%USERPROFILE%\.cache\tensoracle`. The cache identity includes the contents of all declared safetensors weight files and optional `plugins` files, the engine name, graph version, input shapes, the builder's `optimization_level` and `tf32` settings, the GPU's compatibility properties, OS, CPU architecture, and TensorRT version. The GPU is the device of the current CUDA context, or device 0 when no context is current. Its properties are the product name, compute capability, memory bus width, L2 cache size, shared memory limits, texture alignment, multiprocessor count, whether the GPU is integrated, ECC mode, and total memory; another device with the same properties reuses the saved engine. Raise `graph_version` when graph code changes; without a version change, the previous engine is reused. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. Include in `graph_version` any other caller value that changes the engine contents but is not part of the cache identity, such as a plugin manifest or a build recipe. Pass `force_rebuild=True` to replace a cached engine even when it is intact. tensoracle closes a saved engine file after reading it, so another process using an opened engine does not block replacement. On Windows, a process that keeps the saved file itself open prevents replacement or removal with `PermissionError`; after that process closes the file, a file left empty by the failed rebuild is rejected as `CorruptEngineError` and can be rebuilt with `force_rebuild=True`.
117
+
118
+ `workspace_limit` is excluded from the cache identity. Changing it reuses a matching saved engine; pass `force_rebuild=True` to `open_engine` to build again with the new limit. The saved header records the workspace limit used for that build, including `None`, as a reference outside the conditions used for matching. Cache reads leave this reference unchanged. `tensoracle cache list` shows the recorded limit.
115
119
 
116
120
  `plugins` names native TensorRT plugin libraries. Both `open_engine` and `build_serialized_engine` accept it. Each function checks that every library exports `getCreators`, loads it through TensorRT's registry, and retains it for the process before building the graph or deserializing an engine. Repeating the same library does not register it twice. `open_engine` also hashes the loaded library bytes into the engine identity; a changed library selects a different saved engine in a new process. A missing, unreadable, or incompatible library, or a library changed after loading in the same process, raises `PluginLibraryError` before a cached plan is opened. Keep the library and its cubin bundle together in the caller's wheel.
117
121
 
@@ -137,7 +141,7 @@ tensoracle cache prune --max-size 10GiB
137
141
 
138
142
  The commands use the same `XDG_CACHE_HOME` / home `.cache` directory as `open_engine`. They read only filesystem metadata, engine headers, and saved weight digest records. They do not read or checksum the plan body, load the original weights, deserialize engines, import TensorRT, or query a GPU. Listing recognizes the header format; it does not establish that an engine can be loaded.
139
143
 
140
- `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
144
+ `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. Each recognized entry also contains `workspace_limit`: a positive integer in bytes for an explicit build limit, JSON `null` when the build left the limit at TensorRT's default, or `"unknown"` when the header has no reference value. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
141
145
 
142
146
  An engine's `.engine` modification time records its initial save or the last successful `open_engine` cache read. Failed loads and these management commands leave that time unchanged. A timestamp write failure, including a read-only cache, does not prevent a successful load, and a disappeared file is never recreated to record its time. The record describes the time of `open_engine`, not whether inference continues afterward. Files saved before timestamp recording was introduced, and files whose timestamp could not be written, may not reflect the actual last successful read.
143
147
 
@@ -74,6 +74,8 @@ handle = open_engine(
74
74
 
75
75
  `my_model` represents the application's model package. On a valid cache hit, `open_engine` does not call `graph`; on a miss, it builds and saves a new engine. Raise `graph_version` when the graph code changes or when any other caller value that changes the engine contents but is not part of the cache identity changes, such as a plugin manifest or a build recipe. `open_engine` itself does not execute the engine or manage GPU buffers. Create an execution unit from its returned handle to enqueue inference. Saved engines live under `XDG_CACHE_HOME/tensoracle`, or `~/.cache/tensoracle` when `XDG_CACHE_HOME` is unset. On Windows, `~` is `%USERPROFILE%`. See the [complete cache example](https://github.com/sync-dev-org/tensoracle#save-and-reopen-an-engine) for the declaration and expected input and output types.
76
76
 
77
+ `workspace_limit` is excluded from the cache identity; `optimization_level` and `tf32` remain in the conditions. Changing only the workspace limit reuses a matching saved engine. Pass `force_rebuild=True` to `open_engine` to build again with the new limit. The saved header records the workspace limit used for that build, including `None`, as a reference outside the conditions used for matching. Cache reads leave this reference unchanged. `tensoracle cache list` shows the recorded limit.
78
+
77
79
  ### Manage saved engines
78
80
 
79
81
  Inspect saved engines and preview or perform cleanup with the installed command:
@@ -88,7 +90,7 @@ tensoracle cache prune --max-size 10GiB
88
90
 
89
91
  The commands use the same `XDG_CACHE_HOME` / home `.cache` directory as `open_engine`. They read only filesystem metadata, engine headers, and saved weight digest records. They do not read or checksum the plan body, load the original weights, deserialize engines, import TensorRT, or query a GPU. Listing recognizes the header format; it does not establish that an engine can be loaded.
90
92
 
91
- `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
93
+ `cache list` prints one JSON object with `directory`, `entries`, and `total_size_bytes`. Each entry contains the file name, size in bytes, recorded time in UTC (`recorded_at`) and exact nanoseconds (`recorded_at_ns`), and the complete header conditions. This distinguishes engines with the same name and graph version but different weights or plugins. Each recognized entry also contains `workspace_limit`: a positive integer in bytes for an explicit build limit, JSON `null` when the build left the limit at TensorRT's default, or `"unknown"` when the header has no reference value. `weight_paths` associates each weight digest with paths from valid, checksum-verified `weight-*.json` records whose SHA-256 matches that digest; an empty `paths` array means unknown. These paths are references and do not establish which application uses an engine or whether the weight file still exists. Empty files have status `building or leftover`; unreadable or invalid headers have status `unrecognized` and a reason.
92
94
 
93
95
  An engine's `.engine` modification time records its initial save or the last successful `open_engine` cache read. Failed loads and these management commands leave that time unchanged. A timestamp write failure, including a read-only cache, does not prevent a successful load, and a disappeared file is never recreated to record its time. The record describes the time of `open_engine`, not whether inference continues afterward. Files saved before timestamp recording was introduced, and files whose timestamp could not be written, may not reflect the actual last successful read.
94
96
 
@@ -252,7 +254,7 @@ Engines with loops or conditionals, or layers or plugins that synchronize with t
252
254
 
253
255
  ### Build a standard graph
254
256
 
255
- Define `graph(network)` with TensorRT's Python network API and load model weights from safetensors. Describe each graph input with `InputShape.fixed` or `InputShape.ranged`, and pass an `EngineBuild` declaration to `build_serialized_engine`. `EngineBuild` declares input shapes and builder settings; the graph function determines precision through the tensor types it places in the network. The supported precisions are fp32 (with TF32 disabled), fp16, bf16, int8, and fp8. The [bf16, int8, and fp8 guide](precision.md) shows tensor casts, explicit Q/DQ layers, and scales supplied through safetensors; fp8 targets sm89 and sm120. `open_engine` uses the same declaration plus a name, graph version, weight files, and `EngineIO` to save and reuse a checked engine. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. The [graph example](https://github.com/sync-dev-org/tensoracle#define-and-build-a-standard-graph) shows the exact calls.
257
+ Define `graph(network)` with TensorRT's Python network API and load model weights from safetensors. Describe each graph input with `InputShape.fixed` or `InputShape.ranged`, and pass an `EngineBuild` declaration to `build_serialized_engine`. `EngineBuild` declares input shapes and builder settings; the graph function determines precision through the tensor types it places in the network. `BuilderSettings.workspace_limit` defaults to `None`, leaving the workspace limit at TensorRT's default. To impose a limit, pass a positive integer in bytes, such as `workspace_limit=4 << 30` for 4 GiB. Zero, negative values, booleans, and noninteger values raise `ValueError`. The supported precisions are fp32 (with TF32 disabled), fp16, bf16, int8, and fp8. The [bf16, int8, and fp8 guide](precision.md) shows tensor casts, explicit Q/DQ layers, and scales supplied through safetensors; fp8 targets sm89 and sm120. `open_engine` uses the same declaration plus a name, graph version, weight files, and `EngineIO` to save and reuse a checked engine. Use a different `name` or `graph_version` for engines with different tensor types, such as fp32 and fp16. The [graph example](https://github.com/sync-dev-org/tensoracle#define-and-build-a-standard-graph) shows the exact calls.
256
258
 
257
259
  The application runs the engine and passes a CPU NumPy candidate output and a corresponding reference output to `compare_outputs`. The application chooses the acceptance limits. See [Compare output arrays](https://github.com/sync-dev-org/tensoracle#compare-output-arrays).
258
260
 
@@ -69,10 +69,16 @@ class InputShape:
69
69
  class BuilderSettings:
70
70
  """TensorRT builder options that affect the serialized engine."""
71
71
 
72
- workspace_limit: int = 1 << 30
72
+ workspace_limit: int | None = None
73
73
  optimization_level: int = 3
74
74
  tf32: bool = False
75
75
 
76
+ def __post_init__(self) -> None:
77
+ if self.workspace_limit is not None and (
78
+ type(self.workspace_limit) is not int or self.workspace_limit <= 0
79
+ ):
80
+ raise ValueError("workspace_limit must be a positive integer or None")
81
+
76
82
 
77
83
  @dataclass(frozen=True)
78
84
  class EngineBuild:
@@ -103,8 +109,6 @@ def _build_serialized_engine(
103
109
  ) -> tuple[bytes, tuple[str, ...]]:
104
110
  if declaration.settings.tf32:
105
111
  raise ValueError("TF32 cannot be enabled")
106
- if declaration.settings.workspace_limit <= 0:
107
- raise ValueError("workspace_limit must be positive")
108
112
  if not 0 <= declaration.settings.optimization_level <= 5:
109
113
  raise ValueError("optimization_level must be between 0 and 5")
110
114
 
@@ -125,9 +129,10 @@ def _build_serialized_engine(
125
129
  _warn_shape_tensor_outputs(output_names, stacklevel=3)
126
130
  config = builder.create_builder_config()
127
131
  config.clear_flag(trt.BuilderFlag.TF32)
128
- config.set_memory_pool_limit(
129
- trt.MemoryPoolType.WORKSPACE, declaration.settings.workspace_limit
130
- )
132
+ if declaration.settings.workspace_limit is not None:
133
+ config.set_memory_pool_limit(
134
+ trt.MemoryPoolType.WORKSPACE, declaration.settings.workspace_limit
135
+ )
131
136
  config.builder_optimization_level = declaration.settings.optimization_level
132
137
 
133
138
  inputs = {
@@ -7,7 +7,7 @@ import os
7
7
  import platform
8
8
  import tempfile
9
9
  from collections.abc import Callable, Mapping, Sequence
10
- from dataclasses import asdict, dataclass
10
+ from dataclasses import dataclass
11
11
  from dataclasses import field as dataclass_field
12
12
  from pathlib import Path
13
13
  from typing import TYPE_CHECKING
@@ -125,7 +125,11 @@ def open_engine(
125
125
  handle = _open_plan(
126
126
  plan, path, declaration, io, actual_logger, shape_tensor_outputs=shape_tensor_outputs
127
127
  )
128
- header = {"conditions": conditions, "plan_sha256": hashlib.sha256(plan).hexdigest()}
128
+ header = {
129
+ "conditions": conditions,
130
+ "plan_sha256": hashlib.sha256(plan).hexdigest(),
131
+ "workspace_limit": declaration.settings.workspace_limit,
132
+ }
129
133
  if shape_tensor_outputs:
130
134
  header["shape_tensor_outputs"] = list(shape_tensor_outputs)
131
135
  header_bytes = _json_bytes(header)
@@ -286,7 +290,10 @@ def _conditions(
286
290
  }
287
291
  for name, shape in declaration.inputs.items()
288
292
  },
289
- "builder_settings": asdict(declaration.settings),
293
+ "builder_settings": {
294
+ "optimization_level": declaration.settings.optimization_level,
295
+ "tf32": declaration.settings.tf32,
296
+ },
290
297
  "gpu": _gpu_identity(),
291
298
  "os": platform.system(),
292
299
  "architecture": platform.machine(),
@@ -58,6 +58,7 @@ def _inspect(path: Path) -> _Entry:
58
58
  raise OSError("file changed during inspection")
59
59
  details["status"] = "recognized"
60
60
  details["conditions"] = header["conditions"]
61
+ details["workspace_limit"] = header.get("workspace_limit", "unknown")
61
62
  except (OSError, CorruptEngineError) as error:
62
63
  details["reason"] = str(error)
63
64
  return _Entry(path, observed, details)
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.5.0'
22
- __version_tuple__ = version_tuple = (1, 5, 0)
21
+ __version__ = version = '1.6.0'
22
+ __version_tuple__ = version_tuple = (1, 6, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -187,8 +187,23 @@ def test_tf32_request_is_rejected_before_graph_is_built():
187
187
 
188
188
 
189
189
  @pytest.mark.req("SP-TENSORACLE-013")
190
- def test_tf32_is_disabled_and_other_builder_settings_are_applied(monkeypatch):
191
- """The builder clears TF32 while applying the declared workspace and optimization level."""
190
+ @pytest.mark.req("SP-TENSORACLE-052")
191
+ @pytest.mark.parametrize(
192
+ "settings, workspace_calls",
193
+ [
194
+ (BuilderSettings(optimization_level=2), []),
195
+ (BuilderSettings(workspace_limit=None, optimization_level=2), []),
196
+ (
197
+ BuilderSettings(workspace_limit=4096, optimization_level=2),
198
+ [(trt.MemoryPoolType.WORKSPACE, 4096)],
199
+ ),
200
+ ],
201
+ ids=["default-workspace", "explicit-none", "positive-workspace"],
202
+ )
203
+ def test_tf32_is_disabled_and_other_builder_settings_are_applied(
204
+ monkeypatch, settings, workspace_calls
205
+ ):
206
+ """The builder clears TF32 and sets a workspace limit only for an explicit positive integer."""
192
207
  import tensoracle._build as module
193
208
 
194
209
  class Config:
@@ -196,13 +211,13 @@ def test_tf32_is_disabled_and_other_builder_settings_are_applied(monkeypatch):
196
211
 
197
212
  def __init__(self):
198
213
  self.flags = {trt.BuilderFlag.TF32}
199
- self.workspace = None
214
+ self.workspace_calls = []
200
215
 
201
216
  def clear_flag(self, flag):
202
217
  self.flags.discard(flag)
203
218
 
204
219
  def set_memory_pool_limit(self, pool, limit):
205
- self.workspace = (pool, limit)
220
+ self.workspace_calls.append((pool, limit))
206
221
 
207
222
  class Network:
208
223
  num_inputs = 1
@@ -229,10 +244,20 @@ def test_tf32_is_disabled_and_other_builder_settings_are_applied(monkeypatch):
229
244
  monkeypatch.setattr(module.trt, "Builder", Builder)
230
245
  declaration = EngineBuild(
231
246
  inputs={"features": InputShape.fixed((1, 3, 2))},
232
- settings=BuilderSettings(workspace_limit=4096, optimization_level=2),
247
+ settings=settings,
233
248
  )
234
249
  assert build_serialized_engine(lambda network: None, declaration) == b"plan"
235
250
  config = observed[0].config
236
251
  assert trt.BuilderFlag.TF32 not in config.flags
237
- assert config.workspace == (trt.MemoryPoolType.WORKSPACE, 4096)
252
+ assert config.workspace_calls == workspace_calls
238
253
  assert config.builder_optimization_level == 2
254
+
255
+
256
+ @pytest.mark.req("SP-TENSORACLE-052")
257
+ @pytest.mark.parametrize("workspace_limit", [0, -1, True, False, 1.5, "4096"])
258
+ def test_invalid_workspace_limit_is_rejected_before_building(monkeypatch, workspace_limit):
259
+ """Zero, negatives, booleans, and nonintegers fail before creating a TensorRT builder."""
260
+ monkeypatch.setattr(trt, "Builder", lambda logger: pytest.fail("builder was created"))
261
+ with pytest.raises(ValueError, match="workspace_limit"):
262
+ declaration = EngineBuild({}, BuilderSettings(workspace_limit=workspace_limit))
263
+ build_serialized_engine(lambda network: pytest.fail("graph was built"), declaration)
@@ -347,6 +347,7 @@ def test_corrupt_plan_and_changed_header_fail_before_deserialization(tmp_path, m
347
347
  @pytest.mark.gpu
348
348
  @pytest.mark.req("SP-TENSORACLE-002")
349
349
  @pytest.mark.req("SP-TENSORACLE-011")
350
+ @pytest.mark.req("SP-TENSORACLE-052")
350
351
  def test_force_rebuild_uses_changed_weight_contents_and_preserves_old_engine(tmp_path, monkeypatch):
351
352
  """A forced rebuild saves under the new digest without replacing the old engine."""
352
353
  monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))
@@ -378,7 +379,7 @@ def test_force_rebuild_uses_changed_weight_contents_and_preserves_old_engine(tmp
378
379
  offset = len(b"TENSORACLE\x01")
379
380
  old_header_size = int.from_bytes(old_engine[offset : offset + 8], "big")
380
381
  old_header = json.loads(old_engine[offset + 8 : offset + 8 + old_header_size])
381
- assert old_header.keys() == {"conditions", "plan_sha256"}
382
+ assert old_header.keys() == {"conditions", "plan_sha256", "workspace_limit"}
382
383
 
383
384
  before = weights.stat()
384
385
  changed = bytearray(weights.read_bytes())
@@ -618,7 +619,6 @@ def test_each_declared_condition_selects_a_distinct_cache_entry(tmp_path, monkey
618
619
  {"shape": (1, 4, 2)},
619
620
  {"input_range": ((1, 1, 2), (1, 3, 2), (1, 5, 2))},
620
621
  {"input_range": ((1, 1, 2), (1, 3, 2), (1, 6, 2))},
621
- {"settings": tensoracle.BuilderSettings(workspace_limit=2048, optimization_level=0)},
622
622
  {"settings": tensoracle.BuilderSettings(optimization_level=1)},
623
623
  ]
624
624
  paths = {open_case(**variant).path for variant in variants}
@@ -22,7 +22,9 @@ def saved_cache(tmp_path, monkeypatch):
22
22
  return directory
23
23
 
24
24
 
25
- def save_engine(directory, filename="one.engine", *, conditions=None, mtime=OLD_NS, size=None):
25
+ def save_engine(
26
+ directory, filename="one.engine", *, conditions=None, mtime=OLD_NS, size=None, metadata=None
27
+ ):
26
28
  if conditions is None:
27
29
  conditions = {
28
30
  "name": "encoder",
@@ -36,7 +38,9 @@ def save_engine(directory, filename="one.engine", *, conditions=None, mtime=OLD_
36
38
  "architecture": "x86_64",
37
39
  "tensorrt_version": "11.0",
38
40
  }
39
- header = json.dumps({"conditions": conditions, "plan_sha256": "0" * 64}).encode()
41
+ header = json.dumps(
42
+ {"conditions": conditions, "plan_sha256": "0" * 64, **(metadata or {})}
43
+ ).encode()
40
44
  path = directory / filename
41
45
  path.write_bytes(b"TENSORACLE\x01" + len(header).to_bytes(8, "big") + header + b"plan")
42
46
  if size is not None:
@@ -104,6 +108,26 @@ def test_list_distinguishes_all_conditions_and_preserves_recorded_time(saved_cac
104
108
  )
105
109
 
106
110
 
111
+ @pytest.mark.req("SP-TENSORACLE-043")
112
+ @pytest.mark.req("SP-TENSORACLE-052")
113
+ @pytest.mark.parametrize(
114
+ "metadata, expected",
115
+ [({"workspace_limit": None}, None), ({"workspace_limit": 4096}, 4096), ({}, "unknown")],
116
+ ids=["tensorrt-default", "explicit-limit", "missing-record"],
117
+ )
118
+ def test_list_shows_recorded_workspace_limit_or_unknown(saved_cache, capsys, metadata, expected):
119
+ """Listing distinguishes TensorRT's default, an explicit byte limit, and a missing record."""
120
+ path, conditions = save_engine(saved_cache, metadata=metadata)
121
+ contents = path.read_bytes()
122
+ status, (listing,), stderr = call_cli(capsys, "list")
123
+ assert status == 0 and not stderr
124
+ entry = listing["entries"][0]
125
+ assert entry["workspace_limit"] == expected
126
+ assert entry["conditions"] == conditions
127
+ assert path.read_bytes() == contents
128
+ assert path.stat().st_mtime_ns == OLD_NS
129
+
130
+
107
131
  @pytest.mark.req("SP-TENSORACLE-049")
108
132
  @pytest.mark.parametrize(
109
133
  "damage", ["missing", "json", "version", "type", "checksum", "digest", "mismatch", "valid"]
@@ -39,19 +39,60 @@ def open_cached(tmp_path, monkeypatch):
39
39
  def graph(network):
40
40
  built.append(True)
41
41
 
42
- def open_engine(*, io=None):
42
+ def open_engine(*, io=None, settings=None, force_rebuild=False):
43
43
  return tensoracle.open_engine(
44
44
  graph,
45
- tensoracle.EngineBuild({}),
45
+ tensoracle.EngineBuild({}, settings or tensoracle.BuilderSettings()),
46
46
  name="timestamp",
47
47
  graph_version="1",
48
48
  weights=(weight,),
49
49
  io=io or tensoracle.EngineIO({}, {}),
50
+ force_rebuild=force_rebuild,
50
51
  )
51
52
 
52
53
  return open_engine, built
53
54
 
54
55
 
56
+ @pytest.mark.req("SP-TENSORACLE-052")
57
+ @pytest.mark.parametrize("workspace_limit", [None, 8192])
58
+ def test_workspace_limit_changes_reuse_saved_engine(open_cached, workspace_limit):
59
+ """Changing only the workspace limit reuses the same file without invoking the graph."""
60
+ open_engine, built = open_cached
61
+ first = open_engine(settings=tensoracle.BuilderSettings(workspace_limit=4096))
62
+ contents = first.path.read_bytes()
63
+ reopened = open_engine(settings=tensoracle.BuilderSettings(workspace_limit=workspace_limit))
64
+ assert reopened.path == first.path
65
+ assert built == [True]
66
+ assert first.path.read_bytes() == contents
67
+
68
+
69
+ @pytest.mark.req("SP-TENSORACLE-052")
70
+ @pytest.mark.req("SP-TENSORACLE-011")
71
+ @pytest.mark.parametrize("workspace_limit", [None, 4096])
72
+ def test_saved_engine_records_workspace_limit_and_updates_it_only_on_rebuild(
73
+ open_cached, workspace_limit
74
+ ):
75
+ """Headers record the actual build's workspace outside conditions and forced rebuilds replace it."""
76
+ open_engine, built = open_cached
77
+
78
+ def header(path):
79
+ with path.open("rb") as stream:
80
+ return cache._read_header(stream, path, path.stat().st_size)
81
+
82
+ first = open_engine(settings=tensoracle.BuilderSettings(workspace_limit=workspace_limit))
83
+ original = header(first.path)
84
+ assert original["workspace_limit"] == workspace_limit
85
+ assert original["conditions"]["builder_settings"] == {"optimization_level": 3, "tf32": False}
86
+ changed_limit = 8192 if workspace_limit is None else None
87
+ rebuilt = open_engine(
88
+ settings=tensoracle.BuilderSettings(workspace_limit=changed_limit), force_rebuild=True
89
+ )
90
+ assert rebuilt.path == first.path
91
+ assert header(rebuilt.path)["workspace_limit"] == changed_limit
92
+ assert header(rebuilt.path)["conditions"] == original["conditions"]
93
+ assert built == [True, True]
94
+
95
+
55
96
  @pytest.mark.req("SP-TENSORACLE-042")
56
97
  @pytest.mark.req("SP-TENSORACLE-048")
57
98
  @pytest.mark.req("SP-TENSORACLE-046")
@@ -297,6 +297,7 @@ def test_record_lives_in_user_cache_directory(tmp_path, monkeypatch, xdg):
297
297
 
298
298
  @pytest.mark.req("SP-TENSORACLE-002")
299
299
  @pytest.mark.req("SP-TENSORACLE-009")
300
+ @pytest.mark.req("SP-TENSORACLE-052")
300
301
  def test_engine_identity_matches_the_full_hash_calculation(tmp_path, monkeypatch):
301
302
  """Stored digests produce the same conditions and engine name as full file hashing."""
302
303
  root = tmp_path / "cache"
@@ -312,7 +313,7 @@ def test_engine_identity_matches_the_full_hash_calculation(tmp_path, monkeypatch
312
313
  "weights": [hashlib.sha256(weight.read_bytes()).hexdigest()],
313
314
  "plugins": [hashlib.sha256(plugin.read_bytes()).hexdigest()],
314
315
  "inputs": {"features": {"minimum": [1, 3, 2], "optimum": [1, 3, 2], "maximum": [1, 3, 2]}},
315
- "builder_settings": {"workspace_limit": 1 << 30, "optimization_level": 3, "tf32": False},
316
+ "builder_settings": {"optimization_level": 3, "tf32": False},
316
317
  "gpu": {"name": "GPU A"},
317
318
  "os": cache.platform.system(),
318
319
  "architecture": cache.platform.machine(),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes