hugpy-engine 0.2.5__tar.gz → 0.2.6__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 (225) hide show
  1. {hugpy_engine-0.2.5/src/hugpy_engine.egg-info → hugpy_engine-0.2.6}/PKG-INFO +1 -1
  2. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/models/models_config.py +40 -7
  3. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/dispatch/dispatch.py +28 -1
  4. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/fit/flex.py +27 -7
  5. hugpy_engine-0.2.6/src/hugpy_engine/fit/gguf_need.py +203 -0
  6. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/fit/plan.py +197 -15
  7. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/fit/types.py +34 -4
  8. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/generate/coder.py +27 -0
  9. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/generate/generate_runner.py +50 -0
  10. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/chat_runner.py +21 -0
  11. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/get.py +48 -0
  12. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/ccp_runner.py +5 -1
  13. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/model_index/__init__.py +27 -1
  14. hugpy_engine-0.2.6/src/hugpy_engine/model_index/planner/__init__.py +5 -0
  15. hugpy_engine-0.2.6/src/hugpy_engine/model_index/planner/compute.py +714 -0
  16. hugpy_engine-0.2.6/src/hugpy_engine/model_index/planner/store.py +466 -0
  17. hugpy_engine-0.2.6/src/hugpy_engine/model_index/query_registry.py +1052 -0
  18. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/model_index/repositories.py +50 -8
  19. hugpy_engine-0.2.6/src/hugpy_engine/model_index/service.py +1025 -0
  20. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/overrides.py +98 -1
  21. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/profiles.py +70 -17
  22. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/slot_agent.py +175 -20
  23. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/slots.py +32 -4
  24. hugpy_engine-0.2.6/src/hugpy_engine/serve/tf_child.py +553 -0
  25. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/spill.py +121 -4
  26. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6/src/hugpy_engine.egg-info}/PKG-INFO +1 -1
  27. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/SOURCES.txt +16 -0
  28. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/scm_file_list.json +16 -0
  29. hugpy_engine-0.2.6/src/hugpy_engine.egg-info/scm_version.json +8 -0
  30. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_plan_fit_contract.py +4 -1
  31. hugpy_engine-0.2.6/tests/test_ctx_range_flex.py +31 -0
  32. hugpy_engine-0.2.6/tests/test_evict_releases_transformers_registry.py +51 -0
  33. hugpy_engine-0.2.6/tests/test_planner_encroachment.py +39 -0
  34. hugpy_engine-0.2.6/tests/test_planner_worker_totals.py +20 -0
  35. hugpy_engine-0.2.6/tests/test_profile_worker_base.py +25 -0
  36. hugpy_engine-0.2.6/tests/test_ram_residents_plan.py +50 -0
  37. hugpy_engine-0.2.6/tests/test_slot_parallel_kv.py +37 -0
  38. hugpy_engine-0.2.6/tests/test_slot_served_ctx.py +24 -0
  39. hugpy_engine-0.2.6/tests/test_tf_child.py +111 -0
  40. hugpy_engine-0.2.6/tests/test_tf_slot_and_routing.py +57 -0
  41. hugpy_engine-0.2.6/tests/test_transformers_kv_geometry_hybrid_vl.py +30 -0
  42. hugpy_engine-0.2.5/src/hugpy_engine/model_index/query_registry.py +0 -399
  43. hugpy_engine-0.2.5/src/hugpy_engine/model_index/service.py +0 -281
  44. hugpy_engine-0.2.5/src/hugpy_engine.egg-info/scm_version.json +0 -8
  45. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/LICENSE +0 -0
  46. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/README.md +0 -0
  47. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/docs/ALLOCATION-MODES.md +0 -0
  48. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/pyproject.toml +0 -0
  49. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/setup.cfg +0 -0
  50. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/__init__.py +0 -0
  51. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/alloc_modes.py +0 -0
  52. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/allocation.py +0 -0
  53. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/__init__.py +0 -0
  54. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/call_api.py +0 -0
  55. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/get_module.py +0 -0
  56. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/reclassify.py +0 -0
  57. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/reconcile.py +0 -0
  58. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/serve/__init__.py +0 -0
  59. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/serve/serve.py +0 -0
  60. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/serve/serve_cli.py +0 -0
  61. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/apis/systemd_units.py +0 -0
  62. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/backend.py +0 -0
  63. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/backends/__init__.py +0 -0
  64. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/backends/local.py +0 -0
  65. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/catalog.py +0 -0
  66. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/catalog_bridge.py +0 -0
  67. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/categories.py +0 -0
  68. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/chat_context/__init__.py +0 -0
  69. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/chat_context/chat_context.py +0 -0
  70. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/chat_context/context_budget.py +0 -0
  71. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/chat_context/unbounded.py +0 -0
  72. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/__init__.py +0 -0
  73. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/main.py +0 -0
  74. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/models/__init__.py +0 -0
  75. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/models/model_meta.py +0 -0
  76. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/models/models_default.py +0 -0
  77. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/config/models/models_dict.py +0 -0
  78. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/contracts.py +0 -0
  79. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/dispatch/__init__.py +0 -0
  80. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/dispatch/acquire.py +0 -0
  81. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/dispatch/activity.py +0 -0
  82. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/draft_models.py +0 -0
  83. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/engines.py +0 -0
  84. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/eviction.py +0 -0
  85. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/fit/__init__.py +0 -0
  86. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/generate/__init__.py +0 -0
  87. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/generate/config.py +0 -0
  88. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/gguf_election.py +0 -0
  89. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/__init__.py +0 -0
  90. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/__init__.py +0 -0
  91. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/__init__.py +0 -0
  92. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/base_runner.py +0 -0
  93. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/imports/__init__.py +0 -0
  94. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/imports/config.py +0 -0
  95. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/imports/constants.py +0 -0
  96. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/imports/init_imports.py +0 -0
  97. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/imports/utils.py +0 -0
  98. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/python_runner.py +0 -0
  99. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/runners/src/shard_server.py +0 -0
  100. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/llama/serve.py +0 -0
  101. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/manifest.py +0 -0
  102. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/model_classifier.py +0 -0
  103. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/model_index/client.py +0 -0
  104. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/name_match.py +0 -0
  105. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/native/__init__.py +0 -0
  106. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/native/build.py +0 -0
  107. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/native/fetch.py +0 -0
  108. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/native/resolve.py +0 -0
  109. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/output_repair.py +0 -0
  110. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/peft_adapters.py +0 -0
  111. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/placement.py +0 -0
  112. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/py.typed +0 -0
  113. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/query.py +0 -0
  114. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/__init__.py +0 -0
  115. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/allocator.py +0 -0
  116. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/assure_model_key.py +0 -0
  117. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/categories/__init__.py +0 -0
  118. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/categories/builders.py +0 -0
  119. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/categories/frameworks.py +0 -0
  120. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/device_placement.py +0 -0
  121. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/groups.py +0 -0
  122. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/model_dict_resolver.py +0 -0
  123. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/model_resolver.py +0 -0
  124. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/resolvers/remote.py +0 -0
  125. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/routing_diagnostics.py +0 -0
  126. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/runtime.py +0 -0
  127. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/__init__.py +0 -0
  128. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/chat_schemas.py +0 -0
  129. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/event_schemas.py +0 -0
  130. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/metadata_schemas.py +0 -0
  131. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/model_schemas.py +0 -0
  132. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/runner_schemas.py +0 -0
  133. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/schemas/task_schemas.py +0 -0
  134. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/__init__.py +0 -0
  135. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/chunksum_verify.py +0 -0
  136. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/hot_cache.py +0 -0
  137. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/load_failure.py +0 -0
  138. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/model_cache.py +0 -0
  139. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/policy.py +0 -0
  140. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/serve.py +0 -0
  141. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/serve_cli.py +0 -0
  142. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/serve/supervisor.py +0 -0
  143. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/task_deps.py +0 -0
  144. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/tasks.py +0 -0
  145. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/utils/__init__.py +0 -0
  146. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/utils/json_scavenge.py +0 -0
  147. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/utils/no_think.py +0 -0
  148. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/__init__.py +0 -0
  149. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/chat_schemas.py +0 -0
  150. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/config_schemas.py +0 -0
  151. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/install_schemas.py +0 -0
  152. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/model_schemas.py +0 -0
  153. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/request_schemas.py +0 -0
  154. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine/wire/specs_schemas.py +0 -0
  155. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/dependency_links.txt +0 -0
  156. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/entry_points.txt +0 -0
  157. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/requires.txt +0 -0
  158. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/src/hugpy_engine.egg-info/top_level.txt +0 -0
  159. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/conftest.py +0 -0
  160. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_call_queue_contract.py +0 -0
  161. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_dependency_acquisition_contract.py +0 -0
  162. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_evict_fit_contract.py +0 -0
  163. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_key_equivalence_contract.py +0 -0
  164. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_missing_dependency_contract.py +0 -0
  165. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_model_format_contract.py +0 -0
  166. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_model_key_contract.py +0 -0
  167. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_politeness_retired_contract.py +0 -0
  168. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_prefill_progress_contract.py +0 -0
  169. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_relay_gate_allocation_contract.py +0 -0
  170. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_relay_stream_stall_contract.py +0 -0
  171. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_remote_error_structure_contract.py +0 -0
  172. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_response_format_contract.py +0 -0
  173. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_slot_seat_admission_contract.py +0 -0
  174. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/known_good/test_slot_serialization_contract.py +0 -0
  175. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_admission_gate_and_diagnostics.py +0 -0
  176. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_allocation_accuracy.py +0 -0
  177. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_budget_bar_spec.py +0 -0
  178. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_catalog_bridge.py +0 -0
  179. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_central_cap_gating.py +0 -0
  180. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_chat_request_caller.py +0 -0
  181. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_chat_role_alternation.py +0 -0
  182. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_chat_tool_messages_schema.py +0 -0
  183. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_chunksum_verify.py +0 -0
  184. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_comms_foundations.py +0 -0
  185. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_ctx_fit.py +0 -0
  186. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_device_placement.py +0 -0
  187. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_engine_boundary.py +0 -0
  188. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_engine_persisted_resolve.py +0 -0
  189. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_eviction_flow.py +0 -0
  190. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_feasible_distribution_engine.py +0 -0
  191. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_gguf_election.py +0 -0
  192. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_gguf_sharded_effective.py +0 -0
  193. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_hot_cache.py +0 -0
  194. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_image_task_classification.py +0 -0
  195. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_import_policy.py +0 -0
  196. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_load_failure_reason.py +0 -0
  197. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_mmproj_effective_bytes.py +0 -0
  198. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_model_aliases.py +0 -0
  199. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_no_makeroom.py +0 -0
  200. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_no_think.py +0 -0
  201. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_peft_adapter_load.py +0 -0
  202. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_per_device_slot.py +0 -0
  203. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_per_request_alloc.py +0 -0
  204. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_placement_defaults.py +0 -0
  205. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_provision_failure_honesty.py +0 -0
  206. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_registry_attribution.py +0 -0
  207. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_registry_symmetry.py +0 -0
  208. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_resolve_routing_key_reconcile.py +0 -0
  209. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_runner_registry_heartbeat_starvation.py +0 -0
  210. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_agent_loading_state.py +0 -0
  211. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_load_stall_health.py +0 -0
  212. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_moe_vram_preflight.py +0 -0
  213. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_placement_projection.py +0 -0
  214. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_port_guard.py +0 -0
  215. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_slot_relaunch.py +0 -0
  216. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_spill_ctx_reserve.py +0 -0
  217. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_spill_vision_fit.py +0 -0
  218. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_tasks_registry.py +0 -0
  219. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_tok_s_plausible_ceiling.py +0 -0
  220. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_transformers_placement.py +0 -0
  221. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_video_model_classification.py +0 -0
  222. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_vision_needs_slot.py +0 -0
  223. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_vram_cliff.py +0 -0
  224. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_worker_error_attribution.py +0 -0
  225. {hugpy_engine-0.2.5 → hugpy_engine-0.2.6}/tests/test_worker_serve_path_central_only.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hugpy-engine
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Hugpy engine: model discovery and classification, allocation and eviction, native llama.cpp and GGUF runners, and the prompt-to-reply inference path with task and placement seams
5
5
  Author-email: putkoff <support@hugpy.ai>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1618,14 +1618,34 @@ def record_worker_models(worker_id: str, rows: dict) -> list[str]:
1618
1618
  clean[key]["worker_location"] = location
1619
1619
  old = (data.get(worker_id) or {}).get("models")
1620
1620
  changed = old != clean
1621
- if changed:
1621
+ # DETACH STEP 2 (operator 2026-10-02): the catalog's home is the DB
1622
+ # (model_worker_presence, source 'discovered'); the JSON is written only
1623
+ # with HUGPY_WORKER_CATALOG_JSON=1 — or when the DB write did not land
1624
+ # (no DB / DB down): the JSON is the no-DB fallback the reader uses, so
1625
+ # a no-DB central must still persist its scan there (2026-10-02).
1626
+ db_changed = False
1627
+ db_ok = False
1628
+ try:
1629
+ from hugpy_engine.model_index import record_worker_presence
1630
+ rows = []
1631
+ for key, row in clean.items():
1632
+ r = dict(row); r["model_key"] = key
1633
+ if r.get("worker_location"):
1634
+ r["external_location"] = r["worker_location"]
1635
+ rows.append(r)
1636
+ st, det = record_worker_presence(worker_id, rows, source="discovered", complete=True)
1637
+ db_ok = st == "ok"
1638
+ db_changed = bool(db_ok and (det or {}).get("changed"))
1639
+ except Exception: # noqa: BLE001 — a DB miss never breaks a heartbeat
1640
+ db_changed = False
1641
+ if changed and (not db_ok or os.environ.get("HUGPY_WORKER_CATALOG_JSON", "0") == "1"):
1622
1642
  data[worker_id] = {"at": time.time(), "models": clean}
1623
1643
  tmp = path + f".{os.getpid()}.tmp"
1624
1644
  with open(tmp, "w", encoding="utf-8") as fh:
1625
1645
  json.dump(data, fh)
1626
1646
  os.replace(tmp, path)
1627
1647
  fcntl.flock(lock, fcntl.LOCK_UN)
1628
- _apply_worker_catalog(force=changed)
1648
+ _apply_worker_catalog(force=(changed or db_changed))
1629
1649
  # The canonical keys may differ from worker-local scan keys when central
1630
1650
  # already has a same-named model from another hub. Return those identities
1631
1651
  # so heartbeat placement uses the same keys as the shared model catalog.
@@ -1639,14 +1659,27 @@ def _apply_worker_catalog(force=False) -> None:
1639
1659
  registry_dict = globals().get("MODEL_REGISTRY_DICT")
1640
1660
  if registry is None or registry_dict is None:
1641
1661
  return # import-time base build
1662
+ data = None
1663
+ # DB first (detach step 2): the discovered catalog lives in
1664
+ # model_worker_presence; the JSON file is the no-DB fallback only.
1642
1665
  try:
1643
- stamp = os.stat(_WORKER_CATALOG_PATH).st_mtime_ns
1666
+ from hugpy_engine.model_index import fetch_presence_catalog
1667
+ data = fetch_presence_catalog()
1668
+ except Exception: # noqa: BLE001
1669
+ data = None
1670
+ if data is not None:
1671
+ stamp = hash(json.dumps(data, sort_keys=True, default=str))
1644
1672
  if not force and stamp == _WORKER_CATALOG_SEEN:
1645
1673
  return
1646
- with open(_WORKER_CATALOG_PATH, encoding="utf-8") as fh:
1647
- data = json.load(fh)
1648
- except (OSError, ValueError):
1649
- return
1674
+ else:
1675
+ try:
1676
+ stamp = os.stat(_WORKER_CATALOG_PATH).st_mtime_ns
1677
+ if not force and stamp == _WORKER_CATALOG_SEEN:
1678
+ return
1679
+ with open(_WORKER_CATALOG_PATH, encoding="utf-8") as fh:
1680
+ data = json.load(fh)
1681
+ except (OSError, ValueError):
1682
+ return
1650
1683
  locations: dict[str, dict[str, str]] = {}
1651
1684
  external_apis: dict[str, dict[str, dict]] = {}
1652
1685
  candidates: dict[str, dict] = {}
@@ -1147,7 +1147,26 @@ def evict(model_key: str, task: Optional[str] = None) -> bool:
1147
1147
  heavy = evict_llama_runner(model_key)
1148
1148
  except Exception:
1149
1149
  heavy = False
1150
- return dropped or heavy
1150
+ # TRANSFORMERS weights live in generate.coder.REGISTRY, not in the runner
1151
+ # wrapper: drop them too, then reclaim so the freed VRAM is visible to the
1152
+ # very next fit check (2026-10-02: an in-process eviction "freed 16 MB" of
1153
+ # a 17 GB model and stranded the rest on the card).
1154
+ tf = 0
1155
+ try:
1156
+ from hugpy_engine.generate.coder import REGISTRY as _TF_REGISTRY
1157
+ tf = _TF_REGISTRY.evict_model(model_key)
1158
+ except Exception:
1159
+ tf = 0
1160
+ if tf:
1161
+ try:
1162
+ import gc
1163
+ gc.collect()
1164
+ import torch
1165
+ if torch.cuda.is_available():
1166
+ torch.cuda.empty_cache()
1167
+ except Exception:
1168
+ pass
1169
+ return dropped or heavy or bool(tf)
1151
1170
 
1152
1171
 
1153
1172
  def clear() -> None:
@@ -1159,6 +1178,14 @@ def clear() -> None:
1159
1178
  clear_llama_runners()
1160
1179
  except Exception:
1161
1180
  pass
1181
+ try:
1182
+ from hugpy_engine.generate.coder import REGISTRY as _TF_REGISTRY
1183
+ for _k in {getattr(getattr(i, "cfg", None), "model_key", None) for i in list(_TF_REGISTRY._instances.values())}:
1184
+ if _k:
1185
+ _TF_REGISTRY.evict_model(_k)
1186
+ _TF_REGISTRY.clear()
1187
+ except Exception:
1188
+ pass
1162
1189
 
1163
1190
 
1164
1191
  def supported_task_keys() -> List[Tuple[str, str]]:
@@ -127,7 +127,8 @@ def band_ceiling(target: float, deviation_pct: Optional[float],
127
127
 
128
128
  # ── band math (ctx%: the 1..100 point scale) ─────────────────────────────────
129
129
  def ctx_band_bounds(ctx_pct: Optional[int],
130
- deviation_pct: Optional[float]) -> "tuple[int, int] | None":
130
+ deviation_pct: Optional[float],
131
+ floor_pct: Optional[int] = None) -> "tuple[int, int] | None":
131
132
  """The hard ctx band ``(floor_pct, ceil_pct)`` in whole percent points, or
132
133
  None when there is no ctx target (ctx band is opt-in — no target == today's
133
134
  default ctx, byte-identical).
@@ -143,6 +144,15 @@ def ctx_band_bounds(ctx_pct: Optional[int],
143
144
  except (TypeError, ValueError):
144
145
  return None
145
146
  c = max(1, min(100, c))
147
+ # CONTEXT RANGE (operator 2026-10-02): an explicit minimum makes the band
148
+ # [min, target] — a polite window below the target, never above it.
149
+ if floor_pct is not None:
150
+ try:
151
+ f = int(floor_pct)
152
+ except (TypeError, ValueError):
153
+ f = None
154
+ if f is not None:
155
+ return max(1, min(c, f)), c
146
156
  dev = float(deviation_pct or 0.0)
147
157
  if dev <= 0:
148
158
  return c, c
@@ -610,17 +620,27 @@ def plan_flex(subject: dict, residents: list, deficit_bytes: int) -> FlexPlan:
610
620
  subj_ctx = subject.get("ctx_pct")
611
621
  subj_dev = subject.get("ctx_deviation_pct")
612
622
  subj_kv = int(subject.get("kv_bytes") or 0)
613
- band = ctx_band_bounds(subj_ctx, subj_dev)
623
+ band = ctx_band_bounds(subj_ctx, subj_dev, subject.get("ctx_floor_pct"))
614
624
  if band is not None and subj_kv > 0:
615
625
  floor_pct, _ceil = band
616
626
  if floor_pct < int(subj_ctx):
617
- kv_floor = kv_at_ctx_pct(subj_kv, int(subj_ctx), floor_pct)
618
- saved = max(0, subj_kv - kv_floor)
627
+ c = int(subj_ctx)
628
+ to_pct = floor_pct
629
+ if subject.get("ctx_floor_pct") is not None:
630
+ # An explicit RANGE: the LARGEST ctx in [min, target) whose KV
631
+ # saving covers the deficit ("adjusting to fit where possible",
632
+ # 2026-10-02) — never deeper than needed; the minimum when even
633
+ # it is not enough. A ± deviation band keeps its floor jump.
634
+ keep = subj_kv - remaining
635
+ fit_pct = int(c * keep // subj_kv) if keep > 0 else 0
636
+ to_pct = max(floor_pct, min(c - 1, fit_pct))
637
+ kv_to = kv_at_ctx_pct(subj_kv, c, to_pct)
638
+ saved = max(0, subj_kv - kv_to)
619
639
  if saved > 0:
620
- self_ctx_pct = floor_pct
640
+ self_ctx_pct = to_pct
621
641
  remaining -= saved
622
642
  note_parts.append(
623
- f"self ctx {subj_ctx}%->{floor_pct}% freed {saved}B")
643
+ f"self ctx {subj_ctx}%->{to_pct}% freed {saved}B")
624
644
 
625
645
  if remaining <= 0:
626
646
  return FlexPlan("flex", self_ctx_pct=self_ctx_pct,
@@ -635,7 +655,7 @@ def plan_flex(subject: dict, residents: list, deficit_bytes: int) -> FlexPlan:
635
655
  for r in residents:
636
656
  if r.get("protected"):
637
657
  continue # protection outranks priority, always
638
- rb = ctx_band_bounds(r.get("ctx_pct"), r.get("ctx_deviation_pct"))
658
+ rb = ctx_band_bounds(r.get("ctx_pct"), r.get("ctx_deviation_pct"), r.get("ctx_floor_pct"))
639
659
  rkv = int(r.get("kv_bytes") or 0)
640
660
  if rb is None or rkv <= 0:
641
661
  continue # nothing to compress
@@ -0,0 +1,203 @@
1
+ """THE GGUF need function (2026-09-30): what a llama-server load of a GGUF puts
2
+ on the GPU and in host RAM, priced from the file's typed structure
3
+ (``hugpy_storage.gguf_inspect.gguf_structure``) — never a flat file size for a
4
+ MoE, never a ``layers x dense`` KV for a hybrid.
5
+
6
+ One source of truth: the worker's admission (``_incoming_need_detail``), the
7
+ allocation rows, central's placement preflight (``_worker_fit``), the planned
8
+ split on the worker row and the Ctx slider preview all call :func:`gguf_need`.
9
+
10
+ Placement follows llama.cpp exactly:
11
+ * ``--n-gpu-layers L`` puts blocks ``i >= n_block - L`` on the GPU; the
12
+ output tensor goes to the GPU only when ``L > n_block`` (``-1``/all does);
13
+ ``token_embd`` is ALWAYS on the CPU (llama.cpp's input device); a tied
14
+ output (no ``output.weight``) duplicates ``token_embd`` onto the output
15
+ device.
16
+ * ``--n-cpu-moe N`` keeps the expert tensors (``ffn_{up,down,gate,gate_up}_exps``)
17
+ of blocks ``i < N`` on the CPU; every always-on tensor of a GPU block stays
18
+ on the GPU.
19
+ * KV / recurrent state lives with its block (offload_kqv default): full
20
+ attention = ``ctx x n_head_kv x (k_len + v_len) x dtype``; sliding-window
21
+ blocks cap the cells at ``window + ubatch``; linear-attention (Gated
22
+ DeltaNet / Mamba) blocks hold a fixed f32 state per sequence, independent
23
+ of ctx.
24
+
25
+ The weights margin applies to the WEIGHTS of each side separately (measured
26
+ per side when a record exists, else the prior); KV, state, the compute
27
+ cushion and an mmproj projector are added unmultiplied. A dense model keeps
28
+ the historical pricing (file bytes x margin on the GPU when fully offloaded),
29
+ so dense admission is byte-for-byte unchanged.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ from typing import Any, Optional
34
+
35
+ COMPUTE_CUSHION_BYTES = 512 * 2 ** 20 # spill._CTX_COMPUTE_RESERVE_BYTES
36
+ SWA_UBATCH = 512 # llama.cpp default n_ubatch
37
+
38
+
39
+ def structure_for(path: Optional[str]) -> dict:
40
+ """``gguf_structure`` of the served file, ``{}`` when unreadable."""
41
+ if not path:
42
+ return {}
43
+ try:
44
+ from hugpy_storage.gguf_inspect import gguf_structure
45
+ return gguf_structure(path) or {}
46
+ except Exception: # noqa: BLE001 — a structure gap degrades to opaque pricing
47
+ return {}
48
+
49
+
50
+ def _layers(struct: dict) -> dict:
51
+ return {int(k): v for k, v in (struct.get("layers") or {}).items()}
52
+
53
+
54
+ def kv_breakdown(struct: dict, ctx: Optional[int], *, dtype_bytes: float = 2.0,
55
+ n_seq: int = 1, gpu_layers: Optional[set] = None) -> dict:
56
+ """Context cost by attention kind: ``{kv_bytes, kv_full_bytes, kv_swa_bytes,
57
+ state_bytes, layers: {full, swa, linear}, per_token_bytes}``; with
58
+ ``gpu_layers`` also the GPU-side share (``gpu_bytes``)."""
59
+ ctx = max(0, int(ctx or 0))
60
+ win = struct.get("swa_window")
61
+ swa_cells = min(ctx, int(win) + SWA_UBATCH) if win else ctx
62
+ full = swa = state = gpu = 0
63
+ per_tok = 0
64
+ counts = {"full": 0, "swa": 0, "linear": 0}
65
+ for i, L in _layers(struct).items():
66
+ kind = L.get("attn")
67
+ e = int(L.get("kv_elems_per_token") or 0)
68
+ b = 0
69
+ if kind == "full":
70
+ b = int(ctx * e * dtype_bytes)
71
+ full += b
72
+ per_tok += int(e * dtype_bytes)
73
+ elif kind == "swa":
74
+ b = int(swa_cells * e * dtype_bytes)
75
+ swa += b
76
+ elif kind == "linear":
77
+ b = int(L.get("state_bytes") or 0) * max(1, int(n_seq))
78
+ state += b
79
+ if kind in counts:
80
+ counts[kind] += 1
81
+ if gpu_layers is not None and i in gpu_layers:
82
+ gpu += b
83
+ out = {"ctx": ctx, "kv_bytes": full + swa + state, "kv_full_bytes": full,
84
+ "kv_swa_bytes": swa, "state_bytes": state, "layers": counts,
85
+ "per_token_bytes": per_tok, "swa_window": win}
86
+ if gpu_layers is not None:
87
+ out["gpu_bytes"] = gpu
88
+ return out
89
+
90
+
91
+ def gguf_need(struct: dict, *, ctx: Optional[int], n_cpu_moe: Optional[int] = 0,
92
+ n_gpu_layers: int = -1, gpu_margin: float = 1.15,
93
+ ram_margin: float = 1.15, dtype_bytes: float = 2.0,
94
+ cushion_bytes: int = COMPUTE_CUSHION_BYTES, mmproj_bytes: int = 0,
95
+ n_seq: int = 1) -> dict:
96
+ """GPU / RAM bytes of a llama-server load at ``ctx`` (see module doc)."""
97
+ layers = _layers(struct)
98
+ n_block = int(struct.get("block_count") or len(layers))
99
+ ngl = int(n_gpu_layers if n_gpu_layers is not None else -1)
100
+ if ngl < 0 or ngl > n_block:
101
+ start, out_on_gpu = 0, True
102
+ else:
103
+ start, out_on_gpu = n_block - ngl, False
104
+ gpu_layers = {i for i in layers if i >= start} if ngl != 0 else set()
105
+ is_moe = bool(struct.get("is_moe"))
106
+ ncm = max(0, int(n_cpu_moe or 0)) if is_moe else 0
107
+ gw = rw = 0
108
+ experts_gpu = experts_cpu = 0
109
+ for i, L in layers.items():
110
+ a, e = int(L.get("always_bytes") or 0), int(L.get("expert_bytes") or 0)
111
+ if i in gpu_layers:
112
+ gw += a
113
+ if i < ncm:
114
+ rw += e
115
+ experts_cpu += e
116
+ else:
117
+ gw += e
118
+ experts_gpu += e
119
+ else:
120
+ rw += a + e
121
+ experts_cpu += e
122
+ embd = int(struct.get("token_embd_bytes") or 0)
123
+ outb = int(struct.get("output_bytes") or 0)
124
+ glob = int(struct.get("global_bytes") or 0)
125
+ rw += embd # input layer: always CPU
126
+ if out_on_gpu:
127
+ gw += outb + glob + (embd if struct.get("output_tied") else 0)
128
+ else:
129
+ rw += outb + glob
130
+ file_bytes = int(struct.get("file_bytes") or 0)
131
+ if not is_moe and out_on_gpu and file_bytes:
132
+ # Dense, fully offloaded: the historical pricing (file x margin on the
133
+ # GPU) — dense admission stays byte-identical.
134
+ gw, rw = file_bytes, 0
135
+ kv = kv_breakdown(struct, ctx, dtype_bytes=dtype_bytes, n_seq=n_seq,
136
+ gpu_layers=gpu_layers)
137
+ kv_gpu = int(kv.get("gpu_bytes") or 0)
138
+ kv_cpu = int(kv["kv_bytes"]) - kv_gpu
139
+ # Compute cushion on the GPU for a MoE split (dense pricing never carried one).
140
+ cushion = int(cushion_bytes) if (gpu_layers and is_moe) else 0
141
+ gpu_total = int(gw * float(gpu_margin)) + kv_gpu + cushion + int(mmproj_bytes or 0)
142
+ ram_total = int(rw * float(ram_margin)) + kv_cpu
143
+ return {"n_cpu_moe": (min(ncm, n_block) if is_moe else None),
144
+ "n_gpu_layers": ngl, "block_count": n_block, "is_moe": is_moe,
145
+ "ctx": kv["ctx"],
146
+ "gpu_bytes": gpu_total, "ram_bytes": ram_total,
147
+ "gpu_weights_bytes": int(gw), "ram_weights_bytes": int(rw),
148
+ "gpu_margin": float(gpu_margin), "ram_margin": float(ram_margin),
149
+ "experts_gpu_bytes": int(experts_gpu), "experts_cpu_bytes": int(experts_cpu),
150
+ "kv_bytes": int(kv["kv_bytes"]), "kv_gpu_bytes": kv_gpu, "kv_cpu_bytes": kv_cpu,
151
+ "kv_full_bytes": kv["kv_full_bytes"], "kv_swa_bytes": kv["kv_swa_bytes"],
152
+ "state_bytes": kv["state_bytes"], "kv_layers": kv["layers"],
153
+ "cushion_bytes": cushion, "mmproj_bytes": int(mmproj_bytes or 0),
154
+ # mmap: the whole file is mapped; pages of GPU-uploaded tensors stay
155
+ # in page cache (RSS) until reclaimed — reclaimable, not a need.
156
+ "mmap_bytes": file_bytes}
157
+
158
+
159
+ def choose_n_cpu_moe(struct: dict, room_bytes: Optional[int], **kw) -> dict:
160
+ """The SMALLEST ``n_cpu_moe`` whose GPU side fits ``room_bytes`` whole
161
+ (GPU bytes fall monotonically as N grows). ``fits`` False -> every expert
162
+ on the CPU and the backbone still does not fit (N = block_count)."""
163
+ n_block = int(struct.get("block_count") or 0)
164
+ if room_bytes is None:
165
+ need = gguf_need(struct, n_cpu_moe=n_block, **kw)
166
+ return dict(need, fits=None, choice="unmeasurable card — all experts on CPU")
167
+ for n in range(0, n_block + 1):
168
+ need = gguf_need(struct, n_cpu_moe=n, **kw)
169
+ if need["gpu_bytes"] <= int(room_bytes):
170
+ return dict(need, fits=True, room_bytes=int(room_bytes),
171
+ choice=f"smallest n_cpu_moe fitting {int(room_bytes)} B")
172
+ need = gguf_need(struct, n_cpu_moe=n_block, **kw)
173
+ return dict(need, fits=False, room_bytes=int(room_bytes),
174
+ choice="backbone does not fit even with every expert on CPU")
175
+
176
+
177
+ def whole_seat_ctx(need_at, room: int, upper: int, floor: int = 4096) -> "tuple[int, bool]":
178
+ """Largest ctx (1024 multiple in [floor, upper]) with ``need_at(ctx) <= room``."""
179
+ upper = max(int(floor), int(upper))
180
+ top = upper if upper < 1024 else (upper // 1024) * 1024
181
+ if need_at(top) <= room:
182
+ return top, True
183
+ lo, hi, best = -(-int(floor) // 1024), top // 1024, None
184
+ while lo <= hi:
185
+ mid = (lo + hi) // 2
186
+ if need_at(mid * 1024) <= room:
187
+ best, lo = mid * 1024, mid + 1
188
+ else:
189
+ hi = mid - 1
190
+ if best is None:
191
+ return int(floor), need_at(int(floor)) <= room
192
+ return max(int(floor), best), True
193
+
194
+
195
+ def verdict_text(need: dict) -> str:
196
+ """"MoE: 12/48 layers' experts on CPU; GPU 17.2 GiB, RAM 21.0 GiB" (+ KV)."""
197
+ g = 2 ** 30
198
+ if not need.get("is_moe"):
199
+ return (f"GPU {need['gpu_bytes'] / g:.1f} GiB, RAM {need['ram_bytes'] / g:.1f} GiB "
200
+ f"(KV {need['kv_bytes'] / g:.2f} GiB @ {need['ctx']})")
201
+ return (f"MoE: {need['n_cpu_moe']}/{need['block_count']} layers' experts on CPU; "
202
+ f"GPU {need['gpu_bytes'] / g:.1f} GiB, RAM {need['ram_bytes'] / g:.1f} GiB "
203
+ f"(KV {need['kv_bytes'] / g:.2f} GiB @ {need['ctx']} ctx)")