inline-core 1.2.52__tar.gz → 1.2.61__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 (253) hide show
  1. {inline_core-1.2.52 → inline_core-1.2.61}/CLAUDE.md +85 -4
  2. {inline_core-1.2.52 → inline_core-1.2.61}/PKG-INFO +16 -6
  3. {inline_core-1.2.52 → inline_core-1.2.61}/README.md +8 -3
  4. {inline_core-1.2.52 → inline_core-1.2.61}/pyproject.toml +22 -4
  5. inline_core-1.2.61/scripts/flux2_train_matrix.py +204 -0
  6. inline_core-1.2.61/src/inline_core/__init__.py +14 -0
  7. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/memory.py +16 -4
  8. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/policy.py +5 -1
  9. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/cache.py +35 -2
  10. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/executor.py +16 -4
  11. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/registry.py +4 -0
  12. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/schema.py +5 -1
  13. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/catalog.py +21 -0
  14. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/checkpoint.py +5 -0
  15. inline_core-1.2.61/src/inline_core/models/controlspace.py +26 -0
  16. inline_core-1.2.61/src/inline_core/models/flux2/__init__.py +1 -0
  17. inline_core-1.2.61/src/inline_core/models/flux2/controlnet.py +234 -0
  18. inline_core-1.2.61/src/inline_core/models/flux2/embeds.py +165 -0
  19. inline_core-1.2.61/src/inline_core/models/flux2/provider.py +84 -0
  20. inline_core-1.2.61/src/inline_core/models/flux2/requirements.py +414 -0
  21. inline_core-1.2.61/src/inline_core/models/flux2/runner.py +677 -0
  22. inline_core-1.2.61/src/inline_core/models/flux2/variants.py +334 -0
  23. inline_core-1.2.61/src/inline_core/models/keymap.py +304 -0
  24. inline_core-1.2.61/src/inline_core/models/krea2/depth_control.py +149 -0
  25. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/img2img.py +2 -2
  26. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/provider.py +11 -0
  27. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/requirements.py +75 -1
  28. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/runner.py +65 -6
  29. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/loaders.py +574 -7
  30. inline_core-1.2.61/src/inline_core/models/minimaxh3/__init__.py +7 -0
  31. inline_core-1.2.61/src/inline_core/models/minimaxh3/adaln.py +138 -0
  32. inline_core-1.2.61/src/inline_core/models/minimaxh3/keys.py +134 -0
  33. inline_core-1.2.61/src/inline_core/models/minimaxh3/load.py +286 -0
  34. inline_core-1.2.61/src/inline_core/models/minimaxh3/pipeline.py +757 -0
  35. inline_core-1.2.61/src/inline_core/models/minimaxh3/provider.py +102 -0
  36. inline_core-1.2.61/src/inline_core/models/minimaxh3/requirements.py +249 -0
  37. inline_core-1.2.61/src/inline_core/models/minimaxh3/runner.py +406 -0
  38. inline_core-1.2.61/src/inline_core/models/minimaxh3/vae_keys.py +125 -0
  39. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/__init__.py +46 -0
  40. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +922 -0
  41. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +679 -0
  42. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/before_denoise.py +425 -0
  43. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/before_encoder.py +408 -0
  44. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/decoders.py +199 -0
  45. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/denoise.py +325 -0
  46. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/encoders.py +639 -0
  47. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +345 -0
  48. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +115 -0
  49. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/packing.py +538 -0
  50. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +841 -0
  51. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +288 -0
  52. inline_core-1.2.61/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +644 -0
  53. inline_core-1.2.61/src/inline_core/models/offload.py +281 -0
  54. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/pipeline_runtime.py +149 -20
  55. inline_core-1.2.61/src/inline_core/models/prepared.py +151 -0
  56. inline_core-1.2.61/src/inline_core/models/preprocess/__init__.py +5 -0
  57. inline_core-1.2.61/src/inline_core/models/preprocess/requirements.py +59 -0
  58. inline_core-1.2.61/src/inline_core/models/preprocess/runner.py +174 -0
  59. inline_core-1.2.61/src/inline_core/models/references.py +125 -0
  60. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/requirements.py +57 -2
  61. inline_core-1.2.61/src/inline_core/models/video_params.py +144 -0
  62. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/zimage/provider.py +12 -0
  63. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/zimage/requirements.py +90 -4
  64. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/zimage/runner.py +72 -8
  65. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/runtime/file_store.py +36 -6
  66. inline_core-1.2.61/src/inline_core/runtime/store.py +45 -0
  67. inline_core-1.2.61/src/inline_core/runtime/video_encode.py +204 -0
  68. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/app.py +6 -4
  69. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/bootstrap.py +35 -0
  70. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/serialize.py +28 -4
  71. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/fal.py +43 -5
  72. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/frames.py +44 -18
  73. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/generation.py +63 -9
  74. inline_core-1.2.61/src/inline_core/studio/graph_build.py +326 -0
  75. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/handlers.py +43 -6
  76. inline_core-1.2.61/src/inline_core/studio/image_meta.py +36 -0
  77. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/models.py +38 -4
  78. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/moodboard.py +52 -3
  79. inline_core-1.2.61/src/inline_core/studio/recipe.py +109 -0
  80. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/schema.py +8 -2
  81. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/arch.py +107 -4
  82. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/dataset.py +54 -3
  83. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/models.py +101 -2
  84. {inline_core-1.2.52 → inline_core-1.2.61}/tests/helpers.py +1 -0
  85. inline_core-1.2.61/tests/test_cache.py +116 -0
  86. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_executor.py +26 -1
  87. inline_core-1.2.61/tests/test_flux2_controlnet.py +159 -0
  88. inline_core-1.2.61/tests/test_flux2_folder.py +179 -0
  89. inline_core-1.2.61/tests/test_flux2_resolve.py +176 -0
  90. inline_core-1.2.61/tests/test_flux2_runner.py +126 -0
  91. inline_core-1.2.61/tests/test_flux2_training.py +199 -0
  92. inline_core-1.2.61/tests/test_flux2_variants.py +129 -0
  93. inline_core-1.2.61/tests/test_keymap.py +268 -0
  94. inline_core-1.2.61/tests/test_krea2_depth_control.py +104 -0
  95. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_krea2_requirements.py +35 -2
  96. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_krea2_runner.py +4 -3
  97. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_loaders.py +35 -0
  98. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_memory_policy.py +39 -0
  99. inline_core-1.2.61/tests/test_minimaxh3_adaln.py +159 -0
  100. inline_core-1.2.61/tests/test_minimaxh3_keys.py +138 -0
  101. inline_core-1.2.61/tests/test_minimaxh3_load.py +257 -0
  102. inline_core-1.2.61/tests/test_minimaxh3_nodes.py +335 -0
  103. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_model_requirements.py +25 -0
  104. inline_core-1.2.61/tests/test_offload_prepared.py +233 -0
  105. inline_core-1.2.61/tests/test_output_kind_contract.py +67 -0
  106. inline_core-1.2.61/tests/test_pipeline_cache.py +82 -0
  107. inline_core-1.2.61/tests/test_recipe.py +100 -0
  108. inline_core-1.2.61/tests/test_references.py +84 -0
  109. inline_core-1.2.61/tests/test_staged_residency.py +101 -0
  110. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_fal.py +46 -0
  111. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_generation.py +126 -3
  112. inline_core-1.2.61/tests/test_studio_graph_build.py +111 -0
  113. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_models.py +20 -0
  114. inline_core-1.2.61/tests/test_studio_multi_reference.py +163 -0
  115. inline_core-1.2.61/tests/test_studio_node_size.py +58 -0
  116. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_rpc.py +17 -0
  117. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_schema.py +19 -0
  118. inline_core-1.2.61/tests/test_video_encode.py +158 -0
  119. inline_core-1.2.61/tests/test_video_params.py +114 -0
  120. inline_core-1.2.61/tests/test_webui_install.py +154 -0
  121. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_zimage_resolve.py +6 -2
  122. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_zimage_runner.py +109 -13
  123. {inline_core-1.2.52 → inline_core-1.2.61}/uv.lock +260 -3
  124. {inline_core-1.2.52 → inline_core-1.2.61}/webui.bat +110 -16
  125. {inline_core-1.2.52 → inline_core-1.2.61}/webui.sh +96 -18
  126. inline_core-1.2.52/src/inline_core/__init__.py +0 -7
  127. inline_core-1.2.52/src/inline_core/runtime/store.py +0 -18
  128. inline_core-1.2.52/src/inline_core/studio/graph_build.py +0 -141
  129. inline_core-1.2.52/tests/test_cache.py +0 -48
  130. {inline_core-1.2.52 → inline_core-1.2.61}/.gitignore +0 -0
  131. {inline_core-1.2.52 → inline_core-1.2.61}/.python-version +0 -0
  132. {inline_core-1.2.52 → inline_core-1.2.61}/main.py +0 -0
  133. {inline_core-1.2.52 → inline_core-1.2.61}/scripts/reference.py +0 -0
  134. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/components/__init__.py +0 -0
  135. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/components/conditioning.py +0 -0
  136. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/components/interfaces.py +0 -0
  137. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/config.py +0 -0
  138. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/__init__.py +0 -0
  139. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/auto.py +0 -0
  140. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/detect.py +0 -0
  141. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/device/types.py +0 -0
  142. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/errors.py +0 -0
  143. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/__init__.py +0 -0
  144. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/api.py +0 -0
  145. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/constraints.py +0 -0
  146. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/fetch.py +0 -0
  147. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/handlers.py +0 -0
  148. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/importer.py +0 -0
  149. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/install.py +0 -0
  150. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/loader.py +0 -0
  151. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/manifest.py +0 -0
  152. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/models.py +0 -0
  153. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/paths.py +0 -0
  154. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/resolve.py +0 -0
  155. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/scanner.py +0 -0
  156. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/state.py +0 -0
  157. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/extensions/tools.py +0 -0
  158. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/ffmpeg.py +0 -0
  159. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/__init__.py +0 -0
  160. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/descriptor.py +0 -0
  161. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/loader_runners.py +0 -0
  162. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/primitives.py +0 -0
  163. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/runners.py +0 -0
  164. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/topo.py +0 -0
  165. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/graph/validate.py +0 -0
  166. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/media.py +0 -0
  167. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/__init__.py +0 -0
  168. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/__init__.py +0 -0
  169. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/krea2/convert.py +0 -0
  170. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/lora.py +0 -0
  171. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/sampling.py +0 -0
  172. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/zimage/__init__.py +0 -0
  173. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/models/zimage/primitives.py +0 -0
  174. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/__init__.py +0 -0
  175. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/config.py +0 -0
  176. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/group.py +0 -0
  177. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/launch.py +0 -0
  178. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/protocol.py +0 -0
  179. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/registry.py +0 -0
  180. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/parallel/worker.py +0 -0
  181. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/runtime/__init__.py +0 -0
  182. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/runtime/context.py +0 -0
  183. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/runtime/progress.py +0 -0
  184. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/runtime/run.py +0 -0
  185. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/sampling/__init__.py +0 -0
  186. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/sampling/batch.py +0 -0
  187. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/__init__.py +0 -0
  188. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/__main__.py +0 -0
  189. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/assets.py +0 -0
  190. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/frontend.py +0 -0
  191. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/manager.py +0 -0
  192. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/rpc.py +0 -0
  193. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/server/run_store.py +0 -0
  194. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/__init__.py +0 -0
  195. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/assets.py +0 -0
  196. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/config.py +0 -0
  197. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/peaks.py +0 -0
  198. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/store.py +0 -0
  199. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/system_stats.py +0 -0
  200. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/timeline/__init__.py +0 -0
  201. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/timeline/compose.py +0 -0
  202. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  203. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/timeline/render.py +0 -0
  204. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/timeline/resolve.py +0 -0
  205. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/training.py +0 -0
  206. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/studio/training_store.py +0 -0
  207. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/takes.py +0 -0
  208. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/__init__.py +0 -0
  209. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/__main__.py +0 -0
  210. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/caption.py +0 -0
  211. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/protocol.py +0 -0
  212. {inline_core-1.2.52 → inline_core-1.2.61}/src/inline_core/training/trainer.py +0 -0
  213. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_catalog.py +0 -0
  214. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_checkpoint.py +0 -0
  215. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_config.py +0 -0
  216. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_device_detect.py +0 -0
  217. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_api.py +0 -0
  218. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_install.py +0 -0
  219. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_manifest.py +0 -0
  220. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_resolve.py +0 -0
  221. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_scanner.py +0 -0
  222. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_spine.py +0 -0
  223. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_extension_state.py +0 -0
  224. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_file_store.py +0 -0
  225. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_frontend_serving.py +0 -0
  226. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_hidden_nodes.py +0 -0
  227. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_krea2_convert.py +0 -0
  228. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_loader_runners.py +0 -0
  229. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_lora.py +0 -0
  230. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_lora_download.py +0 -0
  231. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_parallel_group.py +0 -0
  232. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_primitives.py +0 -0
  233. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_rpc_bridge.py +0 -0
  234. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_run_store.py +0 -0
  235. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_sampling.py +0 -0
  236. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_schema.py +0 -0
  237. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_server.py +0 -0
  238. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_assets.py +0 -0
  239. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_frames.py +0 -0
  240. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_moodboard.py +0 -0
  241. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_peaks.py +0 -0
  242. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_store.py +0 -0
  243. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_timeline.py +0 -0
  244. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_studio_training.py +0 -0
  245. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_take_bytes.py +0 -0
  246. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_topo.py +0 -0
  247. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_training_arch.py +0 -0
  248. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_training_dataset.py +0 -0
  249. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_training_models.py +0 -0
  250. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_training_resolve.py +0 -0
  251. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_validate.py +0 -0
  252. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_xfuser_sampler.py +0 -0
  253. {inline_core-1.2.52 → inline_core-1.2.61}/tests/test_zimage_primitives.py +0 -0
@@ -95,7 +95,16 @@ HTTP/WS → server/app.py → RunManager → Executor → Registry (desc
95
95
  the ComfyUI-equivalent decomposed graph and the intended long-term surface. **Descriptor-only
96
96
  today - their runners land in C2.** A graph built from them validates and type-checks but raises
97
97
  `No runner registered` at execution.
98
- - **Model runners** (`models/<name>/`) - high-level single generation nodes. **`alibaba/z-image-turbo`
98
+ - **Model runners** (`models/<name>/`) - high-level single generation nodes. **`black-forest-labs/flux-2`
99
+ (`models/flux2/`) is the reference for a _family_:** one node covers klein 4B/9B, their base builds,
100
+ the KV variant and dev, because the picked checkpoint is identified from its own tensor shapes
101
+ (`flux2/variants.py`) and everything else follows. Adding a checkpoint is a row in that table.
102
+ Two rules it establishes: **identify a checkpoint by content, never by filename** (`diffusion_models/`
103
+ is shared across architectures, and Qwen3-4B and Qwen3-VL-4B have byte-identical embedding shapes),
104
+ and **derive geometry from the checkpoint** rather than bundling a config per variant, so a future
105
+ build loads with no code change. A **diffusers folder** is a valid checkpoint too, which is the only
106
+ way a 32B model reaches a 24 GB card: its NF4 shards stream through `from_pretrained`, where
107
+ `from_single_file` cannot quantize at all. **`alibaba/z-image-turbo`
99
108
  (`models/zimage/`) is the one runnable generation path today** (prompt + optional image → one take,
100
109
  backed by diffusers' `ZImagePipeline`/`ZImageImg2ImgPipeline`). This is the "Z-Image pipeline that
101
110
  already works" - the primitives will reach parity in C2. It loads from a **single diffusion
@@ -112,6 +121,11 @@ between nodes and are never takes.
112
121
 
113
122
  ### Storage & configuration (all env, see `config.py`)
114
123
 
124
+ - **Never put weights on an instance's ephemeral/scratch disk.** On cloud GPU boxes (`/opt/dlami/nvme`,
125
+ `/mnt/resource`, and anything else labelled ephemeral or instance-store) the volume is **wiped on
126
+ every stop/start**, taking the models and leaving dangling symlinks in `models/`. This has cost two
127
+ full re-downloads of MiniMax H3 at ~130 GB each. Weights go on the persistent root volume, or on an
128
+ attached volume that survives a restart. Scratch is fine for logs and temporary output only.
115
129
  - **Models root** - `INLINE_MODELS_DIR`, else `./models`. **Bring your own weights; nothing is
116
130
  downloaded.** ComfyUI-style category subfolders (`diffusion_models/`, `vae/`, `text_encoders/`,
117
131
  `loras/`, `controlnet/`, `checkpoints/`, `clip_vision/`, `upscale_models/`, `embeddings/`). The
@@ -143,6 +157,18 @@ between nodes and are never takes.
143
157
  (Turing/Volta - compute capability < 8.0, e.g. a T4): same footprint, but it uses the fp16 tensor
144
158
  cores instead of bf16's slow path. The **VAE stays upcast** (bf16, or fp32 when the denoiser is
145
159
  fp16) because fp16 VAE decode can overflow to black/NaN images (`_compute_dtype` in `device/`).
160
+ - **Quantization rungs** - the fit ladder is full-precision resident → int8 resident → **NF4 resident**
161
+ → sequential offload → wont-fit. NF4 is what makes a 32B model viable on a 24 GB card. Note int8
162
+ forces bf16 (torchao's weight-only int8 silently no-ops under fp16) while NF4 does not, so a Turing
163
+ card keeps its fp16 tensor cores under NF4.
164
+ - **Prequantized checkpoints must not be re-quantized.** A checkpoint that ships already quantized
165
+ (`flux2/variants.is_prequantized`) has an on-disk size that already _is_ its resident size, so the
166
+ ladder's assumption that quantization halves it does not hold, and handing diffusers a second,
167
+ different quantization config is a hard error. Pass `Quantization.NONE` for those.
168
+ - **Staged loading** - when the text encoder and the transformer cannot be co-resident (dev is a
169
+ 15 GB encoder beside an 18 GB transformer), the prompt is encoded first and the encoder freed before
170
+ the transformer loads. The decision has to be made _before_ the load: by the time an OOM fires there
171
+ is nothing left to free.
146
172
  - **Multi-GPU** - `INLINE_PARALLEL` (e.g. `pipefusion=2`, `pipefusion=2,ulysses=2`); degrees multiply
147
173
  to the world size, which must equal the GPU count.
148
174
 
@@ -202,6 +228,11 @@ real codec that moves tensors lives with the model runner.
202
228
  Don't scatter it.
203
229
  - **Bring-your-own models.** Nothing is downloaded by the engine. The catalog scans; the user places
204
230
  files. A model picker is a `SELECT` param with `options_from="<category>"`.
231
+ - **Verify image models by rendering.** The FLUX.2 work shipped five bugs past a green test suite,
232
+ and every one produced a _wrong image rather than an error_: a mis-keyed checkpoint, a
233
+ vision-language encoder loaded in place of a text one, an unnormalized latent, and a control context
234
+ cast to a quantized weight's `uint8` storage dtype. A unit test cannot see a plausible-but-wrong
235
+ image. Render something and look at it.
205
236
  - **Tests (pytest).** Cover the logic that matters: graph validate/topo/executor/cache, the catalog
206
237
  scan, the run store + server contract, the device/memory policy, the parallel group + xfuser seam,
207
238
  and each model runner (import-guarded, no GPU needed). See `tests/`.
@@ -211,14 +242,17 @@ real codec that moves tensors lives with the model runner.
211
242
 
212
243
  ```
213
244
  uv venv # create ./.venv
214
- uv pip install -e ".[server,dev]" # engine + server + test tooling
215
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (real generation)
216
- uv pip install -e ".[runtime,parallel]" # + xfuser, for multi-GPU denoise (2+ GPUs)
245
+ # --python pins the target: without it uv installs into an activated venv/conda env, not ./.venv
246
+ uv pip install --python .venv/bin/python -e ".[server,dev]" # engine + server + test tooling
247
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
248
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, multi-GPU denoise
217
249
 
218
250
  ./webui.sh # run (loopback:8848); friendly flags → INLINE_* env
219
251
  ./webui.sh --listen --port 9000 # bind all interfaces
220
252
  ./webui.sh --lowvram # tight-VRAM profile
221
253
  ./webui.sh --install --extra runtime # set up ./.venv with the model runtime, then exit
254
+ # (reuses an existing ./.venv; --recreate rebuilds it, and
255
+ # an activated foreign env is reported, never modified)
222
256
  python -m inline_core.server # run the server directly (INLINE_HOST / INLINE_PORT)
223
257
 
224
258
  ruff check . # lint (zero warnings)
@@ -227,6 +261,53 @@ uv run pytest -q # tests (no GPU; model code is import-
227
261
 
228
262
  ## Where to add things
229
263
 
264
+ - **New video model** → do **not** rediscover the plumbing; it exists as shared seams, and
265
+ `models/minimaxh3/` is the reference caller:
266
+ - `runtime/video_encode.py` + `TakeStore.save_video` / `save_audio` - frames plus a waveform to one
267
+ playable MP4. A model that generates video and its soundtrack jointly returns **both** takes;
268
+ only the one matching the descriptor's `output_kind` claims the node's canvas slot (see
269
+ `studio/generation._save_take`, and `tests/test_output_kind_contract.py` which keeps the
270
+ declarations honest).
271
+ - `models/video_params.py` - `VideoGrid` + `video_param_fields(...)`, the way
272
+ `sampling_param_fields(...)` already works. A duration snaps **up** onto the model's frame grid
273
+ and is then clamped into the model's window, which is what both reference implementations do:
274
+ asking H3 for 10 seconds gives 10.125, not 9.417. **fps is a model constant, never a param**,
275
+ or it desyncs from the grid.
276
+ - `models/references.py` - wired image/video/audio ports as one ordered, numbered list. Wiring
277
+ order is what the prompt addresses, so it is meaning, not decoration.
278
+ - `models/keymap.py` - load a checkpoint written for another implementation. Declare a key plan
279
+ (rename / split / swap halves / drop / assert-equal) and apply it while weights stream in. The
280
+ transforms it performs are the ones that fail **silently**, so a plan declares its expected row
281
+ layout and the detector measures the real one. It needs the **whole tensor**: one head's worth of
282
+ rows cannot tell the layouts apart, and it raises rather than guessing.
283
+ - `models/prepared.py` - cache a quantised model once. Everything that changes the bytes goes in
284
+ the hash, including model-specific flags, or switching a flag serves a stale artifact.
285
+ - `models/offload.py` - the device policy's plan to a concrete torchao + group-offload recipe,
286
+ plus **split residency**: group offload holds the whole model in host RAM, so a model bigger
287
+ than the RAM available has nowhere to sit and the kernel swaps it. `blocks_to_place` sizes the
288
+ overflow and those leading blocks go on the accelerator instead, placed as they land rather
289
+ than after the load. It moves the minimum, because every block left resident is VRAM the
290
+ render wanted for activations.
291
+ - **A group-offloaded model's host footprint is reclaimable at load and unreclaimable one step
292
+ later, so never size a split from free memory during the load.** Streaming from a safetensors
293
+ mmap leaves the CPU-side weights as clean file-backed pages the kernel can drop and re-read for
294
+ free. The first denoising step ends that: group offload returns each block with
295
+ `module.to("cpu")`, which allocates fresh anonymous memory and drops the file-backed storage.
296
+ A planner reading `available` mid-load is reading a number that is about to stop being true,
297
+ and the failure mode is not an exception. It is the machine resetting with the page cache
298
+ converted out from under it, no OOM message and no shutdown sequence. Budget the full
299
+ post-conversion footprint, and count what other components will claim from the same RAM
300
+ afterwards (a leaf-offloaded VAE lands there too).
301
+ - **Ordering, when a load both transforms and quantises:** structural transform first,
302
+ quantisation last, and a prequantized source takes no structural transform at all. The three
303
+ clauses and why they are not negotiable are in `models/offload.py`'s docstring.
304
+ - **Vendoring unreleased upstream code** → `models/<name>/vendor/`, **verbatim, import rewrites
305
+ only**. Put a provenance header in its `__init__.py` naming the repo, PR, branch, commit sha and
306
+ date. `models/*/vendor/` is already excluded from ruff and pyright by a glob, because editing it to
307
+ satisfy our linters destroys the one property that makes a re-sync reviewable. Never patch
308
+ installed diffusers: construct components directly and pass them in, so nothing resolves a class by
309
+ name through a registry we do not own. Pin, don't floor, any dependency whose experimental surface
310
+ the vendored code imports from.
230
311
  - **New model runner** → a subpackage `models/<name>/` with `runner.py` (a `NodeDescriptor` + a
231
312
  `NodeRunner` + `register_<name>(registry, store, policy)`) and an `__init__.py` re-exporting it;
232
313
  add a `try/except ImportError` block in `server/bootstrap.py`; add an optional-deps extra in
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inline-core
3
- Version: 1.2.52
3
+ Version: 1.2.61
4
4
  Summary: The generation engine behind Inline Studio.
5
5
  License-Expression: GPL-3.0-or-later
6
6
  Requires-Python: >=3.11
@@ -9,12 +9,14 @@ Requires-Dist: psutil>=5.9
9
9
  Provides-Extra: all
10
10
  Requires-Dist: accelerate>=0.30; extra == 'all'
11
11
  Requires-Dist: bitsandbytes>=0.43; (platform_system != 'Darwin') and extra == 'all'
12
- Requires-Dist: diffusers>=0.39; extra == 'all'
12
+ Requires-Dist: controlnet-aux>=0.0.7; extra == 'all'
13
+ Requires-Dist: diffusers==0.39.0; extra == 'all'
13
14
  Requires-Dist: einops>=0.7; extra == 'all'
14
15
  Requires-Dist: fastapi>=0.110; extra == 'all'
15
16
  Requires-Dist: huggingface-hub>=0.23; extra == 'all'
16
17
  Requires-Dist: imageio-ffmpeg>=0.4; extra == 'all'
17
18
  Requires-Dist: nvidia-ml-py>=12; extra == 'all'
19
+ Requires-Dist: onnxruntime>=1.17; extra == 'all'
18
20
  Requires-Dist: peft>=0.11; extra == 'all'
19
21
  Requires-Dist: pillow>=10; extra == 'all'
20
22
  Requires-Dist: psutil>=5.9; extra == 'all'
@@ -35,8 +37,11 @@ Requires-Dist: nvidia-ml-py>=12; extra == 'parallel'
35
37
  Requires-Dist: xfuser>=0.4; extra == 'parallel'
36
38
  Provides-Extra: runtime
37
39
  Requires-Dist: accelerate>=0.30; extra == 'runtime'
38
- Requires-Dist: diffusers>=0.39; extra == 'runtime'
40
+ Requires-Dist: av>=12; extra == 'runtime'
41
+ Requires-Dist: controlnet-aux>=0.0.7; extra == 'runtime'
42
+ Requires-Dist: diffusers==0.39.0; extra == 'runtime'
39
43
  Requires-Dist: huggingface-hub>=0.23; extra == 'runtime'
44
+ Requires-Dist: onnxruntime>=1.17; extra == 'runtime'
40
45
  Requires-Dist: safetensors>=0.4; extra == 'runtime'
41
46
  Requires-Dist: scipy>=1.11; extra == 'runtime'
42
47
  Requires-Dist: torch>=2.2; extra == 'runtime'
@@ -97,13 +102,18 @@ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the grap
97
102
 
98
103
  Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
99
104
 
105
+ `--python` pins each install to `./.venv`; without it uv targets whatever venv or conda env is
106
+ activated in your shell, and installs land there instead.
107
+
100
108
  ```
101
109
  uv venv
102
- uv pip install -e ".[server]" # engine + HTTP/websocket API
103
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (for real generation)
104
- uv pip install -e ".[runtime,parallel]" # + xfuser, to split one image across GPUs (2+ GPUs)
110
+ uv pip install --python .venv/bin/python -e ".[server]" # engine + HTTP/websocket API
111
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
112
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, 2+ GPUs
105
113
  ```
106
114
 
115
+ Or let the launcher do it: `./webui.sh --install --extra runtime` (Windows: `.\webui.bat`).
116
+
107
117
  ## Models
108
118
 
109
119
  Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
@@ -39,13 +39,18 @@ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the grap
39
39
 
40
40
  Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
41
41
 
42
+ `--python` pins each install to `./.venv`; without it uv targets whatever venv or conda env is
43
+ activated in your shell, and installs land there instead.
44
+
42
45
  ```
43
46
  uv venv
44
- uv pip install -e ".[server]" # engine + HTTP/websocket API
45
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (for real generation)
46
- uv pip install -e ".[runtime,parallel]" # + xfuser, to split one image across GPUs (2+ GPUs)
47
+ uv pip install --python .venv/bin/python -e ".[server]" # engine + HTTP/websocket API
48
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
49
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, 2+ GPUs
47
50
  ```
48
51
 
52
+ Or let the launcher do it: `./webui.sh --install --extra runtime` (Windows: `.\webui.bat`).
53
+
49
54
  ## Models
50
55
 
51
56
  Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  # PyPI name; the import package is `inline_core` (src/inline_core).
3
3
  name = "inline-core"
4
- version = "1.2.52"
4
+ version = "1.2.61"
5
5
  description = "The generation engine behind Inline Studio."
6
6
  readme = "README.md"
7
7
  license = "GPL-3.0-or-later"
@@ -18,7 +18,9 @@ runtime = [
18
18
  # A bare `torch` is CPU-only on Windows; see [tool.uv] below and requirements.txt.
19
19
  "torch>=2.2",
20
20
  # Krea 2 needs diffusers >= 0.39 (Krea2Pipeline); Z-Image needs >= 0.36 (ZImagePipeline).
21
- "diffusers>=0.39",
21
+ # Pinned, not floored: MiniMax H3 is vendored from an unmerged PR and imports six symbols
22
+ # from the experimental Modular Diffusers surface, which a minor release may rename.
23
+ "diffusers==0.39.0",
22
24
  "transformers>=4.44",
23
25
  "accelerate>=0.30",
24
26
  "safetensors>=0.4",
@@ -28,6 +30,14 @@ runtime = [
28
30
  "scipy>=1.11",
29
31
  # We call snapshot_download directly for the model popup, so pin it rather than rely on transit.
30
32
  "huggingface_hub>=0.23",
33
+ # ControlNet preprocessors (the Apply ControlNet node): OpenPose/DWPose, MiDaS/Zoe depth, canny,
34
+ # HED, lineart, MLSD, scribble, normal. DWPose runs its detector on ONNX Runtime.
35
+ "controlnet-aux>=0.0.7",
36
+ "onnxruntime>=1.17",
37
+ # MiniMax H3's reference node decodes a wired video or audio clip when it builds the reference.
38
+ # The blocks raise a plain ImportError without it, so the node would advertise two ports it
39
+ # cannot read. Video output goes out through ffmpeg, not this.
40
+ "av>=12",
31
41
  ]
32
42
  server = [
33
43
  "fastapi>=0.110",
@@ -67,13 +77,15 @@ dev = [
67
77
  all = [
68
78
  # runtime
69
79
  "torch>=2.2",
70
- "diffusers>=0.39",
80
+ "diffusers==0.39.0",
71
81
  "transformers>=4.44",
72
82
  "accelerate>=0.30",
73
83
  "safetensors>=0.4",
74
84
  "torchao>=0.14",
75
85
  "scipy>=1.11",
76
86
  "huggingface_hub>=0.23",
87
+ "controlnet-aux>=0.0.7",
88
+ "onnxruntime>=1.17",
77
89
  # server
78
90
  "fastapi>=0.110",
79
91
  "uvicorn[standard]>=0.29",
@@ -110,15 +122,21 @@ packages = ["src/inline_core"]
110
122
  [tool.ruff]
111
123
  line-length = 100
112
124
  target-version = "py311"
125
+ # Vendored upstream code (see models/minimaxh3/vendor/__init__.py). Editing it to satisfy our
126
+ # linters would destroy the one property that makes a re-sync reviewable: it is verbatim.
127
+ extend-exclude = ["src/inline_core/models/*/vendor"]
113
128
 
114
129
  [tool.ruff.lint]
115
130
  select = ["E", "F", "I", "UP", "B"]
116
131
 
117
132
  [tool.pyright]
118
133
  include = ["src", "tests"]
134
+ exclude = ["**/models/*/vendor"]
119
135
  pythonVersion = "3.11"
120
136
  typeCheckingMode = "strict"
121
137
 
122
138
  [tool.pytest.ini_options]
123
139
  testpaths = ["tests"]
124
- pythonpath = ["src"]
140
+ # "." so `tests` imports as a package: several suites share fixtures via `from tests.x import y`,
141
+ # and without it those modules fail to collect and silently stop running.
142
+ pythonpath = ["src", "."]
@@ -0,0 +1,204 @@
1
+ """VRAM + step-time sweep for FLUX.2 LoRA training: the cells behind the README benchmark table.
2
+
3
+ cd core && PYTHONPATH=src .venv/bin/python scripts/flux2_train_matrix.py --dataset <dir>
4
+
5
+ One cell = one real run of `python -m inline_core.training`, the same entry point the Trainer tab
6
+ spawns, so a number here is a number a user would see. Anything else (importing `train` in-process,
7
+ or a hand-rolled loop) would measure a different program.
8
+
9
+ Held fixed at the settings the existing Z-Image and Krea 2 rows used: 12 steps, rank 16, batch 1,
10
+ gradient checkpointing on. What varies is resolution and base precision. Peak VRAM is the trainer's
11
+ own `torch.cuda.max_memory_allocated` reading off the last progress line; an OOM is recorded as a
12
+ cell rather than aborting the sweep, because "does not fit" is a result the table needs.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ _REPO = Path(__file__).resolve().parent.parent.parent
26
+ _CORE = _REPO / "core"
27
+ _DEFAULT_OUT = _REPO / "outputs" / "flux2-train-matrix"
28
+
29
+ # (resolution, baseQuant). `none` is the bf16 base; `nf4` is the 4-bit (QLoRA) base.
30
+ CELLS: tuple[tuple[int, str], ...] = (
31
+ (512, "none"),
32
+ (512, "nf4"),
33
+ (1024, "none"),
34
+ (1024, "nf4"),
35
+ )
36
+
37
+ _STEPS = 12
38
+ _RANK = 16
39
+
40
+
41
+ def _manifest(work: Path, dataset: Path, models: Path, resolution: int, quant: str) -> Path:
42
+ """The same manifest shape `studio/training.py::_prepare` writes."""
43
+ checkpoints = work / "checkpoints"
44
+ checkpoints.mkdir(parents=True, exist_ok=True)
45
+ manifest = {
46
+ "runId": work.name,
47
+ "workingDir": str(work),
48
+ "datasetDir": str(dataset),
49
+ "checkpointDir": str(checkpoints),
50
+ "outputPath": str(work / "lora.safetensors"),
51
+ "resumeFrom": None,
52
+ "modelsDir": str(models),
53
+ "arch": "flux2",
54
+ # FLUX.2 offers one base mode: the undistilled klein base. `raw` is that mode's key.
55
+ "baseMode": "raw",
56
+ "triggerWord": "",
57
+ "hyperparams": {
58
+ "arch": "flux2",
59
+ "baseMode": "raw",
60
+ "baseQuant": quant,
61
+ # Explicit, not `auto`: the sweep is measuring what each precision costs, and auto would
62
+ # silently swap a bf16 cell for NF4 the moment it predicted a bad fit.
63
+ "offload": "off",
64
+ "loraScope": "full",
65
+ "captionDropout": 0.0,
66
+ "flipAugment": False,
67
+ "rank": _RANK,
68
+ "alpha": _RANK,
69
+ "learningRate": 1e-4,
70
+ "batchSize": 1,
71
+ "steps": _STEPS,
72
+ "saveEvery": _STEPS,
73
+ "resolution": resolution,
74
+ },
75
+ "gpuIds": [],
76
+ }
77
+ path = work / "manifest.json"
78
+ path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
79
+ return path
80
+
81
+
82
+ def _run_cell(python: str, manifest: Path, log: Path) -> dict[str, object]:
83
+ """Drain the JSON-line protocol, keeping the last VRAM reading and the wall time from the first
84
+ training step onward - loading and latent precache are not what the table reports."""
85
+ env = {**os.environ, "PYTHONPATH": str(_CORE / "src")}
86
+ proc = subprocess.Popen(
87
+ [python, "-m", "inline_core.training", str(manifest)],
88
+ cwd=str(_CORE),
89
+ env=env,
90
+ stdout=subprocess.PIPE,
91
+ stderr=subprocess.STDOUT,
92
+ text=True,
93
+ bufsize=1,
94
+ )
95
+ vram: float | None = None
96
+ error: str | None = None
97
+ first_step_at: float | None = None
98
+ last_step_at: float | None = None
99
+ steps_seen = 0
100
+ started = time.perf_counter()
101
+ lines: list[str] = []
102
+ assert proc.stdout is not None
103
+ for line in proc.stdout:
104
+ lines.append(line)
105
+ line = line.strip()
106
+ if not line.startswith("{"):
107
+ continue
108
+ try:
109
+ message = json.loads(line)
110
+ except json.JSONDecodeError:
111
+ continue
112
+ kind = message.get("type")
113
+ if kind == "progress":
114
+ if message.get("vram") is not None:
115
+ vram = float(message["vram"])
116
+ if message.get("step"):
117
+ steps_seen = int(message["step"])
118
+ now = time.perf_counter()
119
+ if first_step_at is None:
120
+ first_step_at = now
121
+ last_step_at = now
122
+ elif kind == "error":
123
+ error = str(message.get("message") or "")
124
+ proc.wait()
125
+ log.write_text("".join(lines), encoding="utf-8")
126
+
127
+ oom = bool(error) and ("out of gpu memory" in error.lower() or "out of memory" in error.lower())
128
+ # Step 1 pays for the first graph build, so time the interval after it and scale by the gap.
129
+ per_step: float | None = None
130
+ if first_step_at is not None and last_step_at is not None and steps_seen > 1:
131
+ per_step = (last_step_at - first_step_at) / (steps_seen - 1)
132
+ return {
133
+ "peak_vram_gb": vram,
134
+ "seconds_per_step": round(per_step, 2) if per_step else None,
135
+ "seconds_12_steps": round(per_step * _STEPS, 1) if per_step else None,
136
+ "total_seconds": round(time.perf_counter() - started, 1),
137
+ "steps_completed": steps_seen,
138
+ "status": "oom" if oom else ("ok" if proc.returncode == 0 else "failed"),
139
+ "error": error,
140
+ "log": log.name,
141
+ }
142
+
143
+
144
+ def main() -> int:
145
+ parser = argparse.ArgumentParser()
146
+ parser.add_argument("--dataset", type=Path, required=True, help="dir of NNNN.jpg + NNNN.txt")
147
+ parser.add_argument("--out", type=Path, default=_DEFAULT_OUT)
148
+ parser.add_argument("--models", type=Path, default=_CORE / "models")
149
+ parser.add_argument("--python", default=str(_CORE / ".venv" / "bin" / "python"))
150
+ parser.add_argument("--gpu", default="", help="label for the results file, e.g. 'L40S (46GB)'")
151
+ parser.add_argument("--only", default="", help="substring of a cell id, to redo one row")
152
+ args = parser.parse_args()
153
+
154
+ if not args.dataset.is_dir():
155
+ raise SystemExit(f"dataset not found: {args.dataset}")
156
+
157
+ args.out.mkdir(parents=True, exist_ok=True)
158
+ results_path = args.out / "results.json"
159
+ results: dict[str, dict[str, object]] = {}
160
+ if results_path.exists():
161
+ results = json.loads(results_path.read_text()).get("cells", {})
162
+
163
+ label = args.gpu
164
+ if not label:
165
+ try:
166
+ name = subprocess.run(
167
+ ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
168
+ capture_output=True,
169
+ text=True,
170
+ check=True,
171
+ ).stdout.strip()
172
+ label = name.splitlines()[0]
173
+ except Exception: # noqa: BLE001 - the label is cosmetic
174
+ label = "unknown GPU"
175
+
176
+ def write() -> None:
177
+ results_path.write_text(
178
+ json.dumps(
179
+ {"gpu": label, "steps": _STEPS, "rank": _RANK, "cells": results},
180
+ indent=2,
181
+ )
182
+ )
183
+
184
+ for resolution, quant in CELLS:
185
+ cell_id = f"{resolution}-{quant}"
186
+ if args.only and args.only not in cell_id:
187
+ continue
188
+ work = args.out / cell_id
189
+ work.mkdir(parents=True, exist_ok=True)
190
+ manifest = _manifest(work, args.dataset, args.models, resolution, quant)
191
+ print(f"--- {cell_id}: {resolution}px, base {quant} ---", flush=True)
192
+ result = _run_cell(args.python, manifest, args.out / f"{cell_id}.log")
193
+ result.update({"resolution": resolution, "base_quant": quant})
194
+ results[cell_id] = result
195
+ print(json.dumps(result, indent=2), flush=True)
196
+ write()
197
+
198
+ write()
199
+ print(f"\n{results_path}")
200
+ return 0
201
+
202
+
203
+ if __name__ == "__main__":
204
+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1
+ """Inline Core: the generation engine behind Inline.
2
+
3
+ Takes a typed node graph and returns immutable takes. See PLAN.md for the architecture and
4
+ docs/contract.md for the Storyline API.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ try:
10
+ #: Resolved from the installed package, so pyproject.toml stays the only place a release is
11
+ #: bumped. An editable install records this at install time; reinstall after bumping.
12
+ __version__ = version("inline-core")
13
+ except PackageNotFoundError: # a source tree that was never installed
14
+ __version__ = "0.0.0"
@@ -47,6 +47,10 @@ _SMART_RESIDENT_MIN_VRAM_GB = 6.0
47
47
  # ~half the fp16 weight bytes. Deliberately generous so the estimate errs toward a lighter plan.
48
48
  _ACTIVATION_HEADROOM_GB = 2.5
49
49
  _INT8_FACTOR = 0.5
50
+ # NF4 (bitsandbytes) stores 4-bit weights plus per-block scales, so ~0.55 bytes per parameter
51
+ # against fp16's 2. The rung exists for the very large checkpoints (FLUX.2 dev and friends) that
52
+ # int8 still cannot fit; it is CUDA-only and, like int8, never combined with CPU offload.
53
+ _NF4_FACTOR = 0.28
50
54
 
51
55
 
52
56
  def _system_ram_gb() -> float | None:
@@ -202,9 +206,10 @@ class MemoryPolicy(DevicePolicy):
202
206
  return None
203
207
  cap = max(0.0, budget - _ACTIVATION_HEADROOM_GB)
204
208
  big = (fp.diffusion_bytes + fp.text_encoder_bytes) / 1e9
205
- vae = fp.vae_bytes / 1e9
206
- full = big + vae
207
- int8 = big * _INT8_FACTOR + vae
209
+ # The VAE and a ControlNet are never quantized, so they cost the same under every plan.
210
+ fixed = (fp.vae_bytes + fp.controlnet_bytes) / 1e9
211
+ full = big + fixed
212
+ int8 = big * _INT8_FACTOR + fixed
208
213
  forced = _env_profile() is not None # explicit --profile pins the profile; fit picks quant
209
214
 
210
215
  def prof(auto: Profile) -> Profile:
@@ -221,7 +226,14 @@ class MemoryPolicy(DevicePolicy):
221
226
  int8, budget, True,
222
227
  "Weights are int8-quantized to fit this GPU's VRAM.",
223
228
  )
224
- # int8 still won't fit resident -> CPU-offload streaming. Only viable if the (unquantized)
229
+ nf4 = big * _NF4_FACTOR + fixed
230
+ if nf4 <= cap:
231
+ return FitEstimate(
232
+ "nf4", Quantization.NF4, OffloadMode.NONE, prof(Profile.LOWVRAM),
233
+ nf4, budget, True,
234
+ "Weights are 4-bit (NF4) quantized to fit this GPU's VRAM.",
235
+ )
236
+ # Even 4-bit won't fit resident -> CPU-offload streaming. Only viable if the (unquantized)
225
237
  # model fits in system RAM, since sequential offload holds the off-GPU weights there.
226
238
  ram = self._ram_gb
227
239
  if ram is not None and full > ram:
@@ -87,10 +87,14 @@ class ModelFootprint:
87
87
  diffusion_bytes: int = 0
88
88
  text_encoder_bytes: int = 0
89
89
  vae_bytes: int = 0
90
+ #: A ControlNet loaded alongside the denoiser. Never quantized, so it counts full in every plan.
91
+ controlnet_bytes: int = 0
90
92
 
91
93
  @property
92
94
  def total_bytes(self) -> int:
93
- return self.diffusion_bytes + self.text_encoder_bytes + self.vae_bytes
95
+ return (
96
+ self.diffusion_bytes + self.text_encoder_bytes + self.vae_bytes + self.controlnet_bytes
97
+ )
94
98
 
95
99
 
96
100
  @dataclass(frozen=True)
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  from ..takes import Take
14
14
  from .registry import Registry
15
- from .schema import Graph, Node
15
+ from .schema import Graph, Node, PortKind
16
16
 
17
17
 
18
18
  class NodeCache(ABC):
@@ -40,8 +40,15 @@ def _canonical_params(node: Node, registry: Registry) -> dict[str, Any]:
40
40
 
41
41
 
42
42
  def is_cache_eligible(node: Node, registry: Registry) -> bool:
43
- """False when any seed param resolves to a negative (random) value."""
43
+ """False when a control map is wired, or any seed param resolves to a negative (random) value.
44
+
45
+ A node driven by a control map re-runs every time: the user iterates on the pose/depth and
46
+ expects each run to apply the current control, so a cached take would read as "control not
47
+ taking effect" (even a re-render at the same seed must re-apply it)."""
44
48
  descriptor = registry.get(node.type)
49
+ for port in descriptor.inputs:
50
+ if port.kind is PortKind.CONTROL and node.inputs.get(port.id):
51
+ return False
45
52
  defaults = descriptor.defaults()
46
53
  for key in descriptor.seed_keys():
47
54
  value = node.params.get(key, defaults.get(key))
@@ -81,3 +88,29 @@ def node_cache_key(
81
88
  digest = hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()
82
89
  memo[node_id] = digest
83
90
  return digest
91
+
92
+
93
+ def asset_content_hashes(graph: Graph) -> dict[str, str]:
94
+ """The byte hash of each file-backed source node's asset, keyed by node id. Feeds
95
+ ``node_cache_key`` so the cache invalidates when a file's *content* changes even though its path
96
+ did not (a re-rendered control map, an in-place-replaced input image). Only ``ref="path"`` refs
97
+ are hashable; a missing file is skipped - its path still keys the node through its params."""
98
+ import os
99
+
100
+ hashes: dict[str, str] = {}
101
+ for node in graph.nodes:
102
+ asset = node.params.get("asset")
103
+ if not isinstance(asset, dict) or asset.get("ref") != "path":
104
+ continue
105
+ path = asset.get("path")
106
+ if isinstance(path, str) and os.path.isfile(path):
107
+ hashes[node.id] = _file_hash(path)
108
+ return hashes
109
+
110
+
111
+ def _file_hash(path: str) -> str:
112
+ digest = hashlib.sha256()
113
+ with open(path, "rb") as handle:
114
+ for chunk in iter(lambda: handle.read(1 << 20), b""):
115
+ digest.update(chunk)
116
+ return digest.hexdigest()