inline-core 1.2.66__tar.gz → 1.2.68__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 (266) hide show
  1. {inline_core-1.2.66 → inline_core-1.2.68}/CLAUDE.md +21 -0
  2. {inline_core-1.2.66 → inline_core-1.2.68}/PKG-INFO +2 -2
  3. {inline_core-1.2.66 → inline_core-1.2.68}/pyproject.toml +1 -1
  4. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/config.py +23 -2
  5. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/catalog.py +102 -15
  6. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/keymap.py +23 -8
  7. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/lora.py +70 -28
  8. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/adaln.py +102 -0
  9. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/keys.py +29 -5
  10. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/load.py +107 -14
  11. inline_core-1.2.68/src/inline_core/models/minimaxh3/lora_keys.py +235 -0
  12. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/pipeline.py +33 -8
  13. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/requirements.py +83 -13
  14. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/runner.py +20 -1
  15. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/pipeline_runtime.py +127 -4
  16. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/progress.py +11 -1
  17. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/run.py +3 -0
  18. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/app.py +74 -7
  19. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/manager.py +85 -8
  20. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/serialize.py +14 -0
  21. inline_core-1.2.68/src/inline_core/studio/activity.py +396 -0
  22. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/fal.py +93 -25
  23. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/generation.py +116 -44
  24. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/handlers.py +52 -3
  25. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/schema.py +18 -1
  26. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/store.py +58 -3
  27. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/training.py +149 -9
  28. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/training_store.py +2 -0
  29. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/arch.py +12 -0
  30. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/cache.py +46 -0
  31. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/dataset.py +8 -0
  32. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/h3.py +20 -1
  33. inline_core-1.2.68/src/inline_core/training/precache_store.py +136 -0
  34. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/protocol.py +5 -0
  35. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/trainer.py +54 -6
  36. inline_core-1.2.68/tests/test_catalog.py +146 -0
  37. inline_core-1.2.68/tests/test_encoder_parking.py +77 -0
  38. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_lora.py +43 -0
  39. inline_core-1.2.68/tests/test_manager_queue.py +152 -0
  40. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_minimaxh3_load.py +148 -0
  41. inline_core-1.2.68/tests/test_minimaxh3_lora_keys.py +249 -0
  42. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_minimaxh3_nodes.py +243 -8
  43. inline_core-1.2.68/tests/test_precache_store.py +113 -0
  44. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_staged_residency.py +92 -0
  45. inline_core-1.2.68/tests/test_studio_activity.py +270 -0
  46. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_fal.py +16 -0
  47. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_generation.py +40 -0
  48. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_schema.py +25 -0
  49. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_training.py +79 -0
  50. inline_core-1.2.68/tests/test_training_snapshots.py +68 -0
  51. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_webui_install.py +3 -0
  52. {inline_core-1.2.66 → inline_core-1.2.68}/uv.lock +5 -3
  53. inline_core-1.2.66/tests/test_catalog.py +0 -64
  54. {inline_core-1.2.66 → inline_core-1.2.68}/.gitignore +0 -0
  55. {inline_core-1.2.66 → inline_core-1.2.68}/.python-version +0 -0
  56. {inline_core-1.2.66 → inline_core-1.2.68}/README.md +0 -0
  57. {inline_core-1.2.66 → inline_core-1.2.68}/main.py +0 -0
  58. {inline_core-1.2.66 → inline_core-1.2.68}/scripts/flux2_train_matrix.py +0 -0
  59. {inline_core-1.2.66 → inline_core-1.2.68}/scripts/minimax_h3_lora_check.py +0 -0
  60. {inline_core-1.2.66 → inline_core-1.2.68}/scripts/minimax_h3_train_matrix.py +0 -0
  61. {inline_core-1.2.66 → inline_core-1.2.68}/scripts/reference.py +0 -0
  62. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/__init__.py +0 -0
  63. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/components/__init__.py +0 -0
  64. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/components/conditioning.py +0 -0
  65. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/components/interfaces.py +0 -0
  66. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/__init__.py +0 -0
  67. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/auto.py +0 -0
  68. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/detect.py +0 -0
  69. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/memory.py +0 -0
  70. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/policy.py +0 -0
  71. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/probe.py +0 -0
  72. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/device/types.py +0 -0
  73. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/errors.py +0 -0
  74. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/__init__.py +0 -0
  75. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/api.py +0 -0
  76. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/constraints.py +0 -0
  77. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/fetch.py +0 -0
  78. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/handlers.py +0 -0
  79. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/importer.py +0 -0
  80. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/install.py +0 -0
  81. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/loader.py +0 -0
  82. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/manifest.py +0 -0
  83. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/models.py +0 -0
  84. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/paths.py +0 -0
  85. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/resolve.py +0 -0
  86. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/scanner.py +0 -0
  87. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/state.py +0 -0
  88. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/extensions/tools.py +0 -0
  89. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/ffmpeg.py +0 -0
  90. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/__init__.py +0 -0
  91. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/cache.py +0 -0
  92. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/descriptor.py +0 -0
  93. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/executor.py +0 -0
  94. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/loader_runners.py +0 -0
  95. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/primitives.py +0 -0
  96. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/registry.py +0 -0
  97. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/runners.py +0 -0
  98. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/schema.py +0 -0
  99. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/topo.py +0 -0
  100. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/graph/validate.py +0 -0
  101. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/media.py +0 -0
  102. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/__init__.py +0 -0
  103. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/checkpoint.py +0 -0
  104. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/controlspace.py +0 -0
  105. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/__init__.py +0 -0
  106. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/controlnet.py +0 -0
  107. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/embeds.py +0 -0
  108. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/provider.py +0 -0
  109. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/requirements.py +0 -0
  110. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/runner.py +0 -0
  111. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/flux2/variants.py +0 -0
  112. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/__init__.py +0 -0
  113. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/convert.py +0 -0
  114. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/depth_control.py +0 -0
  115. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/img2img.py +0 -0
  116. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/provider.py +0 -0
  117. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/requirements.py +0 -0
  118. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/krea2/runner.py +0 -0
  119. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/loaders.py +0 -0
  120. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/__init__.py +0 -0
  121. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/provider.py +0 -0
  122. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vae_keys.py +0 -0
  123. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/__init__.py +0 -0
  124. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +0 -0
  125. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +0 -0
  126. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/before_denoise.py +0 -0
  127. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/before_encoder.py +0 -0
  128. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/decoders.py +0 -0
  129. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/denoise.py +0 -0
  130. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/encoders.py +0 -0
  131. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +0 -0
  132. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +0 -0
  133. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/packing.py +0 -0
  134. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +0 -0
  135. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +0 -0
  136. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +0 -0
  137. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/offload.py +0 -0
  138. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/prepared.py +0 -0
  139. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/preprocess/__init__.py +0 -0
  140. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/preprocess/requirements.py +0 -0
  141. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/preprocess/runner.py +0 -0
  142. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/references.py +0 -0
  143. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/requirements.py +0 -0
  144. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/sampling.py +0 -0
  145. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/video_params.py +0 -0
  146. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/zimage/__init__.py +0 -0
  147. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/zimage/primitives.py +0 -0
  148. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/zimage/provider.py +0 -0
  149. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/zimage/requirements.py +0 -0
  150. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/models/zimage/runner.py +0 -0
  151. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/__init__.py +0 -0
  152. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/config.py +0 -0
  153. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/group.py +0 -0
  154. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/launch.py +0 -0
  155. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/protocol.py +0 -0
  156. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/registry.py +0 -0
  157. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/parallel/worker.py +0 -0
  158. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/__init__.py +0 -0
  159. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/context.py +0 -0
  160. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/file_store.py +0 -0
  161. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/store.py +0 -0
  162. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/runtime/video_encode.py +0 -0
  163. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/sampling/__init__.py +0 -0
  164. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/sampling/batch.py +0 -0
  165. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/__init__.py +0 -0
  166. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/__main__.py +0 -0
  167. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/assets.py +0 -0
  168. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/bootstrap.py +0 -0
  169. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/frontend.py +0 -0
  170. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/rpc.py +0 -0
  171. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/server/run_store.py +0 -0
  172. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/__init__.py +0 -0
  173. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/assets.py +0 -0
  174. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/config.py +0 -0
  175. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/frames.py +0 -0
  176. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/graph_build.py +0 -0
  177. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/image_meta.py +0 -0
  178. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/models.py +0 -0
  179. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/moodboard.py +0 -0
  180. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/peaks.py +0 -0
  181. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/recipe.py +0 -0
  182. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/system_stats.py +0 -0
  183. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/timeline/__init__.py +0 -0
  184. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/timeline/compose.py +0 -0
  185. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  186. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/timeline/render.py +0 -0
  187. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/studio/timeline/resolve.py +0 -0
  188. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/takes.py +0 -0
  189. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/__init__.py +0 -0
  190. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/__main__.py +0 -0
  191. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/caption.py +0 -0
  192. {inline_core-1.2.66 → inline_core-1.2.68}/src/inline_core/training/models.py +0 -0
  193. {inline_core-1.2.66 → inline_core-1.2.68}/tests/conftest.py +0 -0
  194. {inline_core-1.2.66 → inline_core-1.2.68}/tests/helpers.py +0 -0
  195. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_cache.py +0 -0
  196. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_checkpoint.py +0 -0
  197. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_config.py +0 -0
  198. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_device_detect.py +0 -0
  199. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_executor.py +0 -0
  200. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_api.py +0 -0
  201. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_install.py +0 -0
  202. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_manifest.py +0 -0
  203. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_resolve.py +0 -0
  204. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_scanner.py +0 -0
  205. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_spine.py +0 -0
  206. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_extension_state.py +0 -0
  207. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_file_store.py +0 -0
  208. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_controlnet.py +0 -0
  209. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_folder.py +0 -0
  210. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_resolve.py +0 -0
  211. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_runner.py +0 -0
  212. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_training.py +0 -0
  213. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_flux2_variants.py +0 -0
  214. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_frontend_serving.py +0 -0
  215. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_hidden_nodes.py +0 -0
  216. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_keymap.py +0 -0
  217. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_krea2_convert.py +0 -0
  218. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_krea2_depth_control.py +0 -0
  219. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_krea2_requirements.py +0 -0
  220. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_krea2_runner.py +0 -0
  221. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_loader_runners.py +0 -0
  222. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_loaders.py +0 -0
  223. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_lora_download.py +0 -0
  224. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_memory_policy.py +0 -0
  225. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_minimaxh3_adaln.py +0 -0
  226. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_minimaxh3_keys.py +0 -0
  227. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_minimaxh3_training.py +0 -0
  228. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_model_requirements.py +0 -0
  229. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_offload_prepared.py +0 -0
  230. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_output_kind_contract.py +0 -0
  231. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_parallel_group.py +0 -0
  232. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_pipeline_cache.py +0 -0
  233. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_primitives.py +0 -0
  234. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_recipe.py +0 -0
  235. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_references.py +0 -0
  236. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_rpc_bridge.py +0 -0
  237. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_run_store.py +0 -0
  238. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_sampling.py +0 -0
  239. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_schema.py +0 -0
  240. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_server.py +0 -0
  241. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_assets.py +0 -0
  242. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_frames.py +0 -0
  243. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_graph_build.py +0 -0
  244. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_models.py +0 -0
  245. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_moodboard.py +0 -0
  246. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_multi_reference.py +0 -0
  247. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_node_size.py +0 -0
  248. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_peaks.py +0 -0
  249. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_rpc.py +0 -0
  250. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_store.py +0 -0
  251. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_studio_timeline.py +0 -0
  252. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_take_bytes.py +0 -0
  253. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_topo.py +0 -0
  254. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_training_arch.py +0 -0
  255. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_training_dataset.py +0 -0
  256. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_training_models.py +0 -0
  257. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_training_resolve.py +0 -0
  258. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_validate.py +0 -0
  259. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_video_encode.py +0 -0
  260. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_video_params.py +0 -0
  261. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_xfuser_sampler.py +0 -0
  262. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_zimage_primitives.py +0 -0
  263. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_zimage_resolve.py +0 -0
  264. {inline_core-1.2.66 → inline_core-1.2.68}/tests/test_zimage_runner.py +0 -0
  265. {inline_core-1.2.66 → inline_core-1.2.68}/webui.bat +0 -0
  266. {inline_core-1.2.66 → inline_core-1.2.68}/webui.sh +0 -0
@@ -163,6 +163,17 @@ between nodes and are never takes.
163
163
  → sequential offload → wont-fit. NF4 is what makes a 32B model viable on a 24 GB card. Note int8
164
164
  forces bf16 (torchao's weight-only int8 silently no-ops under fp16) while NF4 does not, so a Turing
165
165
  card keeps its fp16 tensor cores under NF4.
166
+ - **A pre-reduced checkpoint must not be re-reduced, structurally or numerically.** MiniMax H3's
167
+ `pruned` builds ship the AdaLN branch already factorised to rank 8 and drop the timestep path
168
+ entirely, so re-running our factorisation multiplies a `[96768, 8]` projection by a full-width
169
+ basis. `minimaxh3/pipeline.py` turns `factorise_adaln` off for those, the same way the rule below
170
+ turns quantization off for a prequantized file. Both are the same rule: the source is already in
171
+ the target form.
172
+ - **Size a checkpoint by what it becomes, not by what it weighs.** A pruned file has already lost
173
+ its AdaLN branch and an fp8 file stores half the bytes it will occupy once dequantised, so scaling
174
+ the on-disk number under-sizes both, by up to 3x. `minimaxh3.requirements.resident_bytes` counts
175
+ from the header. Under-sizing is the dangerous direction: the fit ladder then promises a machine
176
+ that dies to a host-RAM OOM kill instead of raising.
166
177
  - **Prequantized checkpoints must not be re-quantized.** A checkpoint that ships already quantized
167
178
  (`flux2/variants.is_prequantized`) has an on-disk size that already _is_ its resident size, so the
168
179
  ladder's assumption that quantization halves it does not hold, and handing diffusers a second,
@@ -230,6 +241,16 @@ real codec that moves tensors lives with the model runner.
230
241
  Don't scatter it.
231
242
  - **Bring-your-own models.** Nothing is downloaded by the engine. The catalog scans; the user places
232
243
  files. A model picker is a `SELECT` param with `options_from="<category>"`.
244
+ - **Adapter strength is not a quality metric, and a threshold on it is a false-positive machine.**
245
+ Measured against real bases, published LoRAs that work well span `|B@A| / |W|` from 0.017%
246
+ (a style LoRA) to 1.2% (a restoration LoRA), so "this adapter looks weak" is not a finding. What
247
+ predicts a LoRA doing nothing is whether its per-weight change clears one quantization step. Warn
248
+ on that, and only when the base is actually quantized.
249
+ - **Patching a diffusers object may patch a copy, and it will not tell you.**
250
+ `ModularPipeline.blocks` is a property returning `deepcopy(self._blocks)`, so hooking the block
251
+ graph through it installs cleanly onto a throwaway and reports success. That is how H3's denoise
252
+ ran with no per-step progress while the hook said it was attached. Reach for the backing
253
+ attribute, and prove a hook fires against the real object rather than trusting a return value.
233
254
  - **Verify image models by rendering.** The FLUX.2 work shipped five bugs past a green test suite,
234
255
  and every one produced a _wrong image rather than an error_: a mis-keyed checkpoint, a
235
256
  vision-language encoder loaded in place of a text one, an unnormalized latent, and a control context
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: inline-core
3
- Version: 1.2.66
3
+ Version: 1.2.68
4
4
  Summary: The generation engine behind Inline Studio.
5
5
  License-Expression: GPL-3.0-or-later
6
6
  Requires-Python: >=3.11
@@ -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.66"
4
+ version = "1.2.68"
5
5
  description = "The generation engine behind Inline Studio."
6
6
  readme = "README.md"
7
7
  license = "GPL-3.0-or-later"
@@ -7,12 +7,33 @@ from pathlib import Path
7
7
 
8
8
 
9
9
  def models_dir() -> Path:
10
- """The models root scanned on start (category subfolders inside). `INLINE_MODELS_DIR`, else
11
- `./models`. Users drop their own weight files here; nothing is downloaded."""
10
+ """The **writable** models root: where downloads and trained LoRAs land. `INLINE_MODELS_DIR`,
11
+ else `./models`. Users drop their own weight files here; nothing is downloaded."""
12
12
  env = os.environ.get("INLINE_MODELS_DIR")
13
13
  return Path(env).expanduser() if env else Path("models")
14
14
 
15
15
 
16
+ def models_dirs() -> list[Path]:
17
+ """Every root scanned for installed weights, writable one first.
18
+
19
+ Pointing `INLINE_MODELS_DIR` (or `--models-dir`) somewhere else used to hide `./models`
20
+ entirely, so a user with weights in both places could only ever see one set. The extra roots
21
+ are read-only: only `models_dir()` is ever written to.
22
+ """
23
+ roots = [models_dir()]
24
+ default = Path("models")
25
+ for extra in (default, *_extra_models_dirs()):
26
+ if extra not in roots and extra.is_dir():
27
+ roots.append(extra)
28
+ return roots
29
+
30
+
31
+ def _extra_models_dirs() -> list[Path]:
32
+ """Additional read-only roots from `INLINE_EXTRA_MODELS_DIRS` (os.pathsep-separated)."""
33
+ raw = os.environ.get("INLINE_EXTRA_MODELS_DIRS", "")
34
+ return [Path(p).expanduser() for p in raw.split(os.pathsep) if p.strip()]
35
+
36
+
16
37
  def data_dir() -> Path:
17
38
  """Engine-owned working data (the run DB, takes). `INLINE_DATA_DIR`, else `./.inline`."""
18
39
  env = os.environ.get("INLINE_DATA_DIR")
@@ -13,9 +13,11 @@ from __future__ import annotations
13
13
 
14
14
  import hashlib
15
15
  import json
16
+ from collections.abc import Sequence
16
17
  from pathlib import Path
18
+ from typing import Any
17
19
 
18
- from ..config import models_dir
20
+ from ..config import models_dirs
19
21
 
20
22
  # Category subfolders scanned under the models root. These are the keys a param's `options_from`
21
23
  # may reference (see graph/primitives.py); ensure_dirs() creates them so drop-in is obvious.
@@ -29,6 +31,9 @@ CATEGORIES: tuple[str, ...] = (
29
31
  "controlnet",
30
32
  "upscale_models",
31
33
  "embeddings",
34
+ # Written by the preprocess runner rather than the user, but it holds real weights and the
35
+ # panel is meant to show everything on disk.
36
+ "annotators",
32
37
  )
33
38
 
34
39
  # Extensions we treat as model weights. A folder counts as a model if it contains one of these.
@@ -45,34 +50,111 @@ def _folder_has_weight(path: Path) -> bool:
45
50
  return any(_is_weight(child) for child in path.rglob("*"))
46
51
 
47
52
 
53
+ def _file_node(path: Path) -> dict[str, Any]:
54
+ stat = path.stat()
55
+ return {
56
+ "name": path.name,
57
+ "path": str(path),
58
+ "kind": "file",
59
+ "sizeBytes": stat.st_size,
60
+ "mtime": int(stat.st_mtime * 1000),
61
+ }
62
+
63
+
64
+ def _dir_node(path: Path, name: str) -> dict[str, Any] | None:
65
+ """A directory as a tree node, or None when it holds nothing worth showing."""
66
+ if not path.is_dir():
67
+ return None
68
+ children: list[dict[str, Any]] = []
69
+ try:
70
+ entries = sorted(path.iterdir(), key=lambda p: p.name.lower())
71
+ except OSError:
72
+ return None
73
+ for entry in entries:
74
+ # Caches like loras/.cache and text_encoders/.snap are noise in a panel about weights.
75
+ if entry.name.startswith("."):
76
+ continue
77
+ if _is_weight(entry):
78
+ children.append(_file_node(entry))
79
+ elif entry.is_dir():
80
+ child = _dir_node(entry, entry.name)
81
+ if child is not None:
82
+ children.append(child)
83
+ if not children:
84
+ return None
85
+ return {
86
+ "name": name,
87
+ "path": str(path),
88
+ "kind": "dir",
89
+ "children": children,
90
+ "fileCount": sum(1 for c in children if c["kind"] == "file"),
91
+ }
92
+
93
+
48
94
  class ModelCatalog:
49
- """Scans the models root and answers "what's installed" per category.
95
+ """Scans the models roots and answers "what's installed" per category.
50
96
 
51
97
  Cheap to construct; nothing touches disk until ``ensure_dirs`` or ``rescan``/``scan``. Results
52
- are cached so ``list`` and ``fingerprint`` are hits between scans.
98
+ are cached so ``list`` and ``fingerprint`` are hits between scans. Several roots may be scanned
99
+ (see ``config.models_dirs``); only the first is ever written to.
53
100
  """
54
101
 
55
- def __init__(self, root: Path) -> None:
56
- self._root = Path(root)
102
+ def __init__(self, root: Path | Sequence[Path]) -> None:
103
+ roots = [Path(root)] if isinstance(root, str | Path) else [Path(r) for r in root]
104
+ self._roots = roots or [Path("models")]
57
105
  self._entries: dict[str, list[str]] = {category: [] for category in CATEGORIES}
58
106
 
59
107
  @property
60
108
  def root(self) -> Path:
61
- return self._root
109
+ """The writable root. Downloads and trained LoRAs land here."""
110
+ return self._roots[0]
111
+
112
+ @property
113
+ def roots(self) -> list[Path]:
114
+ return list(self._roots)
62
115
 
63
116
  def ensure_dirs(self) -> None:
64
- """Create the root and every category subfolder, so users have somewhere to drop weights."""
117
+ """Create the writable root and its category subfolders, so drop-in has somewhere to go."""
65
118
  for category in CATEGORIES:
66
- (self._root / category).mkdir(parents=True, exist_ok=True)
119
+ (self.root / category).mkdir(parents=True, exist_ok=True)
67
120
 
68
121
  def rescan(self) -> dict[str, list[str]]:
69
- """Re-read every category from disk, cache the result, and return it."""
122
+ """Re-read every category from every root, cache the result, and return it."""
70
123
  entries: dict[str, list[str]] = {}
71
124
  for category in CATEGORIES:
72
- entries[category] = self._scan_category(self._root / category)
125
+ names: list[str] = []
126
+ for root in self._roots:
127
+ for name in self._scan_category(root / category):
128
+ if name not in names:
129
+ names.append(name)
130
+ entries[category] = sorted(names)
73
131
  self._entries = entries
74
132
  return entries
75
133
 
134
+ def tree(self) -> list[dict[str, Any]]:
135
+ """Every root as a nested, read-only listing for the Models panel.
136
+
137
+ Separate from ``rescan`` because the select only needs flat names, while the panel wants
138
+ nesting, sizes and mtimes; keeping them apart stops the hot path paying for the detail.
139
+ """
140
+ out: list[dict[str, Any]] = []
141
+ for index, root in enumerate(self._roots):
142
+ categories = [
143
+ node
144
+ for category in CATEGORIES
145
+ if (node := _dir_node(root / category, category)) is not None
146
+ ]
147
+ out.append(
148
+ {
149
+ "path": str(root),
150
+ "label": root.name or str(root),
151
+ "writable": index == 0,
152
+ "exists": root.is_dir(),
153
+ "categories": categories,
154
+ }
155
+ )
156
+ return out
157
+
76
158
  # app.py calls scan() in the lifespan; rescan() is the same work exposed for tests/callers that
77
159
  # want the mapping back. Keep both so neither call site has to know about the other.
78
160
  def scan(self) -> dict[str, list[str]]:
@@ -92,6 +174,8 @@ class ModelCatalog:
92
174
  return []
93
175
  names: list[str] = []
94
176
  for entry in directory.iterdir():
177
+ if entry.name.startswith("."):
178
+ continue
95
179
  if _is_weight(entry):
96
180
  names.append(entry.name)
97
181
  elif entry.is_dir() and _folder_has_weight(entry):
@@ -112,8 +196,11 @@ def resolve_picked(category: str, chosen: object) -> Path | None:
112
196
  name = str(chosen or "").strip()
113
197
  if not name:
114
198
  return None
115
- root = models_dir() / category
116
- for candidate in (root / name, root / Path(name).name, Path(name)):
117
- if candidate.exists():
118
- return candidate
119
- return None
199
+ # Every root, writable one first, so a pick resolves wherever the user actually keeps it.
200
+ for models_root in models_dirs():
201
+ root = models_root / category
202
+ for candidate in (root / name, root / Path(name).name):
203
+ if candidate.exists():
204
+ return candidate
205
+ bare = Path(name)
206
+ return bare if bare.exists() else None
@@ -262,7 +262,7 @@ def transform(
262
262
  raise ComponentError(f"{key} has {tensor.shape[0]} rows, not divisible into {parts} parts.")
263
263
  if verify_layout:
264
264
  assert_layout(tensor, action, key=key)
265
- source = _deinterleave(tensor, parts, action.head_dim) if (
265
+ source = deinterleave_rows(tensor, parts, action.head_dim) if (
266
266
  action.layout is RowLayout.INTERLEAVED
267
267
  ) else tensor
268
268
  block = source.shape[0] // parts
@@ -270,16 +270,31 @@ def transform(
270
270
  yield target, source[index * block : (index + 1) * block]
271
271
 
272
272
 
273
- def _deinterleave(tensor: Any, parts: int, head_dim: int) -> Any:
274
- """``[p0_h0; p1_h0; p2_h0][p0_h1; …]`` to ``[p0_all; p1_all; p2_all]``.
275
-
276
- ``transpose`` is not the same call in torch and numpy - torch swaps two axes, numpy wants a full
277
- permutation - so the swap is spelled per backend rather than duck-typed.
278
- """
273
+ def deinterleave_rows(tensor: Any, parts: int, head_dim: int) -> Any:
274
+ """``[p0_h0; p1_h0; p2_h0][p0_h1; …]`` to ``[p0_all; p1_all; p2_all]``."""
279
275
  if head_dim < 1:
280
276
  raise ComponentError("De-interleaving needs the head dimension the parts are grouped by.")
281
277
  heads = tensor.shape[0] // (parts * head_dim)
282
- reshaped = tensor.reshape(heads, parts, head_dim, *tensor.shape[1:])
278
+ return _swap01(tensor, (heads, parts, head_dim))
279
+
280
+
281
+ def interleave_rows(tensor: Any, parts: int, head_dim: int) -> Any:
282
+ """``[p0_all; p1_all; p2_all]`` back to per-head groups: the inverse of ``deinterleave_rows``.
283
+
284
+ Needed to *write* a checkpoint or adapter in a publisher's interleaved layout, where the load
285
+ path only ever reads one."""
286
+ if head_dim < 1:
287
+ raise ComponentError("Interleaving needs the head dimension the parts are grouped by.")
288
+ heads = tensor.shape[0] // (parts * head_dim)
289
+ return _swap01(tensor, (parts, heads, head_dim))
290
+
291
+
292
+ def _swap01(tensor: Any, shape: tuple[int, int, int]) -> Any:
293
+ """Reshape to ``shape`` plus the trailing dims, exchange the first two, flatten back.
294
+
295
+ ``transpose`` is not the same call in torch and numpy - torch swaps two axes, numpy wants a full
296
+ permutation - so the swap is spelled per backend rather than duck-typed."""
297
+ reshaped = tensor.reshape(*shape, *tensor.shape[1:])
283
298
  if _is_torch(tensor):
284
299
  moved = reshaped.transpose(0, 1).contiguous()
285
300
  else:
@@ -29,6 +29,10 @@ if TYPE_CHECKING:
29
29
  #: while ostris' training adapter uses the reference names.
30
30
  Alias = Callable[[str], str | None]
31
31
 
32
+ #: Rewrites a whole adapter before it is matched, for an arch whose checkpoint keys need more than a
33
+ #: rename - MiniMax H3 ships attention fused, so three of our modules are one of theirs.
34
+ Translate = Callable[[dict[str, Any]], dict[str, Any]]
35
+
32
36
  _DOWN = ("lora_down.weight", "lora_A.weight", "lora_A.default.weight")
33
37
  _UP = ("lora_up.weight", "lora_B.weight", "lora_B.default.weight")
34
38
  # Prefixes checkpoints put in front of the module path; stripped when matching against the model.
@@ -39,12 +43,22 @@ _PREFIXES = ("diffusion_model.", "transformer.", "lora_unet_", "lora_te_", "base
39
43
  LoraPlan = dict[str, list[tuple[Any, Any, float]]]
40
44
 
41
45
 
42
- def fuse_loras(model: Any, loras: tuple[LoraRef, ...], alias: Alias | None = None) -> None:
46
+ def fuse_loras(
47
+ model: Any,
48
+ loras: tuple[LoraRef, ...],
49
+ alias: Alias | None = None,
50
+ translate: Translate | None = None,
51
+ ) -> None:
43
52
  """Merge each LoRA into ``model``'s weights in order. No-op for an empty stack."""
44
- apply_plan(model, plan_loras(model, loras, alias))
53
+ apply_plan(model, plan_loras(model, loras, alias, translate))
45
54
 
46
55
 
47
- def plan_loras(model: Any, loras: tuple[LoraRef, ...], alias: Alias | None = None) -> LoraPlan:
56
+ def plan_loras(
57
+ model: Any,
58
+ loras: tuple[LoraRef, ...],
59
+ alias: Alias | None = None,
60
+ translate: Translate | None = None,
61
+ ) -> LoraPlan:
48
62
  """Resolve every LoRA against ``model``'s module names, without touching any weights.
49
63
 
50
64
  Split from the fusing so a streaming loader can validate the whole stack **before** reading a
@@ -53,7 +67,7 @@ def plan_loras(model: Any, loras: tuple[LoraRef, ...], alias: Alias | None = Non
53
67
  plan: LoraPlan = {}
54
68
  names = _linear_module_names(model)
55
69
  for lora in loras:
56
- _plan_one(plan, names, lora.file, lora.strength, alias)
70
+ _plan_one(plan, names, lora.file, lora.strength, alias, translate)
57
71
  return plan
58
72
 
59
73
 
@@ -74,7 +88,12 @@ def apply_plan(module: Any, plan: LoraPlan, prefix: str = "") -> None:
74
88
 
75
89
 
76
90
  def _plan_one(
77
- plan: LoraPlan, names: dict[str, None], path: str, strength: float, alias: Alias | None
91
+ plan: LoraPlan,
92
+ names: dict[str, None],
93
+ path: str,
94
+ strength: float,
95
+ alias: Alias | None,
96
+ translate: Translate | None = None,
78
97
  ) -> None:
79
98
  from safetensors.torch import load_file
80
99
 
@@ -83,6 +102,8 @@ def _plan_one(
83
102
  except Exception as exc: # noqa: BLE001
84
103
  raise ComponentError(f"Could not read LoRA {path!r}: {exc}") from exc
85
104
 
105
+ if translate is not None:
106
+ state = translate(state)
86
107
  pairs, alphas = _group(state)
87
108
  if not pairs:
88
109
  raise ComponentError(f"LoRA {path!r} contains no recognisable lora_down/lora_up pairs.")
@@ -104,27 +125,39 @@ def _plan_one(
104
125
  )
105
126
 
106
127
 
128
+ #: Most fp32 delta held at once. The product is computed a slice of output rows at a time because
129
+ #: the whole of it is enormous: one MiniMax H3 block's six Linears come to 1.5GB and the model to
130
+ #: 80GB, which is host RAM during a staged load and took a 60GB box down three times.
131
+ _DELTA_CHUNK_BYTES = 64 * 1024 * 1024
132
+
133
+
107
134
  def _add_delta(weight: Any, up: Any, down: Any, scale: float) -> None:
108
135
  """Fuse ``scale * (up @ down)`` into ``weight`` in place.
109
136
 
110
- Computed on the weight's own device: a big LoRA (Krea 2's are ~260 modules on a 12.9B model)
111
- materializes tens of GB of fp32 deltas, and doing that on the CPU costs ~20s of maths plus the
112
- transfer where the GPU takes ~2s. Falls back to the CPU if the device runs out of memory, so a
113
- tight card still fuses, just slowly."""
137
+ Computed on the weight's own device: doing it on the CPU costs ~20s of maths plus the transfer
138
+ where the GPU takes ~2s. Falls back to the CPU if the device runs out of memory, so a tight card
139
+ still fuses, just slowly."""
114
140
  import torch
115
141
 
116
142
  try:
117
- weight.add_(_delta(up, down, weight, weight.device) * scale)
143
+ _accumulate(weight, up, down, scale, weight.device)
118
144
  except torch.cuda.OutOfMemoryError:
119
- weight.add_((_delta(up, down, weight, "cpu") * scale).to(weight.device, weight.dtype))
145
+ _accumulate(weight, up, down, scale, "cpu")
120
146
 
121
147
 
122
- def _delta(up: Any, down: Any, weight: Any, device: Any) -> Any:
123
- """``up @ down`` on ``device``, shaped and typed for the target weight. Conv LoRAs flatten the
124
- spatial dims."""
148
+ def _accumulate(weight: Any, up: Any, down: Any, scale: float, device: Any) -> None:
149
+ """Add the product into ``weight`` in row slices. Conv LoRAs flatten the spatial dims."""
125
150
  dtype = _fuse_dtype(up)
126
- delta = up.to(device, dtype=dtype).flatten(1) @ down.to(device, dtype=dtype).flatten(1)
127
- return delta.reshape(weight.shape).to(weight.dtype)
151
+ rows = up.to(device, dtype=dtype).flatten(1)
152
+ cols = down.to(device, dtype=dtype).flatten(1)
153
+ if not weight.is_contiguous(): # a non-contiguous target cannot be written through a view
154
+ weight.add_((rows @ cols).reshape(weight.shape).to(weight.dtype) * scale)
155
+ return
156
+ target = weight.view(rows.shape[0], -1)
157
+ step = max(1, _DELTA_CHUNK_BYTES // max(1, cols.shape[1] * 4))
158
+ for start in range(0, rows.shape[0], step):
159
+ stop = start + step
160
+ target[start:stop].add_((rows[start:stop] @ cols).to(weight.dtype) * scale)
128
161
 
129
162
 
130
163
  def _fuse_dtype(tensor: Any) -> Any:
@@ -141,20 +174,29 @@ def _alpha_scale(alpha: Any, rank: int) -> float:
141
174
  return float(alpha.item() if hasattr(alpha, "item") else alpha) / float(rank)
142
175
 
143
176
 
177
+ def split_key(key: str) -> tuple[str, str] | None:
178
+ """``…to_q.lora_A.weight`` to ``("…to_q", "down")``. None for anything that is not a LoRA key.
179
+
180
+ Public so a per-arch key translator groups by exactly the suffixes the fuser recognises: a
181
+ convention known to one and not the other would drop tensors silently."""
182
+ for suffix in _DOWN:
183
+ if key.endswith("." + suffix):
184
+ return key[: -len(suffix) - 1], "down"
185
+ for suffix in _UP:
186
+ if key.endswith("." + suffix):
187
+ return key[: -len(suffix) - 1], "up"
188
+ if key.endswith(".alpha"):
189
+ return key[: -len(".alpha")], "alpha"
190
+ return None
191
+
192
+
144
193
  def _group(state: dict[str, Any]) -> tuple[dict[str, tuple[Any, Any]], dict[str, Any]]:
145
- downs: dict[str, Any] = {}
146
- ups: dict[str, Any] = {}
147
- alphas: dict[str, Any] = {}
194
+ parts: dict[str, dict[str, Any]] = {}
148
195
  for key, value in state.items():
149
- for suffix in _DOWN:
150
- if key.endswith("." + suffix):
151
- downs[key[: -len(suffix) - 1]] = value
152
- for suffix in _UP:
153
- if key.endswith("." + suffix):
154
- ups[key[: -len(suffix) - 1]] = value
155
- if key.endswith(".alpha"):
156
- alphas[key[: -len(".alpha")]] = value
157
- return {k: (downs[k], ups[k]) for k in downs if k in ups}, alphas
196
+ if (split := split_key(key)) is not None:
197
+ parts.setdefault(split[0], {})[split[1]] = value
198
+ pairs = {k: (v["down"], v["up"]) for k, v in parts.items() if "down" in v and "up" in v}
199
+ return pairs, {k: v["alpha"] for k, v in parts.items() if "alpha" in v}
158
200
 
159
201
 
160
202
  def _linear_module_names(model: Any) -> dict[str, None]:
@@ -41,6 +41,11 @@ from typing import Any
41
41
  import torch
42
42
  from torch import nn
43
43
 
44
+ from .vendor.transformer_minimax_h3 import (
45
+ MiniMaxH3AdaLayerNormModulation,
46
+ MiniMaxH3AdaLayerNormOut,
47
+ )
48
+
44
49
  logger = logging.getLogger("inline_core.minimaxh3")
45
50
 
46
51
  #: Rank kept. Five directions carry the energy; eight matches the published build and leaves slack.
@@ -144,3 +149,100 @@ def factorise(model: Any, *, rank: int = RANK) -> int:
144
149
  len(model.transformer_blocks), saved / 1e9,
145
150
  )
146
151
  return saved
152
+
153
+
154
+ # --- the published pruned builds ------------------------------------------------------------
155
+ #
156
+ # MiniMax ship `pruned` checkpoints that do this same rank-8 reduction ahead of time, and go one
157
+ # step further: the timestep path itself is gone. There is no `time_embedder` in the file at all,
158
+ # only `adaln_t_table [1025, 8]`, holding `silu(temb)` already projected into their basis at 1025
159
+ # points across t in [0, 1]. So the branch cannot be rebuilt as a basis applied to a `silu(temb)` we
160
+ # compute; the table has to be read directly.
161
+ #
162
+ # Off-grid timesteps are interpolated. Measured against the full bf16 weights at 24 random t, the
163
+ # table reaches 1.636e-4 relative with linear interpolation and 1.874e-4 taking the nearest row,
164
+ # where one bf16 ulp of the reference is 1.307e-3. The grid is dense enough that interpolating costs
165
+ # nothing and removes the sampler constraint a lookup would otherwise impose.
166
+
167
+ #: Rows in the published table. Checked, not assumed: a build on a different grid must not be read
168
+ #: as though it were on this one.
169
+ TABLE_ROWS = 1025
170
+
171
+
172
+ class TableEmbedder(nn.Module):
173
+ """Stands in for ``time_proj`` + ``time_embedder``, returning the pruned build's rank-8 row.
174
+
175
+ ``linear_1`` exists because the port reads ``time_embedder.linear_1.weight.dtype`` to cast its
176
+ input; it carries the table's dtype and nothing else, which keeps ``vendor/`` verbatim.
177
+ """
178
+
179
+ #: Declared so the buffer reads as a tensor; ``register_buffer`` alone types as ``Module``.
180
+ table: torch.Tensor
181
+
182
+ def __init__(self, table: torch.Tensor) -> None:
183
+ super().__init__()
184
+ self.register_buffer("table", table, persistent=True)
185
+ self.linear_1 = nn.Linear(1, 1, bias=False, dtype=table.dtype)
186
+
187
+ def forward(self, timestep: torch.Tensor) -> torch.Tensor:
188
+ rows = self.table.shape[0]
189
+ position = timestep.to(self.table.dtype).flatten() * (rows - 1)
190
+ low = position.floor().long().clamp(0, rows - 2)
191
+ frac = (position - low).unsqueeze(1)
192
+ return self.table[low] * (1 - frac) + self.table[low + 1] * frac
193
+
194
+
195
+ class TabulatedModulation(MiniMaxH3AdaLayerNormModulation):
196
+ """``adaln_proj`` reading a table row. It already holds ``silu(temb)``, so no activation."""
197
+
198
+ def forward(self, temb: torch.Tensor) -> tuple[torch.Tensor, ...]:
199
+ out = self.linear(temb.to(self.linear.weight.dtype)).view(-1, 6 * self.hidden_size)
200
+ return out.chunk(6, dim=-1)
201
+
202
+
203
+ class TabulatedNormOut(MiniMaxH3AdaLayerNormOut):
204
+ """``norm_out`` reading a table row, otherwise the port's own forward."""
205
+
206
+ def forward(
207
+ self, hidden_states: torch.Tensor, temb: torch.Tensor, timestep_indices: torch.Tensor
208
+ ) -> torch.Tensor:
209
+ shift, scale = self.linear(temb.to(self.linear.weight.dtype)).chunk(2, dim=-1)
210
+ hidden_states = self.norm(hidden_states)
211
+ return hidden_states * (1.0 + scale.index_select(0, timestep_indices)) + shift.index_select(
212
+ 0, timestep_indices
213
+ )
214
+
215
+
216
+ @torch.no_grad()
217
+ def tabulate(model: Any, table: torch.Tensor) -> None:
218
+ """Rebuild the timestep path around a pruned build's table, on the meta device before streaming.
219
+
220
+ Every module the table feeds changes shape, so this has to happen before any weight is placed:
221
+ the rank-8 ``adaln_proj`` in the file would otherwise be assigned into a ``[96768, 2688]`` slot.
222
+ """
223
+ if table.ndim != 2 or table.shape[0] != TABLE_ROWS:
224
+ raise ValueError(
225
+ f"adaln_t_table is {tuple(table.shape)}, not [{TABLE_ROWS}, rank]. This build is on a "
226
+ "different timestep grid from the one measured, and reading it as if it were not would "
227
+ "shift the modulation at every step while still rendering."
228
+ )
229
+ rank = int(table.shape[1])
230
+ model.time_proj = nn.Identity()
231
+ model.time_embedder = TableEmbedder(table)
232
+ for block in model.transformer_blocks:
233
+ block.adaln_proj = _retyped(block.adaln_proj, TabulatedModulation, rank)
234
+ model.norm_out = _retyped(model.norm_out, TabulatedNormOut, rank)
235
+
236
+
237
+ def _retyped(module: Any, cls: type, rank: int) -> Any:
238
+ """The same module with its projection narrowed to ``rank`` inputs, still on meta."""
239
+ replacement = module
240
+ replacement.__class__ = cls
241
+ old = module.linear
242
+ with torch.device("meta"):
243
+ replacement.linear = nn.Linear(rank, old.out_features, bias=old.bias is not None)
244
+ return replacement
245
+
246
+
247
+ #: Parameters ``tabulate`` creates that no checkpoint fills.
248
+ TABULATED_SELF_COMPUTED = ("time_embedder.linear_1.weight", "time_embedder.table")
@@ -23,7 +23,7 @@ fused QKV into three.
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- from ..keymap import AssertEqual, KeyPlan, Rename, RowLayout, Split, SwapHalves
26
+ from ..keymap import AssertEqual, Drop, KeyPlan, Rename, RowLayout, Split, SwapHalves
27
27
 
28
28
  #: Bumping this invalidates every prepared artifact built by the old plan.
29
29
  PLAN_VERSION = "minimax-h3.keys.1"
@@ -81,9 +81,17 @@ def build_plan(
81
81
  num_blocks: int = NUM_BLOCKS,
82
82
  num_refiner_blocks: int = NUM_REFINER_BLOCKS,
83
83
  head_dim: int = HEAD_DIM,
84
+ pruned: bool = False,
85
+ sidecars: tuple[str, ...] = (),
84
86
  ) -> KeyPlan:
85
87
  """The plan for a publisher's layout. ``source`` selects how the fused QKV rows are arranged.
86
88
 
89
+ ``pruned`` is the published rank-8 build: it ships ``adaln_t_table`` in place of the whole
90
+ timestep path, so the two ``time_embedder`` projections are simply not in the file.
91
+
92
+ ``sidecars`` are the quantisation tensors an fp8 build carries beside each weight. They are
93
+ consumed while streaming and dropped here, so the coverage check still accounts for every key.
94
+
87
95
  The counts are arguments so a round-trip test can exercise the same code at a size that fits in
88
96
  memory; the defaults are the released geometry.
89
97
  """
@@ -95,7 +103,16 @@ def build_plan(
95
103
  ) from None
96
104
 
97
105
  actions: dict[str, object] = {}
98
- for stem, target in _TOP_LEVEL.items():
106
+ top_level = {
107
+ stem: target
108
+ for stem, target in _TOP_LEVEL.items()
109
+ if not (pruned and stem.startswith("time_embedder."))
110
+ }
111
+ if pruned:
112
+ actions["adaln_t_table"] = Drop("read before streaming, to rebuild the timestep path")
113
+ for key in sidecars:
114
+ actions[key] = Drop("a quantisation scale, applied to its weight while streaming")
115
+ for stem, target in top_level.items():
99
116
  for suffix in ("weight", "bias"):
100
117
  actions[f"{stem}.{suffix}"] = Rename(f"{target}.{suffix}")
101
118
  for key, target in _WEIGHT_ONLY.items():
@@ -126,9 +143,16 @@ def build_plan(
126
143
  continue
127
144
  actions[f"{src}.{stem}"] = Rename(f"{dst}.{target}")
128
145
 
129
- return KeyPlan(version=f"{PLAN_VERSION}+{source}", actions=actions) # type: ignore[arg-type]
146
+ suffix = source + ("+pruned" if pruned else "") + ("+fp8" if sidecars else "")
147
+ return KeyPlan(version=f"{PLAN_VERSION}+{suffix}", actions=actions) # type: ignore[arg-type]
130
148
 
131
149
 
132
- def self_computed_targets() -> set[str]:
150
+ def self_computed_targets(*, pruned: bool = False) -> set[str]:
133
151
  """Targets the port builds itself, which ``check_coverage`` must not demand be filled."""
134
- return {"rope.inv_freq"}
152
+ if not pruned:
153
+ return {"rope.inv_freq"}
154
+ return {"rope.inv_freq", *_PRUNED_SELF_COMPUTED}
155
+
156
+
157
+ #: What ``adaln.tabulate`` creates in place of the timestep path. No checkpoint fills these.
158
+ _PRUNED_SELF_COMPUTED = ("time_embedder.table", "time_embedder.linear_1.weight")