Image Segmentation
Transformers
Safetensors
English
falcon_x
feature-extraction
falcon-x
vision-language
custom_code
Instructions to use JonathanJMK/FALCON with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use JonathanJMK/FALCON with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="JonathanJMK/FALCON", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("JonathanJMK/FALCON", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload complete FALCON model and project documentation
Browse files- LICENSE-Llama-2 +125 -0
- LICENSE-code +201 -0
- Notice +17 -0
- README.md +93 -0
- artifacts.py +79 -0
- capabilities.py +141 -0
- config.json +723 -0
- config.py +365 -0
- configuration_falcon.py +154 -0
- detector.py +1048 -0
- generation_config.json +4 -0
- grounding.py +691 -0
- model-00001-of-00004.safetensors +3 -0
- model-00002-of-00004.safetensors +3 -0
- model-00003-of-00004.safetensors +3 -0
- model-00004-of-00004.safetensors +3 -0
- model.py +990 -0
- model.safetensors.index.json +0 -0
- modeling_falcon.py +300 -0
- prediction.py +340 -0
- preprocessor_config.json +28 -0
- regions.py +210 -0
- safety.py +272 -0
- tokenizer/special_tokens_map.json +30 -0
- tokenizer/tokenizer.model +3 -0
- tokenizer/tokenizer_config.json +44 -0
LICENSE-Llama-2
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LLAMA 2 COMMUNITY LICENSE AGREEMENT
|
| 2 |
+
Llama 2 Version Release Date: July 18, 2023
|
| 3 |
+
|
| 4 |
+
"Agreement" means the terms and conditions for use, reproduction, distribution and
|
| 5 |
+
modification of the Llama Materials set forth herein.
|
| 6 |
+
|
| 7 |
+
"Documentation" means the specifications, manuals and documentation
|
| 8 |
+
accompanying Llama 2 distributed by Meta at ai.meta.com/resources/models-and-
|
| 9 |
+
libraries/llama-downloads/.
|
| 10 |
+
|
| 11 |
+
"Licensee" or "you" means you, or your employer or any other person or entity (if
|
| 12 |
+
you are entering into this Agreement on such person or entity's behalf), of the age
|
| 13 |
+
required under applicable laws, rules or regulations to provide legal consent and that
|
| 14 |
+
has legal authority to bind your employer or such other person or entity if you are
|
| 15 |
+
entering in this Agreement on their behalf.
|
| 16 |
+
|
| 17 |
+
"Llama 2" means the foundational large language models and software and
|
| 18 |
+
algorithms, including machine-learning model code, trained model weights,
|
| 19 |
+
inference-enabling code, training-enabling code, fine-tuning enabling code and other
|
| 20 |
+
elements of the foregoing distributed by Meta at ai.meta.com/resources/models-and-
|
| 21 |
+
libraries/llama-downloads/.
|
| 22 |
+
|
| 23 |
+
"Llama Materials" means, collectively, Meta's proprietary Llama 2 and
|
| 24 |
+
Documentation (and any portion thereof) made available under this Agreement.
|
| 25 |
+
|
| 26 |
+
"Meta" or "we" means Meta Platforms Ireland Limited (if you are located in or, if you
|
| 27 |
+
are an entity, your principal place of business is in the EEA or Switzerland) and Meta
|
| 28 |
+
Platforms, Inc. (if you are located outside of the EEA or Switzerland).
|
| 29 |
+
|
| 30 |
+
By clicking "I Accept" below or by using or distributing any portion or element of the
|
| 31 |
+
Llama Materials, you agree to be bound by this Agreement.
|
| 32 |
+
|
| 33 |
+
1. License Rights and Redistribution.
|
| 34 |
+
|
| 35 |
+
a. Grant of Rights. You are granted a non-exclusive, worldwide, non-
|
| 36 |
+
transferable and royalty-free limited license under Meta's intellectual property or
|
| 37 |
+
other rights owned by Meta embodied in the Llama Materials to use, reproduce,
|
| 38 |
+
distribute, copy, create derivative works of, and make modifications to the Llama
|
| 39 |
+
Materials.
|
| 40 |
+
|
| 41 |
+
b. Redistribution and Use.
|
| 42 |
+
|
| 43 |
+
i. If you distribute or make the Llama Materials, or any derivative works
|
| 44 |
+
thereof, available to a third party, you shall provide a copy of this Agreement to such
|
| 45 |
+
third party.
|
| 46 |
+
ii. If you receive Llama Materials, or any derivative works thereof, from
|
| 47 |
+
a Licensee as part of an integrated end user product, then Section 2 of this
|
| 48 |
+
Agreement will not apply to you.
|
| 49 |
+
|
| 50 |
+
iii. You must retain in all copies of the Llama Materials that you
|
| 51 |
+
distribute the following attribution notice within a "Notice" text file distributed as a
|
| 52 |
+
part of such copies: "Llama 2 is licensed under the LLAMA 2 Community License,
|
| 53 |
+
Copyright (c) Meta Platforms, Inc. All Rights Reserved."
|
| 54 |
+
|
| 55 |
+
iv. Your use of the Llama Materials must comply with applicable laws
|
| 56 |
+
and regulations (including trade compliance laws and regulations) and adhere to the
|
| 57 |
+
Acceptable Use Policy for the Llama Materials (available at
|
| 58 |
+
https://ai.meta.com/llama/use-policy), which is hereby incorporated by reference into
|
| 59 |
+
this Agreement.
|
| 60 |
+
|
| 61 |
+
v. You will not use the Llama Materials or any output or results of the
|
| 62 |
+
Llama Materials to improve any other large language model (excluding Llama 2 or
|
| 63 |
+
derivative works thereof).
|
| 64 |
+
|
| 65 |
+
2. Additional Commercial Terms. If, on the Llama 2 version release date, the
|
| 66 |
+
monthly active users of the products or services made available by or for Licensee,
|
| 67 |
+
or Licensee's affiliates, is greater than 700 million monthly active users in the
|
| 68 |
+
preceding calendar month, you must request a license from Meta, which Meta may
|
| 69 |
+
grant to you in its sole discretion, and you are not authorized to exercise any of the
|
| 70 |
+
rights under this Agreement unless or until Meta otherwise expressly grants you
|
| 71 |
+
such rights.
|
| 72 |
+
|
| 73 |
+
3. Disclaimer of Warranty. UNLESS REQUIRED BY APPLICABLE LAW, THE
|
| 74 |
+
LLAMA MATERIALS AND ANY OUTPUT AND RESULTS THEREFROM ARE
|
| 75 |
+
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
| 76 |
+
EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY
|
| 77 |
+
WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR
|
| 78 |
+
FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE
|
| 79 |
+
FOR DETERMINING THE APPROPRIATENESS OF USING OR REDISTRIBUTING
|
| 80 |
+
THE LLAMA MATERIALS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR
|
| 81 |
+
USE OF THE LLAMA MATERIALS AND ANY OUTPUT AND RESULTS.
|
| 82 |
+
|
| 83 |
+
4. Limitation of Liability. IN NO EVENT WILL META OR ITS AFFILIATES BE
|
| 84 |
+
LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT,
|
| 85 |
+
NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, ARISING OUT OF THIS
|
| 86 |
+
AGREEMENT, FOR ANY LOST PROFITS OR ANY INDIRECT, SPECIAL,
|
| 87 |
+
CONSEQUENTIAL, INCIDENTAL, EXEMPLARY OR PUNITIVE DAMAGES, EVEN
|
| 88 |
+
IF META OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF
|
| 89 |
+
ANY OF THE FOREGOING.
|
| 90 |
+
|
| 91 |
+
5. Intellectual Property.
|
| 92 |
+
|
| 93 |
+
a. No trademark licenses are granted under this Agreement, and in
|
| 94 |
+
connection with the Llama Materials, neither Meta nor Licensee may use any name
|
| 95 |
+
or mark owned by or associated with the other or any of its affiliates, except as
|
| 96 |
+
required for reasonable and customary use in describing and redistributing the
|
| 97 |
+
Llama Materials.
|
| 98 |
+
|
| 99 |
+
b. Subject to Meta's ownership of Llama Materials and derivatives made by or
|
| 100 |
+
for Meta, with respect to any derivative works and modifications of the Llama
|
| 101 |
+
Materials that are made by you, as between you and Meta, you are and will be the
|
| 102 |
+
owner of such derivative works and modifications.
|
| 103 |
+
|
| 104 |
+
c. If you institute litigation or other proceedings against Meta or any entity
|
| 105 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that the Llama
|
| 106 |
+
Materials or Llama 2 outputs or results, or any portion of any of the foregoing,
|
| 107 |
+
constitutes an infringement of intellectual property or other rights owned or licensable
|
| 108 |
+
by you, then any licenses granted to you under this Agreement shall terminate as of
|
| 109 |
+
the date such litigation or claim is filed or instituted. You will indemnify and hold
|
| 110 |
+
harmless Meta from and against any claim by any third party arising out of or related
|
| 111 |
+
to your use or distribution of the Llama Materials.
|
| 112 |
+
|
| 113 |
+
6. Term and Termination. The term of this Agreement will commence upon your
|
| 114 |
+
acceptance of this Agreement or access to the Llama Materials and will continue in
|
| 115 |
+
full force and effect until terminated in accordance with the terms and conditions
|
| 116 |
+
herein. Meta may terminate this Agreement if you are in breach of any term or
|
| 117 |
+
condition of this Agreement. Upon termination of this Agreement, you shall delete
|
| 118 |
+
and cease use of the Llama Materials. Sections 3, 4 and 7 shall survive the
|
| 119 |
+
termination of this Agreement.
|
| 120 |
+
|
| 121 |
+
7. Governing Law and Jurisdiction. This Agreement will be governed and
|
| 122 |
+
construed under the laws of the State of California without regard to choice of law
|
| 123 |
+
principles, and the UN Convention on Contracts for the International Sale of Goods
|
| 124 |
+
does not apply to this Agreement. The courts of California shall have exclusive
|
| 125 |
+
jurisdiction of any dispute arising out of this Agreement.
|
LICENSE-code
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
Notice
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Llama 2 is licensed under the LLAMA 2 Community License, Copyright (c) Meta
|
| 2 |
+
Platforms, Inc. All Rights Reserved.
|
| 3 |
+
|
| 4 |
+
Vicuna-7B-v1.5: LMSYS, derived from Llama 2.
|
| 5 |
+
https://huggingface.co/lmsys/vicuna-7b-v1.5
|
| 6 |
+
License: LICENSE-Llama-2. Acceptable Use Policy:
|
| 7 |
+
https://huggingface.co/meta-llama/Llama-2-7b/blob/main/USE_POLICY.md
|
| 8 |
+
|
| 9 |
+
DINOv2: Meta Platforms, Inc. and affiliates, Apache-2.0.
|
| 10 |
+
https://github.com/facebookresearch/dinov2
|
| 11 |
+
|
| 12 |
+
RF-DETR segmentation 1.5.2: Roboflow, Apache-2.0.
|
| 13 |
+
https://github.com/roboflow/rf-detr/tree/1.5.2
|
| 14 |
+
|
| 15 |
+
FALCON code: Apache-2.0. See LICENSE-code for the Apache-2.0 license text.
|
| 16 |
+
The final model includes fine-tuned detector, adapter and LoRA weights;
|
| 17 |
+
upstream model licenses remain applicable.
|
README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: llama2
|
| 4 |
+
base_model:
|
| 5 |
+
- lmsys/vicuna-7b-v1.5
|
| 6 |
+
- facebook/dinov2-large
|
| 7 |
+
tags:
|
| 8 |
+
- falcon-x
|
| 9 |
+
- vision-language
|
| 10 |
+
- image-segmentation
|
| 11 |
+
- custom_code
|
| 12 |
+
language:
|
| 13 |
+
- en
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# FALCON
|
| 17 |
+
|
| 18 |
+
Functional Assembly and Language for Compositional Reasoning in X-ray.
|
| 19 |
+
|
| 20 |
+
[Paper](https://arxiv.org/abs/2606.25701) ·
|
| 21 |
+
[Project page](https://yonathan-kiflom.github.io/FALCON/page/) ·
|
| 22 |
+
[Code](https://github.com/yonathan-kiflom/FALCON)
|
| 23 |
+
|
| 24 |
+
This repository contains the completed Stage-3 model, including Vicuna-7B-v1.5,
|
| 25 |
+
DINOv2-L/14, the trained RF-DETR segmentation detector, multimodal adapters,
|
| 26 |
+
and unmerged LoRA weights.
|
| 27 |
+
|
| 28 |
+
## Usage
|
| 29 |
+
|
| 30 |
+
Install the pinned dependencies from the code repository:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
pip install 'falcon-x[train] @ git+https://github.com/yonathan-kiflom/FALCON.git'
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Use `JonathanJMK/FALCON` or a local downloaded model directory. Authenticate
|
| 37 |
+
with `huggingface-cli login` when accessing the private repository. Review the
|
| 38 |
+
custom code before trusting it; pin a Hub commit with `revision=` for reproducible use.
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from transformers import AutoModel
|
| 42 |
+
|
| 43 |
+
model = AutoModel.from_pretrained(
|
| 44 |
+
"JonathanJMK/FALCON", trust_remote_code=True,
|
| 45 |
+
).to("cuda").eval()
|
| 46 |
+
|
| 47 |
+
result, masks = model.predict(
|
| 48 |
+
image="image.png", prompt="Describe the image.", max_new_tokens=256,
|
| 49 |
+
)
|
| 50 |
+
print(result["answer"])
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Use `predict_segmentation` for grounding prompts and `predict_panoptic` for
|
| 54 |
+
panoptic prompts. Tokenization, image preprocessing, and prompt formatting are
|
| 55 |
+
included. The model supports one device; automatic multi-device dispatch and
|
| 56 |
+
quantized loading are not supported. The saved per-component precision is
|
| 57 |
+
preserved; do not cast the entire model to half precision.
|
| 58 |
+
|
| 59 |
+
Evaluate using the same model:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
python -m falcon evaluate run --model JonathanJMK/FALCON \
|
| 63 |
+
--dataset /path/to/falcon-x --split test --tasks all \
|
| 64 |
+
--run-dir runs/falcon-evaluation --device cuda
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## Scope and limitations
|
| 68 |
+
|
| 69 |
+
Trained on falcon-x for X-ray descriptions, questions, component
|
| 70 |
+
presence/completeness, instance grounding, and segmentation. Generated answers
|
| 71 |
+
and masks can be wrong; this is a research model, not a certified screening
|
| 72 |
+
system. Available structured heads are declared in `config.json`; untrained
|
| 73 |
+
risk and physical-link heads are not presented as predictions. Counterfactual
|
| 74 |
+
completeness does not establish real-world danger or physical connectivity.
|
| 75 |
+
|
| 76 |
+
The package preserves the trained architecture and LoRA for further research.
|
| 77 |
+
It is not an optimizer-state checkpoint, and loading with Transformers alone
|
| 78 |
+
does not make the repository's stage-training CLI a general fine-tuning tool.
|
| 79 |
+
Task metrics and evaluation limitations are documented in the code repository.
|
| 80 |
+
|
| 81 |
+
## Licenses
|
| 82 |
+
|
| 83 |
+
Vicuna is derived from Llama 2 and retains the
|
| 84 |
+
[Llama 2 Community License](https://huggingface.co/meta-llama/Llama-2-7b/blob/main/LICENSE.txt)
|
| 85 |
+
and [Acceptable Use Policy](https://huggingface.co/meta-llama/Llama-2-7b/blob/main/USE_POLICY.md).
|
| 86 |
+
Llama 2 is licensed under the LLAMA 2 Community License, Copyright (c) Meta
|
| 87 |
+
Platforms, Inc. All Rights Reserved.
|
| 88 |
+
|
| 89 |
+
[DINOv2](https://github.com/facebookresearch/dinov2/blob/main/LICENSE) and
|
| 90 |
+
[RF-DETR segmentation 1.5.2](https://github.com/roboflow/rf-detr/blob/1.5.2/LICENSE)
|
| 91 |
+
use Apache-2.0. FALCON code is Apache-2.0 (`LICENSE-code`); this does not
|
| 92 |
+
relicense upstream model weights. Retain `LICENSE-Llama-2`, `LICENSE-code` and
|
| 93 |
+
`Notice` when redistributing the full package.
|
artifacts.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File output and panoptic image utilities."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import tempfile
|
| 8 |
+
from collections.abc import Iterable
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def panoptic_ids_from_image(image):
|
| 14 |
+
"""Decode integer or RGB(A) panoptic IDs without rescaling."""
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
array = np.asarray(image)
|
| 19 |
+
if array.ndim == 2:
|
| 20 |
+
if not np.issubdtype(array.dtype, np.integer):
|
| 21 |
+
raise ValueError("panoptic map must contain integer IDs")
|
| 22 |
+
return array.astype(np.int64)
|
| 23 |
+
if array.ndim == 3 and array.shape[2] in (3, 4):
|
| 24 |
+
rgb = array[..., :3].astype(np.int64)
|
| 25 |
+
return rgb[..., 0] + 256 * rgb[..., 1] + 65536 * rgb[..., 2]
|
| 26 |
+
raise ValueError("panoptic PNG must be integer or RGB")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def external_output(
|
| 30 |
+
path: str | Path,
|
| 31 |
+
dataset_root: str | Path,
|
| 32 |
+
*,
|
| 33 |
+
protected_roots: Iterable[str | Path] = (),
|
| 34 |
+
) -> Path:
|
| 35 |
+
"""Keep generated files outside dataset and annotation directories."""
|
| 36 |
+
|
| 37 |
+
output = Path(path).expanduser().resolve()
|
| 38 |
+
requested_roots = (dataset_root, *tuple(protected_roots))
|
| 39 |
+
immutable_roots: list[Path] = []
|
| 40 |
+
for raw_root in requested_roots:
|
| 41 |
+
root = Path(raw_root).expanduser().resolve(strict=True)
|
| 42 |
+
if not root.is_dir():
|
| 43 |
+
raise ValueError(f"Protected input root is not a directory: {root}")
|
| 44 |
+
if root not in immutable_roots:
|
| 45 |
+
immutable_roots.append(root)
|
| 46 |
+
for root in immutable_roots:
|
| 47 |
+
if output == root or root in output.parents or output in root.parents:
|
| 48 |
+
raise ValueError(
|
| 49 |
+
"Derived output must be outside immutable input roots and not an ancestor of one"
|
| 50 |
+
)
|
| 51 |
+
return output
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def atomic_json(path: str | Path, value: Any) -> None:
|
| 55 |
+
"""Write a complete UTF-8 JSON document, then atomically replace its target."""
|
| 56 |
+
path = Path(path)
|
| 57 |
+
encoded = (
|
| 58 |
+
json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False, allow_nan=False)
|
| 59 |
+
+ "\n"
|
| 60 |
+
)
|
| 61 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 62 |
+
temporary = None
|
| 63 |
+
try:
|
| 64 |
+
with tempfile.NamedTemporaryFile(
|
| 65 |
+
mode="w",
|
| 66 |
+
encoding="utf-8",
|
| 67 |
+
dir=path.parent,
|
| 68 |
+
prefix=f".{path.name}.",
|
| 69 |
+
suffix=".tmp",
|
| 70 |
+
delete=False,
|
| 71 |
+
) as stream:
|
| 72 |
+
temporary = Path(stream.name)
|
| 73 |
+
stream.write(encoded)
|
| 74 |
+
stream.flush()
|
| 75 |
+
os.fsync(stream.fileno())
|
| 76 |
+
os.replace(temporary, path)
|
| 77 |
+
finally:
|
| 78 |
+
if temporary is not None and temporary.exists():
|
| 79 |
+
temporary.unlink()
|
capabilities.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit structured-head capabilities, independent of model dependencies.
|
| 2 |
+
|
| 3 |
+
Reference availability, training coverage and prediction availability are separate
|
| 4 |
+
facts. A capability records which trained heads may be used; it does not certify
|
| 5 |
+
accuracy. Missing checkpoint metadata must not be interpreted as all-enabled.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from collections.abc import Iterable, Mapping
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class SafetyCapabilities:
|
| 18 |
+
risk: bool = True
|
| 19 |
+
presence: tuple[bool, bool, bool] = (True, True, True)
|
| 20 |
+
links: tuple[bool, bool, bool] = (True, True, True)
|
| 21 |
+
|
| 22 |
+
def __post_init__(self) -> None:
|
| 23 |
+
if not isinstance(self.risk, bool):
|
| 24 |
+
raise ValueError("risk capability must be boolean")
|
| 25 |
+
for name in ("presence", "links"):
|
| 26 |
+
values = getattr(self, name)
|
| 27 |
+
if (
|
| 28 |
+
not isinstance(values, tuple)
|
| 29 |
+
or len(values) != 3
|
| 30 |
+
or any(not isinstance(value, bool) for value in values)
|
| 31 |
+
):
|
| 32 |
+
raise ValueError(f"{name} capabilities must be a tuple of three booleans")
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def token_indices(self) -> tuple[int, ...]:
|
| 36 |
+
flags = (self.risk, *self.presence, *self.links)
|
| 37 |
+
return tuple(index for index, enabled in enumerate(flags) if enabled)
|
| 38 |
+
|
| 39 |
+
def as_dict(self) -> dict[str, Any]:
|
| 40 |
+
return {"risk": self.risk, "presence": list(self.presence), "links": list(self.links)}
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def unavailable(cls) -> SafetyCapabilities:
|
| 44 |
+
return cls(False, (False, False, False), (False, False, False))
|
| 45 |
+
|
| 46 |
+
@classmethod
|
| 47 |
+
def from_dict(cls, value: Mapping[str, Any]) -> SafetyCapabilities:
|
| 48 |
+
if not isinstance(value, Mapping) or set(value) != {"risk", "presence", "links"}:
|
| 49 |
+
raise ValueError("safety capabilities require exactly risk, presence, and links")
|
| 50 |
+
if not isinstance(value["presence"], list | tuple) or not isinstance(
|
| 51 |
+
value["links"], list | tuple
|
| 52 |
+
):
|
| 53 |
+
raise ValueError("presence and links capabilities must be arrays")
|
| 54 |
+
return cls(value["risk"], tuple(value["presence"]), tuple(value["links"]))
|
| 55 |
+
|
| 56 |
+
def restrict(self, allowed: SafetyCapabilities) -> SafetyCapabilities:
|
| 57 |
+
"""Ablations can disable capabilities, never enable untrained heads."""
|
| 58 |
+
return SafetyCapabilities(
|
| 59 |
+
self.risk and allowed.risk,
|
| 60 |
+
tuple(a and b for a, b in zip(self.presence, allowed.presence, strict=True)),
|
| 61 |
+
tuple(a and b for a, b in zip(self.links, allowed.links, strict=True)),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
ALL_SAFETY_HEADS = SafetyCapabilities()
|
| 66 |
+
SSA_ABLATIONS = ("none", "no_ssa", "no_presence", "no_links", "no_risk")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def apply_ablation(capabilities: SafetyCapabilities, name: str) -> SafetyCapabilities:
|
| 70 |
+
"""Disable named heads before token construction; never invent capability."""
|
| 71 |
+
if name not in SSA_ABLATIONS:
|
| 72 |
+
raise ValueError(f"unknown SSA ablation {name!r}")
|
| 73 |
+
mask = {
|
| 74 |
+
"none": ALL_SAFETY_HEADS,
|
| 75 |
+
"no_ssa": SafetyCapabilities.unavailable(),
|
| 76 |
+
"no_presence": SafetyCapabilities(True, (False, False, False), (True, True, True)),
|
| 77 |
+
"no_links": SafetyCapabilities(True, (True, True, True), (False, False, False)),
|
| 78 |
+
"no_risk": SafetyCapabilities(False, (True, True, True), (True, True, True)),
|
| 79 |
+
}[name]
|
| 80 |
+
return capabilities.restrict(mask)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def supervision_coverage(rows: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
|
| 84 |
+
"""Count authoritative labels per image, without substituting missing values.
|
| 85 |
+
|
| 86 |
+
Callers must provide unique training images and validated provenance. This
|
| 87 |
+
function validates numeric domains, not the authority of the source itself.
|
| 88 |
+
"""
|
| 89 |
+
coverage = {"images": 0, "risk": 0, "presence": [0, 0, 0], "links": [0, 0, 0]}
|
| 90 |
+
|
| 91 |
+
def observed(value: Any, *, binary: bool = False) -> bool:
|
| 92 |
+
if value is None:
|
| 93 |
+
return False
|
| 94 |
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
| 95 |
+
raise ValueError("structured labels must be numeric or null")
|
| 96 |
+
if not math.isfinite(value) or not 0 <= value <= 1:
|
| 97 |
+
raise ValueError("structured labels must be finite and in [0, 1]")
|
| 98 |
+
if binary and value not in (0, 1):
|
| 99 |
+
raise ValueError("presence labels must be binary")
|
| 100 |
+
return True
|
| 101 |
+
|
| 102 |
+
for row in rows:
|
| 103 |
+
if not isinstance(row, Mapping):
|
| 104 |
+
raise ValueError("structured targets must be objects")
|
| 105 |
+
coverage["images"] += 1
|
| 106 |
+
coverage["risk"] += int(observed(row.get("risk")))
|
| 107 |
+
for name in ("presence", "links"):
|
| 108 |
+
values = row.get(name, [None, None, None])
|
| 109 |
+
if not isinstance(values, list | tuple) or len(values) != 3:
|
| 110 |
+
raise ValueError(f"{name} targets must have three entries")
|
| 111 |
+
for index, value in enumerate(values):
|
| 112 |
+
coverage[name][index] += int(observed(value, binary=name == "presence"))
|
| 113 |
+
return coverage
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def capabilities_from_coverage(coverage: Mapping[str, Any]) -> SafetyCapabilities:
|
| 117 |
+
"""An observed label enables training, not a claim of calibrated predictions."""
|
| 118 |
+
for name in ("images", "risk"):
|
| 119 |
+
value = coverage.get(name)
|
| 120 |
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
| 121 |
+
raise ValueError(f"{name} coverage must be a nonnegative integer")
|
| 122 |
+
counts = []
|
| 123 |
+
for name in ("presence", "links"):
|
| 124 |
+
values = coverage.get(name)
|
| 125 |
+
if (
|
| 126 |
+
not isinstance(values, list | tuple)
|
| 127 |
+
or len(values) != 3
|
| 128 |
+
or any(
|
| 129 |
+
isinstance(value, bool) or not isinstance(value, int) or value < 0
|
| 130 |
+
for value in values
|
| 131 |
+
)
|
| 132 |
+
):
|
| 133 |
+
raise ValueError(f"{name} coverage must contain three nonnegative integers")
|
| 134 |
+
counts.extend(values)
|
| 135 |
+
if any(value > coverage["images"] for value in [coverage["risk"], *counts]):
|
| 136 |
+
raise ValueError("label coverage cannot exceed training image count")
|
| 137 |
+
return SafetyCapabilities(
|
| 138 |
+
coverage["risk"] > 0,
|
| 139 |
+
tuple(value > 0 for value in coverage["presence"]),
|
| 140 |
+
tuple(value > 0 for value in coverage["links"]),
|
| 141 |
+
)
|
config.json
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"FalconModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "configuration_falcon.FalconConfig",
|
| 7 |
+
"AutoModel": "modeling_falcon.FalconModel"
|
| 8 |
+
},
|
| 9 |
+
"checkpoint_metadata": {
|
| 10 |
+
"checkpoint_format": "falcon-checkpoint-v1",
|
| 11 |
+
"config": {
|
| 12 |
+
"detector": {
|
| 13 |
+
"mask_threshold": 0.5,
|
| 14 |
+
"max_regions": 100,
|
| 15 |
+
"nms_threshold": 0.6,
|
| 16 |
+
"resolution": 768,
|
| 17 |
+
"score_threshold": 0.15,
|
| 18 |
+
"variant": "seg-2xlarge"
|
| 19 |
+
},
|
| 20 |
+
"experiment": {
|
| 21 |
+
"backbone_layout": "independent",
|
| 22 |
+
"name": "falcon-x",
|
| 23 |
+
"protocol": "dataset-v1",
|
| 24 |
+
"ssa_ablation": "none",
|
| 25 |
+
"world_size": null
|
| 26 |
+
},
|
| 27 |
+
"model": {
|
| 28 |
+
"component_order": [
|
| 29 |
+
"detonator",
|
| 30 |
+
"explosive",
|
| 31 |
+
"battery"
|
| 32 |
+
],
|
| 33 |
+
"image_size": 448,
|
| 34 |
+
"language_model": "lmsys/vicuna-7b-v1.5",
|
| 35 |
+
"link_order": [
|
| 36 |
+
[
|
| 37 |
+
"battery",
|
| 38 |
+
"detonator"
|
| 39 |
+
],
|
| 40 |
+
[
|
| 41 |
+
"battery",
|
| 42 |
+
"explosive"
|
| 43 |
+
],
|
| 44 |
+
[
|
| 45 |
+
"detonator",
|
| 46 |
+
"explosive"
|
| 47 |
+
]
|
| 48 |
+
],
|
| 49 |
+
"lora_alpha": 32,
|
| 50 |
+
"lora_dropout": 0.05,
|
| 51 |
+
"lora_rank": 16,
|
| 52 |
+
"max_regions": 100,
|
| 53 |
+
"patch_size": 14,
|
| 54 |
+
"region_dim": 1024,
|
| 55 |
+
"roi_size": 4,
|
| 56 |
+
"vision_model": "facebook/dinov2-large"
|
| 57 |
+
},
|
| 58 |
+
"stages": {
|
| 59 |
+
"stage1": {
|
| 60 |
+
"batch_size": 1,
|
| 61 |
+
"encoder_learning_rate": 0.00015,
|
| 62 |
+
"epochs": 12,
|
| 63 |
+
"expanded_scales": false,
|
| 64 |
+
"gradient_accumulation": 16,
|
| 65 |
+
"learning_rate": 0.0001,
|
| 66 |
+
"lr_scheduler": "cosine",
|
| 67 |
+
"multi_scale": false,
|
| 68 |
+
"precision": "backend_mixed",
|
| 69 |
+
"tf32": true,
|
| 70 |
+
"warmup_epochs": 0.0,
|
| 71 |
+
"weight_decay": 0.0001
|
| 72 |
+
},
|
| 73 |
+
"stage2": {
|
| 74 |
+
"batch_size": 4,
|
| 75 |
+
"epoch_size": null,
|
| 76 |
+
"epochs": 1,
|
| 77 |
+
"gradient_accumulation": 4,
|
| 78 |
+
"learning_rate": 0.0001,
|
| 79 |
+
"link_loss_weight": 0.5,
|
| 80 |
+
"max_text_tokens": 256,
|
| 81 |
+
"precision": "bf16",
|
| 82 |
+
"presence_loss_weight": 0.5,
|
| 83 |
+
"risk_loss_weight": 1.0,
|
| 84 |
+
"sampling": "all",
|
| 85 |
+
"seed": 42,
|
| 86 |
+
"text_overflow_policy": "error",
|
| 87 |
+
"tf32": true,
|
| 88 |
+
"warmup_ratio": 0.03,
|
| 89 |
+
"weight_decay": 0.0
|
| 90 |
+
},
|
| 91 |
+
"stage3": {
|
| 92 |
+
"batch_size": 1,
|
| 93 |
+
"epoch_size": null,
|
| 94 |
+
"epochs": 1,
|
| 95 |
+
"gradient_accumulation": 16,
|
| 96 |
+
"learning_rate": 1e-05,
|
| 97 |
+
"link_loss_weight": 0.5,
|
| 98 |
+
"max_text_tokens": 256,
|
| 99 |
+
"precision": "bf16",
|
| 100 |
+
"presence_loss_weight": 0.5,
|
| 101 |
+
"risk_loss_weight": 1.0,
|
| 102 |
+
"sampling": "all",
|
| 103 |
+
"seed": 42,
|
| 104 |
+
"text_overflow_policy": "error",
|
| 105 |
+
"tf32": true,
|
| 106 |
+
"warmup_ratio": 0.03,
|
| 107 |
+
"weight_decay": 0.0
|
| 108 |
+
}
|
| 109 |
+
},
|
| 110 |
+
"training": {
|
| 111 |
+
"batch_size": 1,
|
| 112 |
+
"epoch_size": null,
|
| 113 |
+
"epochs": 1,
|
| 114 |
+
"gradient_accumulation": 16,
|
| 115 |
+
"learning_rate": 0.0001,
|
| 116 |
+
"link_loss_weight": 0.5,
|
| 117 |
+
"max_text_tokens": 256,
|
| 118 |
+
"precision": "bf16",
|
| 119 |
+
"presence_loss_weight": 0.5,
|
| 120 |
+
"risk_loss_weight": 1.0,
|
| 121 |
+
"sampling": "all",
|
| 122 |
+
"seed": 42,
|
| 123 |
+
"text_overflow_policy": "error",
|
| 124 |
+
"tf32": true,
|
| 125 |
+
"warmup_ratio": 0.03,
|
| 126 |
+
"weight_decay": 0.0
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"dataset_categories": [
|
| 130 |
+
{
|
| 131 |
+
"id": 1,
|
| 132 |
+
"name": "detonator"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"id": 2,
|
| 136 |
+
"name": "explosive"
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": 3,
|
| 140 |
+
"name": "battery"
|
| 141 |
+
}
|
| 142 |
+
],
|
| 143 |
+
"diagnostic_training_reasons": [],
|
| 144 |
+
"grounding_coverage": {
|
| 145 |
+
"full_match": 177693,
|
| 146 |
+
"ground_truth_instances": 238792,
|
| 147 |
+
"language_supervision_masked": 1405,
|
| 148 |
+
"matched_instances": 237385,
|
| 149 |
+
"partial_match": 496,
|
| 150 |
+
"true_negative": 6,
|
| 151 |
+
"unmatched_positive": 909
|
| 152 |
+
},
|
| 153 |
+
"grounding_policy": {
|
| 154 |
+
"iou_threshold": 0.5,
|
| 155 |
+
"unmatched": "mask_language_loss",
|
| 156 |
+
"version": "falcon-grounding-match/cardinality-iou-v1"
|
| 157 |
+
},
|
| 158 |
+
"observed_supervision_coverage": {
|
| 159 |
+
"images": 34826,
|
| 160 |
+
"links": [
|
| 161 |
+
0,
|
| 162 |
+
0,
|
| 163 |
+
0
|
| 164 |
+
],
|
| 165 |
+
"presence": [
|
| 166 |
+
34826,
|
| 167 |
+
34826,
|
| 168 |
+
34826
|
| 169 |
+
],
|
| 170 |
+
"risk": 0
|
| 171 |
+
},
|
| 172 |
+
"official_result_eligible": true,
|
| 173 |
+
"precision": "bf16",
|
| 174 |
+
"require_observed_supervision": true,
|
| 175 |
+
"safety_capabilities": {
|
| 176 |
+
"links": [
|
| 177 |
+
false,
|
| 178 |
+
false,
|
| 179 |
+
false
|
| 180 |
+
],
|
| 181 |
+
"presence": [
|
| 182 |
+
true,
|
| 183 |
+
true,
|
| 184 |
+
true
|
| 185 |
+
],
|
| 186 |
+
"risk": false
|
| 187 |
+
},
|
| 188 |
+
"ssa_ablation": "none",
|
| 189 |
+
"stage": 3,
|
| 190 |
+
"structured_loss_recipe": {
|
| 191 |
+
"links": "sigmoid_l1",
|
| 192 |
+
"presence": "bce_with_logits_fp32",
|
| 193 |
+
"reduction": "mean_over_finite_enabled_targets",
|
| 194 |
+
"risk": "sigmoid_l1"
|
| 195 |
+
},
|
| 196 |
+
"supervision_capabilities": {
|
| 197 |
+
"links": [
|
| 198 |
+
false,
|
| 199 |
+
false,
|
| 200 |
+
false
|
| 201 |
+
],
|
| 202 |
+
"presence": [
|
| 203 |
+
true,
|
| 204 |
+
true,
|
| 205 |
+
true
|
| 206 |
+
],
|
| 207 |
+
"risk": false
|
| 208 |
+
},
|
| 209 |
+
"training_complete": true
|
| 210 |
+
},
|
| 211 |
+
"core_config": {
|
| 212 |
+
"image_size": 448,
|
| 213 |
+
"language_dim": 4096,
|
| 214 |
+
"link_loss_weight": 0.5,
|
| 215 |
+
"mask_probability_threshold": 0.5,
|
| 216 |
+
"max_regions": 100,
|
| 217 |
+
"max_text_tokens": 256,
|
| 218 |
+
"patch_size": 14,
|
| 219 |
+
"presence_loss_weight": 0.5,
|
| 220 |
+
"region_dim": 1024,
|
| 221 |
+
"risk_loss_weight": 1.0,
|
| 222 |
+
"roi_size": 4,
|
| 223 |
+
"safety_capabilities": {
|
| 224 |
+
"links": [
|
| 225 |
+
false,
|
| 226 |
+
false,
|
| 227 |
+
false
|
| 228 |
+
],
|
| 229 |
+
"presence": [
|
| 230 |
+
true,
|
| 231 |
+
true,
|
| 232 |
+
true
|
| 233 |
+
],
|
| 234 |
+
"risk": false
|
| 235 |
+
},
|
| 236 |
+
"text_overflow_policy": "error",
|
| 237 |
+
"vision_dim": 1024,
|
| 238 |
+
"vision_prefix_tokens": 1
|
| 239 |
+
},
|
| 240 |
+
"detector_config": {
|
| 241 |
+
"architecture": {
|
| 242 |
+
"amp": true,
|
| 243 |
+
"bbox_reparam": true,
|
| 244 |
+
"ca_nheads": 16,
|
| 245 |
+
"cls_loss_coef": 1.0,
|
| 246 |
+
"dec_layers": 6,
|
| 247 |
+
"dec_n_points": 2,
|
| 248 |
+
"encoder": "dinov2_windowed_small",
|
| 249 |
+
"gradient_checkpointing": false,
|
| 250 |
+
"group_detr": 13,
|
| 251 |
+
"hidden_dim": 256,
|
| 252 |
+
"ia_bce_loss": true,
|
| 253 |
+
"layer_norm": true,
|
| 254 |
+
"license": "Apache-2.0",
|
| 255 |
+
"lite_refpoint_refine": true,
|
| 256 |
+
"mask_downsample_ratio": 4,
|
| 257 |
+
"num_classes": 1,
|
| 258 |
+
"num_queries": 300,
|
| 259 |
+
"num_select": 300,
|
| 260 |
+
"num_windows": 2,
|
| 261 |
+
"out_feature_indexes": [
|
| 262 |
+
3,
|
| 263 |
+
6,
|
| 264 |
+
9,
|
| 265 |
+
12
|
| 266 |
+
],
|
| 267 |
+
"patch_size": 12,
|
| 268 |
+
"positional_encoding_size": 64,
|
| 269 |
+
"projector_scale": [
|
| 270 |
+
"P4"
|
| 271 |
+
],
|
| 272 |
+
"resolution": 768,
|
| 273 |
+
"sa_nheads": 8,
|
| 274 |
+
"segmentation_head": true,
|
| 275 |
+
"two_stage": true
|
| 276 |
+
},
|
| 277 |
+
"proposal_policy": {
|
| 278 |
+
"mask_threshold": 0.5,
|
| 279 |
+
"max_regions": 100,
|
| 280 |
+
"nms_threshold": 0.6,
|
| 281 |
+
"reject_empty_masks": true,
|
| 282 |
+
"score_threshold": 0.15,
|
| 283 |
+
"version": "falcon-proposals/v1"
|
| 284 |
+
},
|
| 285 |
+
"variant": "seg-2xlarge"
|
| 286 |
+
},
|
| 287 |
+
"image_processor_config": {
|
| 288 |
+
"_processor_class": null,
|
| 289 |
+
"crop_size": {
|
| 290 |
+
"height": 448,
|
| 291 |
+
"width": 448
|
| 292 |
+
},
|
| 293 |
+
"do_center_crop": true,
|
| 294 |
+
"do_convert_rgb": true,
|
| 295 |
+
"do_normalize": true,
|
| 296 |
+
"do_rescale": true,
|
| 297 |
+
"do_resize": true,
|
| 298 |
+
"image_mean": [
|
| 299 |
+
0.485,
|
| 300 |
+
0.456,
|
| 301 |
+
0.406
|
| 302 |
+
],
|
| 303 |
+
"image_processor_type": "BitImageProcessor",
|
| 304 |
+
"image_std": [
|
| 305 |
+
0.229,
|
| 306 |
+
0.224,
|
| 307 |
+
0.225
|
| 308 |
+
],
|
| 309 |
+
"resample": 3,
|
| 310 |
+
"rescale_factor": 0.00392156862745098,
|
| 311 |
+
"size": {
|
| 312 |
+
"height": 448,
|
| 313 |
+
"width": 448
|
| 314 |
+
}
|
| 315 |
+
},
|
| 316 |
+
"language_config": {
|
| 317 |
+
"_attn_implementation_autoset": true,
|
| 318 |
+
"add_cross_attention": false,
|
| 319 |
+
"architectures": [
|
| 320 |
+
"LlamaForCausalLM"
|
| 321 |
+
],
|
| 322 |
+
"attention_bias": false,
|
| 323 |
+
"attention_dropout": 0.0,
|
| 324 |
+
"attn_implementation": "sdpa",
|
| 325 |
+
"bad_words_ids": null,
|
| 326 |
+
"begin_suppress_tokens": null,
|
| 327 |
+
"bos_token_id": 1,
|
| 328 |
+
"chunk_size_feed_forward": 0,
|
| 329 |
+
"cross_attention_hidden_size": null,
|
| 330 |
+
"decoder_start_token_id": null,
|
| 331 |
+
"diversity_penalty": 0.0,
|
| 332 |
+
"do_sample": false,
|
| 333 |
+
"early_stopping": false,
|
| 334 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 335 |
+
"eos_token_id": 2,
|
| 336 |
+
"exponential_decay_length_penalty": null,
|
| 337 |
+
"finetuning_task": null,
|
| 338 |
+
"forced_bos_token_id": null,
|
| 339 |
+
"forced_eos_token_id": null,
|
| 340 |
+
"head_dim": 128,
|
| 341 |
+
"hidden_act": "silu",
|
| 342 |
+
"hidden_size": 4096,
|
| 343 |
+
"id2label": {
|
| 344 |
+
"0": "LABEL_0",
|
| 345 |
+
"1": "LABEL_1"
|
| 346 |
+
},
|
| 347 |
+
"initializer_range": 0.02,
|
| 348 |
+
"intermediate_size": 11008,
|
| 349 |
+
"is_decoder": false,
|
| 350 |
+
"is_encoder_decoder": false,
|
| 351 |
+
"label2id": {
|
| 352 |
+
"LABEL_0": 0,
|
| 353 |
+
"LABEL_1": 1
|
| 354 |
+
},
|
| 355 |
+
"length_penalty": 1.0,
|
| 356 |
+
"max_length": 20,
|
| 357 |
+
"max_position_embeddings": 4096,
|
| 358 |
+
"min_length": 0,
|
| 359 |
+
"mlp_bias": false,
|
| 360 |
+
"model_type": "llama",
|
| 361 |
+
"no_repeat_ngram_size": 0,
|
| 362 |
+
"num_attention_heads": 32,
|
| 363 |
+
"num_beam_groups": 1,
|
| 364 |
+
"num_beams": 1,
|
| 365 |
+
"num_hidden_layers": 32,
|
| 366 |
+
"num_key_value_heads": 32,
|
| 367 |
+
"num_return_sequences": 1,
|
| 368 |
+
"output_attentions": false,
|
| 369 |
+
"output_hidden_states": false,
|
| 370 |
+
"output_scores": false,
|
| 371 |
+
"pad_token_id": 0,
|
| 372 |
+
"prefix": null,
|
| 373 |
+
"pretraining_tp": 1,
|
| 374 |
+
"problem_type": null,
|
| 375 |
+
"pruned_heads": {},
|
| 376 |
+
"remove_invalid_values": false,
|
| 377 |
+
"repetition_penalty": 1.0,
|
| 378 |
+
"return_dict": true,
|
| 379 |
+
"return_dict_in_generate": false,
|
| 380 |
+
"rms_norm_eps": 1e-05,
|
| 381 |
+
"rope_scaling": null,
|
| 382 |
+
"rope_theta": 10000.0,
|
| 383 |
+
"sep_token_id": null,
|
| 384 |
+
"suppress_tokens": null,
|
| 385 |
+
"task_specific_params": null,
|
| 386 |
+
"temperature": 1.0,
|
| 387 |
+
"tf_legacy_loss": false,
|
| 388 |
+
"tie_encoder_decoder": false,
|
| 389 |
+
"tie_word_embeddings": false,
|
| 390 |
+
"tokenizer_class": null,
|
| 391 |
+
"top_k": 50,
|
| 392 |
+
"top_p": 1.0,
|
| 393 |
+
"torch_dtype": "bfloat16",
|
| 394 |
+
"torchscript": false,
|
| 395 |
+
"transformers_version": "4.49.0",
|
| 396 |
+
"typical_p": 1.0,
|
| 397 |
+
"use_bfloat16": false,
|
| 398 |
+
"use_cache": true,
|
| 399 |
+
"vocab_size": 32000
|
| 400 |
+
},
|
| 401 |
+
"language_generation_config": {
|
| 402 |
+
"_from_model_config": false,
|
| 403 |
+
"assistant_confidence_threshold": 0.4,
|
| 404 |
+
"assistant_early_exit": null,
|
| 405 |
+
"assistant_lookbehind": 10,
|
| 406 |
+
"bad_words_ids": null,
|
| 407 |
+
"begin_suppress_tokens": null,
|
| 408 |
+
"bos_token_id": 1,
|
| 409 |
+
"cache_config": null,
|
| 410 |
+
"cache_implementation": null,
|
| 411 |
+
"constraints": null,
|
| 412 |
+
"decoder_start_token_id": null,
|
| 413 |
+
"disable_compile": false,
|
| 414 |
+
"diversity_penalty": 0.0,
|
| 415 |
+
"do_sample": false,
|
| 416 |
+
"dola_layers": null,
|
| 417 |
+
"early_stopping": false,
|
| 418 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 419 |
+
"encoder_repetition_penalty": 1.0,
|
| 420 |
+
"eos_token_id": 2,
|
| 421 |
+
"epsilon_cutoff": 0.0,
|
| 422 |
+
"eta_cutoff": 0.0,
|
| 423 |
+
"exponential_decay_length_penalty": null,
|
| 424 |
+
"force_words_ids": null,
|
| 425 |
+
"forced_bos_token_id": null,
|
| 426 |
+
"forced_decoder_ids": null,
|
| 427 |
+
"forced_eos_token_id": null,
|
| 428 |
+
"generation_kwargs": {},
|
| 429 |
+
"guidance_scale": null,
|
| 430 |
+
"is_assistant": false,
|
| 431 |
+
"length_penalty": 1.0,
|
| 432 |
+
"low_memory": null,
|
| 433 |
+
"max_length": 4096,
|
| 434 |
+
"max_matching_ngram_size": null,
|
| 435 |
+
"max_new_tokens": null,
|
| 436 |
+
"max_time": null,
|
| 437 |
+
"min_length": 0,
|
| 438 |
+
"min_new_tokens": null,
|
| 439 |
+
"min_p": null,
|
| 440 |
+
"no_repeat_ngram_size": 0,
|
| 441 |
+
"num_assistant_tokens": 20,
|
| 442 |
+
"num_assistant_tokens_schedule": "constant",
|
| 443 |
+
"num_beam_groups": 1,
|
| 444 |
+
"num_beams": 1,
|
| 445 |
+
"num_return_sequences": 1,
|
| 446 |
+
"output_attentions": false,
|
| 447 |
+
"output_hidden_states": false,
|
| 448 |
+
"output_logits": null,
|
| 449 |
+
"output_scores": false,
|
| 450 |
+
"pad_token_id": 0,
|
| 451 |
+
"penalty_alpha": null,
|
| 452 |
+
"prompt_lookup_num_tokens": null,
|
| 453 |
+
"remove_invalid_values": false,
|
| 454 |
+
"renormalize_logits": false,
|
| 455 |
+
"repetition_penalty": 1.0,
|
| 456 |
+
"return_dict_in_generate": false,
|
| 457 |
+
"return_legacy_cache": null,
|
| 458 |
+
"sequence_bias": null,
|
| 459 |
+
"stop_strings": null,
|
| 460 |
+
"suppress_tokens": null,
|
| 461 |
+
"target_lookbehind": 10,
|
| 462 |
+
"temperature": 0.9,
|
| 463 |
+
"token_healing": false,
|
| 464 |
+
"top_k": 50,
|
| 465 |
+
"top_p": 0.6,
|
| 466 |
+
"transformers_version": "4.49.0",
|
| 467 |
+
"typical_p": 1.0,
|
| 468 |
+
"use_cache": true,
|
| 469 |
+
"watermarking_config": null
|
| 470 |
+
},
|
| 471 |
+
"lora_config": {
|
| 472 |
+
"alpha": 32,
|
| 473 |
+
"dropout": 0.05,
|
| 474 |
+
"rank": 16,
|
| 475 |
+
"target_modules": [
|
| 476 |
+
"k_proj",
|
| 477 |
+
"o_proj",
|
| 478 |
+
"q_proj",
|
| 479 |
+
"v_proj"
|
| 480 |
+
]
|
| 481 |
+
},
|
| 482 |
+
"model_type": "falcon_x",
|
| 483 |
+
"runtime_config": {
|
| 484 |
+
"detector": {
|
| 485 |
+
"mask_threshold": 0.5,
|
| 486 |
+
"max_regions": 100,
|
| 487 |
+
"nms_threshold": 0.6,
|
| 488 |
+
"resolution": 768,
|
| 489 |
+
"score_threshold": 0.15,
|
| 490 |
+
"variant": "seg-2xlarge"
|
| 491 |
+
},
|
| 492 |
+
"experiment": {
|
| 493 |
+
"backbone_layout": "independent",
|
| 494 |
+
"name": "falcon-x",
|
| 495 |
+
"protocol": "dataset-v1",
|
| 496 |
+
"ssa_ablation": "none",
|
| 497 |
+
"world_size": null
|
| 498 |
+
},
|
| 499 |
+
"model": {
|
| 500 |
+
"component_order": [
|
| 501 |
+
"detonator",
|
| 502 |
+
"explosive",
|
| 503 |
+
"battery"
|
| 504 |
+
],
|
| 505 |
+
"image_size": 448,
|
| 506 |
+
"language_model": "lmsys/vicuna-7b-v1.5",
|
| 507 |
+
"link_order": [
|
| 508 |
+
[
|
| 509 |
+
"battery",
|
| 510 |
+
"detonator"
|
| 511 |
+
],
|
| 512 |
+
[
|
| 513 |
+
"battery",
|
| 514 |
+
"explosive"
|
| 515 |
+
],
|
| 516 |
+
[
|
| 517 |
+
"detonator",
|
| 518 |
+
"explosive"
|
| 519 |
+
]
|
| 520 |
+
],
|
| 521 |
+
"lora_alpha": 32,
|
| 522 |
+
"lora_dropout": 0.05,
|
| 523 |
+
"lora_rank": 16,
|
| 524 |
+
"max_regions": 100,
|
| 525 |
+
"patch_size": 14,
|
| 526 |
+
"region_dim": 1024,
|
| 527 |
+
"roi_size": 4,
|
| 528 |
+
"vision_model": "facebook/dinov2-large"
|
| 529 |
+
},
|
| 530 |
+
"stages": {
|
| 531 |
+
"stage1": {
|
| 532 |
+
"batch_size": 1,
|
| 533 |
+
"encoder_learning_rate": 0.00015,
|
| 534 |
+
"epochs": 12,
|
| 535 |
+
"expanded_scales": false,
|
| 536 |
+
"gradient_accumulation": 16,
|
| 537 |
+
"learning_rate": 0.0001,
|
| 538 |
+
"lr_scheduler": "cosine",
|
| 539 |
+
"multi_scale": false,
|
| 540 |
+
"precision": "backend_mixed",
|
| 541 |
+
"tf32": true,
|
| 542 |
+
"warmup_epochs": 0.0,
|
| 543 |
+
"weight_decay": 0.0001
|
| 544 |
+
},
|
| 545 |
+
"stage2": {
|
| 546 |
+
"batch_size": 4,
|
| 547 |
+
"epoch_size": null,
|
| 548 |
+
"epochs": 1,
|
| 549 |
+
"gradient_accumulation": 4,
|
| 550 |
+
"learning_rate": 0.0001,
|
| 551 |
+
"link_loss_weight": 0.5,
|
| 552 |
+
"max_text_tokens": 256,
|
| 553 |
+
"precision": "bf16",
|
| 554 |
+
"presence_loss_weight": 0.5,
|
| 555 |
+
"risk_loss_weight": 1.0,
|
| 556 |
+
"sampling": "all",
|
| 557 |
+
"seed": 42,
|
| 558 |
+
"text_overflow_policy": "error",
|
| 559 |
+
"tf32": true,
|
| 560 |
+
"warmup_ratio": 0.03,
|
| 561 |
+
"weight_decay": 0.0
|
| 562 |
+
},
|
| 563 |
+
"stage3": {
|
| 564 |
+
"batch_size": 1,
|
| 565 |
+
"epoch_size": null,
|
| 566 |
+
"epochs": 1,
|
| 567 |
+
"gradient_accumulation": 16,
|
| 568 |
+
"learning_rate": 1e-05,
|
| 569 |
+
"link_loss_weight": 0.5,
|
| 570 |
+
"max_text_tokens": 256,
|
| 571 |
+
"precision": "bf16",
|
| 572 |
+
"presence_loss_weight": 0.5,
|
| 573 |
+
"risk_loss_weight": 1.0,
|
| 574 |
+
"sampling": "all",
|
| 575 |
+
"seed": 42,
|
| 576 |
+
"text_overflow_policy": "error",
|
| 577 |
+
"tf32": true,
|
| 578 |
+
"warmup_ratio": 0.03,
|
| 579 |
+
"weight_decay": 0.0
|
| 580 |
+
}
|
| 581 |
+
},
|
| 582 |
+
"training": {
|
| 583 |
+
"batch_size": 1,
|
| 584 |
+
"epoch_size": null,
|
| 585 |
+
"epochs": 1,
|
| 586 |
+
"gradient_accumulation": 16,
|
| 587 |
+
"learning_rate": 0.0001,
|
| 588 |
+
"link_loss_weight": 0.5,
|
| 589 |
+
"max_text_tokens": 256,
|
| 590 |
+
"precision": "bf16",
|
| 591 |
+
"presence_loss_weight": 0.5,
|
| 592 |
+
"risk_loss_weight": 1.0,
|
| 593 |
+
"sampling": "all",
|
| 594 |
+
"seed": 42,
|
| 595 |
+
"text_overflow_policy": "error",
|
| 596 |
+
"tf32": true,
|
| 597 |
+
"warmup_ratio": 0.03,
|
| 598 |
+
"weight_decay": 0.0
|
| 599 |
+
}
|
| 600 |
+
},
|
| 601 |
+
"tie_word_embeddings": false,
|
| 602 |
+
"torch_dtype": "float32",
|
| 603 |
+
"transformers_version": "4.49.0",
|
| 604 |
+
"vision_config": {
|
| 605 |
+
"_attn_implementation_autoset": true,
|
| 606 |
+
"add_cross_attention": false,
|
| 607 |
+
"apply_layernorm": true,
|
| 608 |
+
"architectures": [
|
| 609 |
+
"Dinov2Model"
|
| 610 |
+
],
|
| 611 |
+
"attention_probs_dropout_prob": 0.0,
|
| 612 |
+
"attn_implementation": "sdpa",
|
| 613 |
+
"bad_words_ids": null,
|
| 614 |
+
"begin_suppress_tokens": null,
|
| 615 |
+
"bos_token_id": null,
|
| 616 |
+
"chunk_size_feed_forward": 0,
|
| 617 |
+
"cross_attention_hidden_size": null,
|
| 618 |
+
"decoder_start_token_id": null,
|
| 619 |
+
"diversity_penalty": 0.0,
|
| 620 |
+
"do_sample": false,
|
| 621 |
+
"drop_path_rate": 0.0,
|
| 622 |
+
"early_stopping": false,
|
| 623 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 624 |
+
"eos_token_id": null,
|
| 625 |
+
"exponential_decay_length_penalty": null,
|
| 626 |
+
"finetuning_task": null,
|
| 627 |
+
"forced_bos_token_id": null,
|
| 628 |
+
"forced_eos_token_id": null,
|
| 629 |
+
"hidden_act": "gelu",
|
| 630 |
+
"hidden_dropout_prob": 0.0,
|
| 631 |
+
"hidden_size": 1024,
|
| 632 |
+
"id2label": {
|
| 633 |
+
"0": "LABEL_0",
|
| 634 |
+
"1": "LABEL_1"
|
| 635 |
+
},
|
| 636 |
+
"image_size": 518,
|
| 637 |
+
"initializer_range": 0.02,
|
| 638 |
+
"is_decoder": false,
|
| 639 |
+
"is_encoder_decoder": false,
|
| 640 |
+
"label2id": {
|
| 641 |
+
"LABEL_0": 0,
|
| 642 |
+
"LABEL_1": 1
|
| 643 |
+
},
|
| 644 |
+
"layer_norm_eps": 1e-06,
|
| 645 |
+
"layerscale_value": 1.0,
|
| 646 |
+
"length_penalty": 1.0,
|
| 647 |
+
"max_length": 20,
|
| 648 |
+
"min_length": 0,
|
| 649 |
+
"mlp_ratio": 4,
|
| 650 |
+
"model_type": "dinov2",
|
| 651 |
+
"no_repeat_ngram_size": 0,
|
| 652 |
+
"num_attention_heads": 16,
|
| 653 |
+
"num_beam_groups": 1,
|
| 654 |
+
"num_beams": 1,
|
| 655 |
+
"num_channels": 3,
|
| 656 |
+
"num_hidden_layers": 24,
|
| 657 |
+
"num_return_sequences": 1,
|
| 658 |
+
"out_features": [
|
| 659 |
+
"stage24"
|
| 660 |
+
],
|
| 661 |
+
"out_indices": [
|
| 662 |
+
24
|
| 663 |
+
],
|
| 664 |
+
"output_attentions": false,
|
| 665 |
+
"output_hidden_states": false,
|
| 666 |
+
"output_scores": false,
|
| 667 |
+
"pad_token_id": null,
|
| 668 |
+
"patch_size": 14,
|
| 669 |
+
"prefix": null,
|
| 670 |
+
"problem_type": null,
|
| 671 |
+
"pruned_heads": {},
|
| 672 |
+
"qkv_bias": true,
|
| 673 |
+
"remove_invalid_values": false,
|
| 674 |
+
"repetition_penalty": 1.0,
|
| 675 |
+
"reshape_hidden_states": true,
|
| 676 |
+
"return_dict": true,
|
| 677 |
+
"return_dict_in_generate": false,
|
| 678 |
+
"sep_token_id": null,
|
| 679 |
+
"stage_names": [
|
| 680 |
+
"stem",
|
| 681 |
+
"stage1",
|
| 682 |
+
"stage2",
|
| 683 |
+
"stage3",
|
| 684 |
+
"stage4",
|
| 685 |
+
"stage5",
|
| 686 |
+
"stage6",
|
| 687 |
+
"stage7",
|
| 688 |
+
"stage8",
|
| 689 |
+
"stage9",
|
| 690 |
+
"stage10",
|
| 691 |
+
"stage11",
|
| 692 |
+
"stage12",
|
| 693 |
+
"stage13",
|
| 694 |
+
"stage14",
|
| 695 |
+
"stage15",
|
| 696 |
+
"stage16",
|
| 697 |
+
"stage17",
|
| 698 |
+
"stage18",
|
| 699 |
+
"stage19",
|
| 700 |
+
"stage20",
|
| 701 |
+
"stage21",
|
| 702 |
+
"stage22",
|
| 703 |
+
"stage23",
|
| 704 |
+
"stage24"
|
| 705 |
+
],
|
| 706 |
+
"suppress_tokens": null,
|
| 707 |
+
"task_specific_params": null,
|
| 708 |
+
"temperature": 1.0,
|
| 709 |
+
"tf_legacy_loss": false,
|
| 710 |
+
"tie_encoder_decoder": false,
|
| 711 |
+
"tie_word_embeddings": true,
|
| 712 |
+
"tokenizer_class": null,
|
| 713 |
+
"top_k": 50,
|
| 714 |
+
"top_p": 1.0,
|
| 715 |
+
"torch_dtype": "float32",
|
| 716 |
+
"torchscript": false,
|
| 717 |
+
"transformers_version": "4.49.0",
|
| 718 |
+
"typical_p": 1.0,
|
| 719 |
+
"use_bfloat16": false,
|
| 720 |
+
"use_mask_token": true,
|
| 721 |
+
"use_swiglu_ffn": false
|
| 722 |
+
}
|
| 723 |
+
}
|
config.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Mapping
|
| 4 |
+
from copy import deepcopy
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
from .capabilities import SSA_ABLATIONS
|
| 11 |
+
|
| 12 |
+
COMPONENT_ORDER = ("detonator", "explosive", "battery")
|
| 13 |
+
LINK_ORDER = (
|
| 14 |
+
("battery", "detonator"),
|
| 15 |
+
("battery", "explosive"),
|
| 16 |
+
("detonator", "explosive"),
|
| 17 |
+
)
|
| 18 |
+
DETECTOR_VARIANTS = {
|
| 19 |
+
"seg-nano",
|
| 20 |
+
"seg-small",
|
| 21 |
+
"seg-medium",
|
| 22 |
+
"seg-large",
|
| 23 |
+
"seg-xlarge",
|
| 24 |
+
"seg-2xlarge",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
DEFAULT_CONFIG: dict[str, Any] = {
|
| 29 |
+
"experiment": {
|
| 30 |
+
"name": "falcon-x",
|
| 31 |
+
"protocol": "dataset-v1",
|
| 32 |
+
"backbone_layout": "independent",
|
| 33 |
+
"world_size": None,
|
| 34 |
+
"ssa_ablation": "none",
|
| 35 |
+
},
|
| 36 |
+
"model": {
|
| 37 |
+
"vision_model": "facebook/dinov2-large",
|
| 38 |
+
"language_model": "lmsys/vicuna-7b-v1.5",
|
| 39 |
+
"image_size": 448,
|
| 40 |
+
"patch_size": 14,
|
| 41 |
+
"region_dim": 1024,
|
| 42 |
+
"roi_size": 4,
|
| 43 |
+
"max_regions": 100,
|
| 44 |
+
"component_order": list(COMPONENT_ORDER),
|
| 45 |
+
"link_order": [list(pair) for pair in LINK_ORDER],
|
| 46 |
+
"lora_rank": 16,
|
| 47 |
+
"lora_alpha": 32,
|
| 48 |
+
"lora_dropout": 0.05,
|
| 49 |
+
},
|
| 50 |
+
"detector": {
|
| 51 |
+
"variant": "seg-2xlarge",
|
| 52 |
+
"resolution": None,
|
| 53 |
+
"score_threshold": 0.15,
|
| 54 |
+
"nms_threshold": 0.6,
|
| 55 |
+
"mask_threshold": 0.5,
|
| 56 |
+
"max_regions": 100,
|
| 57 |
+
},
|
| 58 |
+
"training": {
|
| 59 |
+
"epochs": 1,
|
| 60 |
+
"batch_size": 1,
|
| 61 |
+
"gradient_accumulation": 16,
|
| 62 |
+
"learning_rate": 1e-4,
|
| 63 |
+
"weight_decay": 0.0,
|
| 64 |
+
"warmup_ratio": 0.03,
|
| 65 |
+
"max_text_tokens": 256,
|
| 66 |
+
"text_overflow_policy": "error",
|
| 67 |
+
"risk_loss_weight": 1.0,
|
| 68 |
+
"presence_loss_weight": 0.5,
|
| 69 |
+
"link_loss_weight": 0.5,
|
| 70 |
+
"seed": 42,
|
| 71 |
+
"precision": "bf16",
|
| 72 |
+
"tf32": True,
|
| 73 |
+
"sampling": "all",
|
| 74 |
+
"epoch_size": None,
|
| 75 |
+
},
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Per-device batches and gradient accumulation are configured separately.
|
| 79 |
+
DEFAULT_STAGES: dict[str, dict[str, Any]] = {
|
| 80 |
+
"stage1": {
|
| 81 |
+
"epochs": 12,
|
| 82 |
+
"batch_size": 1,
|
| 83 |
+
"gradient_accumulation": 16,
|
| 84 |
+
"learning_rate": 1e-4,
|
| 85 |
+
"encoder_learning_rate": 1.5e-4,
|
| 86 |
+
"weight_decay": 1e-4,
|
| 87 |
+
"lr_scheduler": "cosine",
|
| 88 |
+
"warmup_epochs": 0.0,
|
| 89 |
+
"multi_scale": False,
|
| 90 |
+
"expanded_scales": False,
|
| 91 |
+
"precision": "backend_mixed",
|
| 92 |
+
"tf32": True,
|
| 93 |
+
},
|
| 94 |
+
"stage2": deepcopy(DEFAULT_CONFIG["training"]),
|
| 95 |
+
"stage3": {
|
| 96 |
+
**deepcopy(DEFAULT_CONFIG["training"]),
|
| 97 |
+
"learning_rate": 1e-5,
|
| 98 |
+
"precision": "bf16",
|
| 99 |
+
},
|
| 100 |
+
}
|
| 101 |
+
DEFAULT_CONFIG["stages"] = deepcopy(DEFAULT_STAGES)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _merge(
|
| 105 |
+
base: dict[str, Any], update: dict[str, Any], prefix: str = ""
|
| 106 |
+
) -> dict[str, Any]:
|
| 107 |
+
if not isinstance(update, dict):
|
| 108 |
+
raise ValueError(f"{prefix or 'Configuration'} must be a mapping")
|
| 109 |
+
for key, value in update.items():
|
| 110 |
+
name = f"{prefix}.{key}" if prefix else key
|
| 111 |
+
if key not in base:
|
| 112 |
+
raise ValueError(f"Unknown configuration key: {name}")
|
| 113 |
+
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
| 114 |
+
_merge(base[key], value, name)
|
| 115 |
+
else:
|
| 116 |
+
base[key] = value
|
| 117 |
+
return base
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _positive_integer(value: Any, name: str) -> None:
|
| 121 |
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
| 122 |
+
raise ValueError(f"{name} must be a positive integer")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _finite_number(value: Any, name: str, *, minimum: float = 0.0) -> float:
|
| 126 |
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
| 127 |
+
raise ValueError(f"{name} must be a number")
|
| 128 |
+
number = float(value)
|
| 129 |
+
if not minimum <= number < float("inf"):
|
| 130 |
+
raise ValueError(f"{name} must be at least {minimum}")
|
| 131 |
+
return number
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def validate_config(config: dict[str, Any]) -> None:
|
| 135 |
+
"""Reject architecture drift and common experiment-configuration mistakes."""
|
| 136 |
+
|
| 137 |
+
for section in ("model", "detector", "training"):
|
| 138 |
+
if not isinstance(config.get(section), dict):
|
| 139 |
+
raise ValueError(f"{section} must be a mapping")
|
| 140 |
+
model = config["model"]
|
| 141 |
+
detector = config["detector"]
|
| 142 |
+
experiment = config.get("experiment", DEFAULT_CONFIG["experiment"])
|
| 143 |
+
if not isinstance(experiment, dict):
|
| 144 |
+
raise ValueError("experiment must be a mapping")
|
| 145 |
+
if not isinstance(experiment["name"], str) or not experiment["name"].strip():
|
| 146 |
+
raise ValueError("experiment.name must be a non-empty string")
|
| 147 |
+
if experiment["protocol"] not in ("dataset-v1", "paper-v2"):
|
| 148 |
+
raise ValueError("experiment.protocol must be dataset-v1 or paper-v2")
|
| 149 |
+
if experiment["backbone_layout"] != "independent":
|
| 150 |
+
raise ValueError("Only the independent-backbone implementation is available")
|
| 151 |
+
if experiment.get("ssa_ablation", "none") not in SSA_ABLATIONS:
|
| 152 |
+
raise ValueError(f"experiment.ssa_ablation must be one of {SSA_ABLATIONS}")
|
| 153 |
+
if experiment["world_size"] is not None:
|
| 154 |
+
_positive_integer(experiment["world_size"], "experiment.world_size")
|
| 155 |
+
|
| 156 |
+
for key in ("vision_model", "language_model"):
|
| 157 |
+
if not isinstance(model[key], str) or not model[key].strip():
|
| 158 |
+
raise ValueError(
|
| 159 |
+
f"model.{key} must be a non-empty path or model identifier"
|
| 160 |
+
)
|
| 161 |
+
for key in ("image_size", "patch_size", "region_dim", "roi_size", "max_regions"):
|
| 162 |
+
_positive_integer(model[key], f"model.{key}")
|
| 163 |
+
if model["image_size"] % (2 * model["patch_size"]):
|
| 164 |
+
raise ValueError("model.image_size must be divisible by twice model.patch_size")
|
| 165 |
+
if tuple(model["component_order"]) != COMPONENT_ORDER:
|
| 166 |
+
raise ValueError(f"model.component_order must be {list(COMPONENT_ORDER)!r}")
|
| 167 |
+
if tuple(tuple(pair) for pair in model["link_order"]) != LINK_ORDER:
|
| 168 |
+
raise ValueError(
|
| 169 |
+
f"model.link_order must be {[list(pair) for pair in LINK_ORDER]!r}"
|
| 170 |
+
)
|
| 171 |
+
_positive_integer(model["lora_rank"], "model.lora_rank")
|
| 172 |
+
_positive_integer(model["lora_alpha"], "model.lora_alpha")
|
| 173 |
+
dropout = _finite_number(model["lora_dropout"], "model.lora_dropout")
|
| 174 |
+
if dropout >= 1.0:
|
| 175 |
+
raise ValueError("model.lora_dropout must be smaller than 1")
|
| 176 |
+
|
| 177 |
+
if detector["variant"] not in DETECTOR_VARIANTS:
|
| 178 |
+
raise ValueError(
|
| 179 |
+
f"detector.variant must be one of {sorted(DETECTOR_VARIANTS)!r}"
|
| 180 |
+
)
|
| 181 |
+
for key in ("score_threshold", "nms_threshold", "mask_threshold"):
|
| 182 |
+
if key not in detector:
|
| 183 |
+
continue
|
| 184 |
+
value = _finite_number(detector[key], f"detector.{key}")
|
| 185 |
+
if value > 1.0:
|
| 186 |
+
raise ValueError(f"detector.{key} must not exceed 1")
|
| 187 |
+
_positive_integer(detector["max_regions"], "detector.max_regions")
|
| 188 |
+
if detector["max_regions"] > model["max_regions"]:
|
| 189 |
+
raise ValueError("detector.max_regions must not exceed model.max_regions")
|
| 190 |
+
|
| 191 |
+
resolution = detector.get("resolution")
|
| 192 |
+
if resolution is not None:
|
| 193 |
+
_positive_integer(resolution, "detector.resolution")
|
| 194 |
+
# Pinned segmentation variants use patch size 12. Nano uses one local
|
| 195 |
+
# window; the other supported variants use two. No model load is needed.
|
| 196 |
+
divisor = 12 if detector["variant"] == "seg-nano" else 24
|
| 197 |
+
if resolution % divisor:
|
| 198 |
+
raise ValueError(
|
| 199 |
+
f"detector.resolution must be divisible by {divisor} for pinned RF-DETR"
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
_validate_training(config["training"], "training")
|
| 203 |
+
stages = config.get("stages", {})
|
| 204 |
+
if not isinstance(stages, dict):
|
| 205 |
+
raise ValueError("stages must be a mapping")
|
| 206 |
+
for stage, values in stages.items():
|
| 207 |
+
if stage not in DEFAULT_STAGES:
|
| 208 |
+
raise ValueError(f"Unknown training stage: {stage}")
|
| 209 |
+
if not isinstance(values, dict):
|
| 210 |
+
raise ValueError(f"stages.{stage} must be a mapping")
|
| 211 |
+
if stage == "stage1":
|
| 212 |
+
for key in ("epochs", "batch_size", "gradient_accumulation"):
|
| 213 |
+
_positive_integer(values[key], f"stages.stage1.{key}")
|
| 214 |
+
for key in (
|
| 215 |
+
"learning_rate",
|
| 216 |
+
"encoder_learning_rate",
|
| 217 |
+
"weight_decay",
|
| 218 |
+
"warmup_epochs",
|
| 219 |
+
):
|
| 220 |
+
_finite_number(values[key], f"stages.stage1.{key}")
|
| 221 |
+
if values["learning_rate"] == 0 or values["encoder_learning_rate"] == 0:
|
| 222 |
+
raise ValueError("Stage 1 learning rates must be positive")
|
| 223 |
+
if values["lr_scheduler"] not in ("cosine", "step"):
|
| 224 |
+
raise ValueError("stages.stage1.lr_scheduler must be cosine or step")
|
| 225 |
+
for key in ("multi_scale", "expanded_scales"):
|
| 226 |
+
if not isinstance(values[key], bool):
|
| 227 |
+
raise ValueError(f"stages.stage1.{key} must be boolean")
|
| 228 |
+
if values.get("precision") not in ("backend_mixed", "fp32"):
|
| 229 |
+
raise ValueError(
|
| 230 |
+
"stages.stage1.precision must be backend_mixed or fp32"
|
| 231 |
+
)
|
| 232 |
+
if not isinstance(values.get("tf32", True), bool):
|
| 233 |
+
raise ValueError("stages.stage1.tf32 must be boolean")
|
| 234 |
+
else:
|
| 235 |
+
_validate_training(values, f"stages.{stage}")
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _validate_precision(values: dict[str, Any], prefix: str) -> None:
|
| 239 |
+
if values.get("precision", "bf16") not in ("fp16", "bf16"):
|
| 240 |
+
raise ValueError(f"{prefix}.precision must be fp16 or bf16")
|
| 241 |
+
if not isinstance(values.get("tf32", True), bool):
|
| 242 |
+
raise ValueError(f"{prefix}.tf32 must be boolean")
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _validate_training(training: dict[str, Any], prefix: str) -> None:
|
| 246 |
+
for key in ("epochs", "batch_size", "gradient_accumulation", "max_text_tokens"):
|
| 247 |
+
_positive_integer(training[key], f"{prefix}.{key}")
|
| 248 |
+
if training["max_text_tokens"] < 3:
|
| 249 |
+
raise ValueError("training.max_text_tokens must be at least 3")
|
| 250 |
+
for key in (
|
| 251 |
+
"learning_rate",
|
| 252 |
+
"weight_decay",
|
| 253 |
+
"risk_loss_weight",
|
| 254 |
+
"presence_loss_weight",
|
| 255 |
+
"link_loss_weight",
|
| 256 |
+
):
|
| 257 |
+
_finite_number(training[key], f"{prefix}.{key}")
|
| 258 |
+
if training["learning_rate"] == 0:
|
| 259 |
+
raise ValueError("training.learning_rate must be greater than 0")
|
| 260 |
+
warmup = _finite_number(training["warmup_ratio"], "training.warmup_ratio")
|
| 261 |
+
if warmup >= 1.0:
|
| 262 |
+
raise ValueError("training.warmup_ratio must be smaller than 1")
|
| 263 |
+
if isinstance(training["seed"], bool) or not isinstance(training["seed"], int):
|
| 264 |
+
raise ValueError("training.seed must be an integer")
|
| 265 |
+
_validate_precision(training, prefix)
|
| 266 |
+
if training.get("text_overflow_policy", "error") not in ("error", "truncate"):
|
| 267 |
+
raise ValueError(f"{prefix}.text_overflow_policy must be error or truncate")
|
| 268 |
+
if training.get("sampling", "all") not in ("all", "family_balanced"):
|
| 269 |
+
raise ValueError(f"{prefix}.sampling must be all or family_balanced")
|
| 270 |
+
if training.get("epoch_size") is not None:
|
| 271 |
+
_positive_integer(training["epoch_size"], f"{prefix}.epoch_size")
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def load_config(path: str | Path | None = None) -> dict[str, Any]:
|
| 275 |
+
config = deepcopy(DEFAULT_CONFIG)
|
| 276 |
+
payload: dict[str, Any] = {}
|
| 277 |
+
if path is not None:
|
| 278 |
+
with Path(path).expanduser().open(encoding="utf-8") as handle:
|
| 279 |
+
payload = yaml.safe_load(handle)
|
| 280 |
+
if payload is None:
|
| 281 |
+
payload = {}
|
| 282 |
+
if not isinstance(payload, dict):
|
| 283 |
+
raise ValueError("Configuration root must be a mapping")
|
| 284 |
+
_merge(config, payload)
|
| 285 |
+
# Explicit legacy/common training options still apply to both multimodal
|
| 286 |
+
# stages. Explicit per-stage overrides win. Defaults, however, differ between
|
| 287 |
+
# Stage 2 and Stage 3; a shared default LR must not erase that distinction.
|
| 288 |
+
common = payload.get("training", {})
|
| 289 |
+
stages = payload.get("stages", {})
|
| 290 |
+
if not isinstance(common, dict) or not isinstance(stages, dict):
|
| 291 |
+
raise ValueError("training and stages must be mappings")
|
| 292 |
+
for key in ("stage2", "stage3"):
|
| 293 |
+
merged = _merge(deepcopy(DEFAULT_STAGES[key]), common, "training")
|
| 294 |
+
config["stages"][key] = _merge(merged, stages.get(key, {}), f"stages.{key}")
|
| 295 |
+
validate_config(config)
|
| 296 |
+
return config
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def resolve_stage_config(config: dict[str, Any], stage: int) -> dict[str, Any]:
|
| 300 |
+
"""Return a detached, fully resolved stage configuration.
|
| 301 |
+
|
| 302 |
+
The fallback preserves callers supplying the original configuration shape.
|
| 303 |
+
Stage-specific configuration takes precedence in newly loaded files.
|
| 304 |
+
"""
|
| 305 |
+
if stage not in (1, 2, 3):
|
| 306 |
+
raise ValueError("Training stage must be 1, 2, or 3")
|
| 307 |
+
key = f"stage{stage}"
|
| 308 |
+
if key in config.get("stages", {}):
|
| 309 |
+
return deepcopy(config["stages"][key])
|
| 310 |
+
if stage == 1:
|
| 311 |
+
return deepcopy(DEFAULT_STAGES[key])
|
| 312 |
+
return deepcopy(config["training"])
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def apply_stage_overrides(
|
| 316 |
+
config: Mapping[str, Any],
|
| 317 |
+
stage: int,
|
| 318 |
+
overrides: Mapping[str, Any],
|
| 319 |
+
) -> dict[str, Any]:
|
| 320 |
+
"""Apply stage overrides to both runtime and saved checkpoint configuration."""
|
| 321 |
+
|
| 322 |
+
if not isinstance(config, Mapping):
|
| 323 |
+
raise TypeError("config must be a mapping")
|
| 324 |
+
if stage not in (1, 2, 3):
|
| 325 |
+
raise ValueError("Training stage must be 1, 2, or 3")
|
| 326 |
+
if not isinstance(overrides, Mapping):
|
| 327 |
+
raise TypeError("stage overrides must be a mapping")
|
| 328 |
+
|
| 329 |
+
effective = deepcopy(dict(config))
|
| 330 |
+
validate_config(effective)
|
| 331 |
+
resolved = resolve_stage_config(effective, stage)
|
| 332 |
+
unknown = sorted(set(overrides).difference(resolved))
|
| 333 |
+
if unknown:
|
| 334 |
+
prefix = f"stages.stage{stage}"
|
| 335 |
+
raise ValueError(
|
| 336 |
+
"Unknown stage override"
|
| 337 |
+
+ ("s" if len(unknown) != 1 else "")
|
| 338 |
+
+ ": "
|
| 339 |
+
+ ", ".join(f"{prefix}.{name}" for name in unknown)
|
| 340 |
+
)
|
| 341 |
+
resolved.update(deepcopy(dict(overrides)))
|
| 342 |
+
effective.setdefault("stages", {})[f"stage{stage}"] = resolved
|
| 343 |
+
validate_config(effective)
|
| 344 |
+
return effective
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def paper_reproduction_issues(config: dict[str, Any]) -> list[str]:
|
| 348 |
+
"""Report known eligibility blockers, not a claim of historical reproduction."""
|
| 349 |
+
issues = [
|
| 350 |
+
"Binary presence supervision uses BCE-with-logits instead of the paper's L1 loss."
|
| 351 |
+
]
|
| 352 |
+
if (
|
| 353 |
+
config.get("experiment", {}).get("backbone_layout", "independent")
|
| 354 |
+
== "independent"
|
| 355 |
+
):
|
| 356 |
+
issues.append(
|
| 357 |
+
"The shared-feature detector described in the paper is not implemented; "
|
| 358 |
+
"the independent RF-DETR/DINO topology is a documented deviation."
|
| 359 |
+
)
|
| 360 |
+
if config["detector"].get("resolution") != 448:
|
| 361 |
+
issues.append(
|
| 362 |
+
"The detector does not use the paper's stated 448-pixel resolution; "
|
| 363 |
+
"448 is incompatible with the pinned segmentation backend."
|
| 364 |
+
)
|
| 365 |
+
return issues
|
configuration_falcon.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the complete Hugging Face FALCON model."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from transformers import PretrainedConfig
|
| 9 |
+
|
| 10 |
+
from .capabilities import (
|
| 11 |
+
SafetyCapabilities,
|
| 12 |
+
apply_ablation,
|
| 13 |
+
capabilities_from_coverage,
|
| 14 |
+
)
|
| 15 |
+
from .config import resolve_stage_config, validate_config
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class FalconConfig(PretrainedConfig):
|
| 19 |
+
model_type = "falcon_x"
|
| 20 |
+
is_composition = True
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
core_config: dict[str, Any] | None = None,
|
| 25 |
+
vision_config: dict[str, Any] | None = None,
|
| 26 |
+
language_config: dict[str, Any] | None = None,
|
| 27 |
+
detector_config: dict[str, Any] | None = None,
|
| 28 |
+
image_processor_config: dict[str, Any] | None = None,
|
| 29 |
+
lora_config: dict[str, Any] | None = None,
|
| 30 |
+
runtime_config: dict[str, Any] | None = None,
|
| 31 |
+
checkpoint_metadata: dict[str, Any] | None = None,
|
| 32 |
+
language_generation_config: dict[str, Any] | None = None,
|
| 33 |
+
**kwargs: Any,
|
| 34 |
+
) -> None:
|
| 35 |
+
kwargs.setdefault("tie_word_embeddings", False)
|
| 36 |
+
super().__init__(**kwargs)
|
| 37 |
+
for name, value in (
|
| 38 |
+
("core_config", core_config),
|
| 39 |
+
("vision_config", vision_config),
|
| 40 |
+
("language_config", language_config),
|
| 41 |
+
("detector_config", detector_config),
|
| 42 |
+
("image_processor_config", image_processor_config),
|
| 43 |
+
("lora_config", lora_config),
|
| 44 |
+
("runtime_config", runtime_config),
|
| 45 |
+
("checkpoint_metadata", checkpoint_metadata),
|
| 46 |
+
("language_generation_config", language_generation_config),
|
| 47 |
+
):
|
| 48 |
+
if value is not None and not isinstance(value, dict):
|
| 49 |
+
raise TypeError(f"{name} must be a mapping")
|
| 50 |
+
setattr(self, name, deepcopy(value or {}))
|
| 51 |
+
|
| 52 |
+
def __getattr__(self, name: str) -> Any:
|
| 53 |
+
# Inference consumes these scalar fields on both native and Hub models.
|
| 54 |
+
core = self.__dict__.get("core_config", {})
|
| 55 |
+
if name in core:
|
| 56 |
+
return core[name]
|
| 57 |
+
raise AttributeError(name)
|
| 58 |
+
|
| 59 |
+
def validate(self) -> None:
|
| 60 |
+
"""Reject incomplete, diagnostic, or inconsistent final-model metadata."""
|
| 61 |
+
for name in (
|
| 62 |
+
"core_config", "vision_config", "language_config", "detector_config",
|
| 63 |
+
"image_processor_config", "lora_config", "runtime_config",
|
| 64 |
+
"checkpoint_metadata",
|
| 65 |
+
):
|
| 66 |
+
if not getattr(self, name):
|
| 67 |
+
raise ValueError(f"FALCON package requires {name}")
|
| 68 |
+
metadata = self.checkpoint_metadata
|
| 69 |
+
validate_config(self.runtime_config)
|
| 70 |
+
if metadata.get("stage") != 3 or metadata.get("training_complete") is not True:
|
| 71 |
+
raise ValueError("FALCON packages require a completed Stage 3 checkpoint")
|
| 72 |
+
if metadata.get("official_result_eligible") is False or metadata.get(
|
| 73 |
+
"diagnostic_training_reasons"
|
| 74 |
+
):
|
| 75 |
+
raise ValueError("Diagnostic checkpoints cannot be packaged as final FALCON models")
|
| 76 |
+
if metadata.get("partial_initialization_allowed") or metadata.get(
|
| 77 |
+
"legacy_initialization_allowed"
|
| 78 |
+
) or metadata.get("oracle_detector", "none") != "none":
|
| 79 |
+
raise ValueError("FALCON packages cannot use partial initialization or oracle proposals")
|
| 80 |
+
if metadata.get("ssa_ablation", "none") != "none" or self.runtime_config.get(
|
| 81 |
+
"experiment", {}
|
| 82 |
+
).get("ssa_ablation", "none") != "none":
|
| 83 |
+
raise ValueError("Final FALCON packages cannot use SSA ablations")
|
| 84 |
+
capabilities = SafetyCapabilities.from_dict(metadata.get("safety_capabilities"))
|
| 85 |
+
core_capabilities = SafetyCapabilities.from_dict(
|
| 86 |
+
self.core_config.get("safety_capabilities")
|
| 87 |
+
)
|
| 88 |
+
if core_capabilities != capabilities:
|
| 89 |
+
raise ValueError("Core and checkpoint safety capabilities differ")
|
| 90 |
+
supervision = metadata.get("supervision_capabilities")
|
| 91 |
+
if supervision is not None and apply_ablation(
|
| 92 |
+
SafetyCapabilities.from_dict(supervision), "none"
|
| 93 |
+
) != capabilities:
|
| 94 |
+
raise ValueError("Checkpoint capabilities disagree with supervision")
|
| 95 |
+
require_observed = metadata.get("require_observed_supervision", False)
|
| 96 |
+
if not isinstance(require_observed, bool):
|
| 97 |
+
raise ValueError("require_observed_supervision must be boolean")
|
| 98 |
+
observed = metadata.get("observed_supervision_coverage")
|
| 99 |
+
if require_observed and observed is None:
|
| 100 |
+
raise ValueError("Checkpoint lacks required observed supervision coverage")
|
| 101 |
+
if observed is not None and (
|
| 102 |
+
not isinstance(observed, dict)
|
| 103 |
+
or set(observed) != {"images", "risk", "presence", "links"}
|
| 104 |
+
):
|
| 105 |
+
raise ValueError("Observed coverage requires images, risk, presence, and links")
|
| 106 |
+
if observed is not None and capabilities.restrict(
|
| 107 |
+
capabilities_from_coverage(observed)
|
| 108 |
+
) != capabilities:
|
| 109 |
+
raise ValueError("Checkpoint enables a head without observed supervision")
|
| 110 |
+
if metadata.get("config") != self.runtime_config:
|
| 111 |
+
raise ValueError("Checkpoint and runtime configurations differ")
|
| 112 |
+
runtime_model = self.runtime_config["model"]
|
| 113 |
+
runtime_training = resolve_stage_config(self.runtime_config, 3)
|
| 114 |
+
for key in ("image_size", "patch_size", "region_dim", "roi_size", "max_regions"):
|
| 115 |
+
if self.core_config.get(key) != runtime_model[key]:
|
| 116 |
+
raise ValueError(f"Core {key} differs from the training configuration")
|
| 117 |
+
for key in (
|
| 118 |
+
"max_text_tokens", "text_overflow_policy", "risk_loss_weight",
|
| 119 |
+
"presence_loss_weight", "link_loss_weight",
|
| 120 |
+
):
|
| 121 |
+
if self.core_config.get(key) != runtime_training[key]:
|
| 122 |
+
raise ValueError(f"Core {key} differs from the training configuration")
|
| 123 |
+
for key, runtime_key in (("rank", "lora_rank"), ("alpha", "lora_alpha"),
|
| 124 |
+
("dropout", "lora_dropout")):
|
| 125 |
+
if self.lora_config.get(key) != runtime_model[runtime_key]:
|
| 126 |
+
raise ValueError(f"LoRA {key} differs from the training configuration")
|
| 127 |
+
runtime_detector = self.runtime_config["detector"]
|
| 128 |
+
if self.detector_config.get("variant") != runtime_detector["variant"]:
|
| 129 |
+
raise ValueError("Detector variant differs from the training configuration")
|
| 130 |
+
policy = self.detector_config.get("proposal_policy", {})
|
| 131 |
+
for key in ("score_threshold", "nms_threshold", "max_regions", "mask_threshold"):
|
| 132 |
+
if policy.get(key) != runtime_detector.get(key, 0.5 if key == "mask_threshold" else None):
|
| 133 |
+
raise ValueError(f"Detector {key} differs from the training configuration")
|
| 134 |
+
resolution = runtime_detector.get("resolution")
|
| 135 |
+
if resolution is not None and self.detector_config.get("architecture", {}).get(
|
| 136 |
+
"resolution"
|
| 137 |
+
) != resolution:
|
| 138 |
+
raise ValueError("Detector resolution differs from the training configuration")
|
| 139 |
+
mask_threshold = self.core_config.get("mask_probability_threshold")
|
| 140 |
+
if isinstance(mask_threshold, bool) or not isinstance(mask_threshold, int | float) or not (
|
| 141 |
+
0 <= mask_threshold <= 1
|
| 142 |
+
):
|
| 143 |
+
raise ValueError("Core mask_probability_threshold must lie in [0, 1]")
|
| 144 |
+
for config_name, dimension in (
|
| 145 |
+
("vision_config", "vision_dim"), ("language_config", "language_dim")
|
| 146 |
+
):
|
| 147 |
+
backbone = getattr(self, config_name)
|
| 148 |
+
if not isinstance(backbone.get("model_type"), str):
|
| 149 |
+
raise ValueError(f"{config_name} requires its architecture model_type")
|
| 150 |
+
if backbone.get("hidden_size") != self.core_config.get(dimension):
|
| 151 |
+
raise ValueError(f"{config_name} hidden_size differs from {dimension}")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
FalconConfig.register_for_auto_class("AutoConfig")
|
detector.py
ADDED
|
@@ -0,0 +1,1048 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import fcntl
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
import stat
|
| 9 |
+
import tempfile
|
| 10 |
+
from collections.abc import Iterator, Mapping
|
| 11 |
+
from contextlib import contextmanager
|
| 12 |
+
from dataclasses import asdict, dataclass
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from types import SimpleNamespace
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
from PIL import Image
|
| 20 |
+
from torch.nn import functional as F
|
| 21 |
+
|
| 22 |
+
from .artifacts import atomic_json, external_output
|
| 23 |
+
|
| 24 |
+
VARIANTS = ("seg-nano", "seg-small", "seg-medium", "seg-large", "seg-xlarge", "seg-2xlarge")
|
| 25 |
+
PROPOSAL_POLICY_VERSION = "falcon-proposals/v1"
|
| 26 |
+
# Pinned RF-DETR applies a strict ``scores > threshold`` filter. Passing the
|
| 27 |
+
# user cutoff there would drop equality-boundary proposals before Falcon's
|
| 28 |
+
# documented inclusive policy can inspect them.
|
| 29 |
+
BACKEND_SCORE_THRESHOLD = -float("inf")
|
| 30 |
+
STAGE1_LOCK_NAME = ".stage1.lock"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass(frozen=True)
|
| 34 |
+
class RFDETRVariantSpec:
|
| 35 |
+
"""Architecture defaults from the pinned ``rfdetr==1.5.2`` package."""
|
| 36 |
+
|
| 37 |
+
resolution: int
|
| 38 |
+
patch_size: int
|
| 39 |
+
num_windows: int
|
| 40 |
+
decoder_layers: int
|
| 41 |
+
num_queries: int
|
| 42 |
+
num_select: int
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def input_divisor(self) -> int:
|
| 46 |
+
return self.patch_size * self.num_windows
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
VARIANT_SPECS: dict[str, RFDETRVariantSpec] = {
|
| 50 |
+
"seg-nano": RFDETRVariantSpec(312, 12, 1, 4, 100, 100),
|
| 51 |
+
"seg-small": RFDETRVariantSpec(384, 12, 2, 4, 100, 100),
|
| 52 |
+
"seg-medium": RFDETRVariantSpec(432, 12, 2, 5, 200, 200),
|
| 53 |
+
"seg-large": RFDETRVariantSpec(504, 12, 2, 5, 200, 200),
|
| 54 |
+
"seg-xlarge": RFDETRVariantSpec(624, 12, 2, 6, 300, 300),
|
| 55 |
+
"seg-2xlarge": RFDETRVariantSpec(768, 12, 2, 6, 300, 300),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclass(frozen=True)
|
| 60 |
+
class RFDETRModelConfig:
|
| 61 |
+
"""Falcon-owned RF-DETR model options validated before model construction."""
|
| 62 |
+
|
| 63 |
+
resolution: int | None = None
|
| 64 |
+
amp: bool | None = None
|
| 65 |
+
|
| 66 |
+
def __post_init__(self) -> None:
|
| 67 |
+
if self.amp is not None and not isinstance(self.amp, bool):
|
| 68 |
+
raise TypeError("RF-DETR amp must be boolean when specified")
|
| 69 |
+
|
| 70 |
+
def resolved_resolution(self, variant: str) -> int:
|
| 71 |
+
if variant not in VARIANT_SPECS:
|
| 72 |
+
raise ValueError(f"Unknown RF-DETR variant: {variant}")
|
| 73 |
+
resolution = (
|
| 74 |
+
VARIANT_SPECS[variant].resolution
|
| 75 |
+
if self.resolution is None
|
| 76 |
+
else self.resolution
|
| 77 |
+
)
|
| 78 |
+
if isinstance(resolution, bool) or not isinstance(resolution, int) or resolution < 1:
|
| 79 |
+
raise ValueError("RF-DETR resolution must be a positive integer")
|
| 80 |
+
divisor = VARIANT_SPECS[variant].input_divisor
|
| 81 |
+
if resolution % divisor:
|
| 82 |
+
raise ValueError(
|
| 83 |
+
f"RF-DETR {variant} resolution must be divisible by {divisor}; "
|
| 84 |
+
f"received {resolution}"
|
| 85 |
+
)
|
| 86 |
+
return resolution
|
| 87 |
+
|
| 88 |
+
def constructor_kwargs(self, variant: str) -> dict[str, Any]:
|
| 89 |
+
self.resolved_resolution(variant)
|
| 90 |
+
values: dict[str, Any] = {}
|
| 91 |
+
if self.resolution is not None:
|
| 92 |
+
values["resolution"] = self.resolution
|
| 93 |
+
if self.amp is not None:
|
| 94 |
+
values["amp"] = self.amp
|
| 95 |
+
return values
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@dataclass(frozen=True)
|
| 99 |
+
class RFDETRBackendConfig:
|
| 100 |
+
"""Effective architecture values read from a constructed pinned backend."""
|
| 101 |
+
|
| 102 |
+
variant: str
|
| 103 |
+
resolution: int
|
| 104 |
+
patch_size: int
|
| 105 |
+
num_windows: int
|
| 106 |
+
decoder_layers: int
|
| 107 |
+
num_queries: int
|
| 108 |
+
num_select: int
|
| 109 |
+
amp: bool
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@dataclass(frozen=True)
|
| 113 |
+
class RFDETRTrainConfig:
|
| 114 |
+
"""Typed Stage-1 settings passed explicitly through RF-DETR's public API."""
|
| 115 |
+
|
| 116 |
+
epochs: int = 12
|
| 117 |
+
batch_size: int = 1
|
| 118 |
+
gradient_accumulation: int = 16
|
| 119 |
+
workers: int = 4
|
| 120 |
+
seed: int = 42
|
| 121 |
+
learning_rate: float = 1e-4
|
| 122 |
+
encoder_learning_rate: float = 1.5e-4
|
| 123 |
+
weight_decay: float = 1e-4
|
| 124 |
+
lr_scheduler: str = "step"
|
| 125 |
+
warmup_epochs: float = 0.0
|
| 126 |
+
multi_scale: bool = True
|
| 127 |
+
expanded_scales: bool = True
|
| 128 |
+
|
| 129 |
+
def __post_init__(self) -> None:
|
| 130 |
+
for name in ("epochs", "batch_size", "gradient_accumulation", "workers"):
|
| 131 |
+
value = getattr(self, name)
|
| 132 |
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
| 133 |
+
raise ValueError(f"{name} must be a positive integer")
|
| 134 |
+
if (
|
| 135 |
+
isinstance(self.seed, bool)
|
| 136 |
+
or not isinstance(self.seed, int)
|
| 137 |
+
or not 0 <= self.seed < 2**32
|
| 138 |
+
):
|
| 139 |
+
raise ValueError("seed must be an unsigned 32-bit integer")
|
| 140 |
+
for name in ("learning_rate", "encoder_learning_rate"):
|
| 141 |
+
value = getattr(self, name)
|
| 142 |
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
| 143 |
+
raise TypeError(f"{name} must be numeric")
|
| 144 |
+
if not 0.0 < float(value) < float("inf"):
|
| 145 |
+
raise ValueError(f"{name} must be finite and positive")
|
| 146 |
+
for name in ("weight_decay", "warmup_epochs"):
|
| 147 |
+
value = getattr(self, name)
|
| 148 |
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
| 149 |
+
raise TypeError(f"{name} must be numeric")
|
| 150 |
+
if not 0.0 <= float(value) < float("inf"):
|
| 151 |
+
raise ValueError(f"{name} must be finite and nonnegative")
|
| 152 |
+
if self.lr_scheduler not in ("cosine", "step"):
|
| 153 |
+
raise ValueError("lr_scheduler must be 'cosine' or 'step'")
|
| 154 |
+
if not isinstance(self.multi_scale, bool) or not isinstance(self.expanded_scales, bool):
|
| 155 |
+
raise TypeError("multi_scale and expanded_scales must be boolean")
|
| 156 |
+
|
| 157 |
+
def backend_kwargs(self) -> dict[str, Any]:
|
| 158 |
+
return {
|
| 159 |
+
"epochs": self.epochs,
|
| 160 |
+
"batch_size": self.batch_size,
|
| 161 |
+
"grad_accum_steps": self.gradient_accumulation,
|
| 162 |
+
"num_workers": self.workers,
|
| 163 |
+
"seed": self.seed,
|
| 164 |
+
"lr": self.learning_rate,
|
| 165 |
+
"lr_encoder": self.encoder_learning_rate,
|
| 166 |
+
"weight_decay": self.weight_decay,
|
| 167 |
+
"lr_scheduler": self.lr_scheduler,
|
| 168 |
+
"warmup_epochs": self.warmup_epochs,
|
| 169 |
+
"multi_scale": self.multi_scale,
|
| 170 |
+
"expanded_scales": self.expanded_scales,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@dataclass(frozen=True)
|
| 175 |
+
class ProposalPolicy:
|
| 176 |
+
"""One deterministic proposal-selection contract for train and inference."""
|
| 177 |
+
|
| 178 |
+
score_threshold: float = 0.15
|
| 179 |
+
nms_threshold: float = 0.6
|
| 180 |
+
max_regions: int = 100
|
| 181 |
+
mask_threshold: float = 0.5
|
| 182 |
+
reject_empty_masks: bool = True
|
| 183 |
+
version: str = PROPOSAL_POLICY_VERSION
|
| 184 |
+
|
| 185 |
+
def __post_init__(self) -> None:
|
| 186 |
+
for name in ("score_threshold", "nms_threshold", "mask_threshold"):
|
| 187 |
+
value = getattr(self, name)
|
| 188 |
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
| 189 |
+
raise TypeError(f"{name} must be numeric")
|
| 190 |
+
if not 0.0 <= float(value) <= 1.0:
|
| 191 |
+
raise ValueError(f"{name} must lie in [0, 1]")
|
| 192 |
+
if isinstance(self.max_regions, bool) or not isinstance(self.max_regions, int):
|
| 193 |
+
raise TypeError("max_regions must be an integer")
|
| 194 |
+
if self.max_regions < 1:
|
| 195 |
+
raise ValueError("max_regions must be positive")
|
| 196 |
+
if not isinstance(self.reject_empty_masks, bool):
|
| 197 |
+
raise TypeError("reject_empty_masks must be boolean")
|
| 198 |
+
if not isinstance(self.version, str) or not self.version:
|
| 199 |
+
raise ValueError("proposal policy version must be a non-empty string")
|
| 200 |
+
|
| 201 |
+
def to_dict(self) -> dict[str, Any]:
|
| 202 |
+
"""Return serializable proposal settings."""
|
| 203 |
+
|
| 204 |
+
return {
|
| 205 |
+
"version": self.version,
|
| 206 |
+
"backend_score_threshold": "-inf",
|
| 207 |
+
"score_threshold": float(self.score_threshold),
|
| 208 |
+
"nms_threshold": float(self.nms_threshold),
|
| 209 |
+
"max_regions": self.max_regions,
|
| 210 |
+
"mask_threshold": float(self.mask_threshold),
|
| 211 |
+
"mask_threshold_operator": ">=",
|
| 212 |
+
"reject_empty_masks": self.reject_empty_masks,
|
| 213 |
+
"ordering": "score_descending_then_backend_index",
|
| 214 |
+
"nms_tie_break": "lower_backend_index",
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
@staticmethod
|
| 218 |
+
def _stable_nms(boxes: torch.Tensor, threshold: float) -> torch.Tensor:
|
| 219 |
+
"""NMS for pre-sorted boxes with an explicit lower-index tie break."""
|
| 220 |
+
|
| 221 |
+
remaining = torch.arange(len(boxes), dtype=torch.long)
|
| 222 |
+
kept = []
|
| 223 |
+
while remaining.numel():
|
| 224 |
+
current = remaining[0]
|
| 225 |
+
kept.append(current)
|
| 226 |
+
if remaining.numel() == 1:
|
| 227 |
+
break
|
| 228 |
+
rest = remaining[1:]
|
| 229 |
+
left_top = torch.maximum(boxes[current, :2], boxes[rest, :2])
|
| 230 |
+
right_bottom = torch.minimum(boxes[current, 2:], boxes[rest, 2:])
|
| 231 |
+
intersection = (right_bottom - left_top).clamp_min(0).prod(dim=1)
|
| 232 |
+
current_area = (boxes[current, 2:] - boxes[current, :2]).prod()
|
| 233 |
+
rest_area = (boxes[rest, 2:] - boxes[rest, :2]).prod(dim=1)
|
| 234 |
+
overlap = intersection / (current_area + rest_area - intersection).clamp_min(1e-12)
|
| 235 |
+
remaining = rest[overlap <= threshold]
|
| 236 |
+
return torch.stack(kept) if kept else torch.empty(0, dtype=torch.long)
|
| 237 |
+
|
| 238 |
+
def select(
|
| 239 |
+
self,
|
| 240 |
+
boxes: Any,
|
| 241 |
+
scores: Any,
|
| 242 |
+
masks: Any,
|
| 243 |
+
original_hw: tuple[int, int],
|
| 244 |
+
*,
|
| 245 |
+
mask_finite: Any | None = None,
|
| 246 |
+
) -> DetectorOutput:
|
| 247 |
+
"""Validate and select proposals without consulting ground truth."""
|
| 248 |
+
|
| 249 |
+
if (
|
| 250 |
+
not isinstance(original_hw, tuple)
|
| 251 |
+
or len(original_hw) != 2
|
| 252 |
+
or any(
|
| 253 |
+
isinstance(value, bool) or not isinstance(value, int) or value < 1
|
| 254 |
+
for value in original_hw
|
| 255 |
+
)
|
| 256 |
+
):
|
| 257 |
+
raise ValueError("original_hw must contain positive integer height and width")
|
| 258 |
+
box_tensor = torch.as_tensor(boxes).detach().to("cpu", torch.float32)
|
| 259 |
+
score_tensor = torch.as_tensor(scores).detach().to("cpu", torch.float32)
|
| 260 |
+
mask_tensor = torch.as_tensor(masks).detach().to("cpu")
|
| 261 |
+
if box_tensor.ndim != 2 or box_tensor.shape[-1] != 4:
|
| 262 |
+
raise ValueError("boxes must have shape [proposals, 4]")
|
| 263 |
+
if score_tensor.ndim != 1 or score_tensor.shape[0] != box_tensor.shape[0]:
|
| 264 |
+
raise ValueError("scores must have shape [proposals]")
|
| 265 |
+
if mask_tensor.ndim != 3 or mask_tensor.shape[0] != box_tensor.shape[0]:
|
| 266 |
+
raise ValueError("masks must have shape [proposals, height, width]")
|
| 267 |
+
if tuple(mask_tensor.shape[-2:]) != original_hw:
|
| 268 |
+
raise ValueError("proposal masks must match original_hw")
|
| 269 |
+
if mask_tensor.dtype != torch.bool:
|
| 270 |
+
raise TypeError("ProposalPolicy.select requires already-binarized masks")
|
| 271 |
+
if mask_finite is None:
|
| 272 |
+
finite_masks = torch.ones(len(box_tensor), dtype=torch.bool)
|
| 273 |
+
else:
|
| 274 |
+
finite_masks = torch.as_tensor(mask_finite).detach().to("cpu", torch.bool)
|
| 275 |
+
if finite_masks.shape != (len(box_tensor),):
|
| 276 |
+
raise ValueError("mask_finite must have shape [proposals]")
|
| 277 |
+
|
| 278 |
+
height, width = original_hw
|
| 279 |
+
box_tensor = box_tensor.clone()
|
| 280 |
+
box_tensor[:, 0::2].clamp_(0, width)
|
| 281 |
+
box_tensor[:, 1::2].clamp_(0, height)
|
| 282 |
+
valid = torch.isfinite(box_tensor).all(dim=1) & torch.isfinite(score_tensor)
|
| 283 |
+
valid &= finite_masks & (score_tensor >= self.score_threshold)
|
| 284 |
+
valid &= (box_tensor[:, 2] > box_tensor[:, 0]) & (box_tensor[:, 3] > box_tensor[:, 1])
|
| 285 |
+
if self.reject_empty_masks:
|
| 286 |
+
valid &= mask_tensor.flatten(1).any(dim=1)
|
| 287 |
+
|
| 288 |
+
raw_indices = torch.arange(len(box_tensor), dtype=torch.long)
|
| 289 |
+
box_tensor = box_tensor[valid]
|
| 290 |
+
score_tensor = score_tensor[valid]
|
| 291 |
+
mask_tensor = mask_tensor[valid]
|
| 292 |
+
raw_indices = raw_indices[valid]
|
| 293 |
+
if not len(box_tensor):
|
| 294 |
+
return DetectorOutput(
|
| 295 |
+
box_tensor,
|
| 296 |
+
score_tensor,
|
| 297 |
+
mask_tensor,
|
| 298 |
+
original_hw,
|
| 299 |
+
raw_indices,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
order = torch.argsort(score_tensor, descending=True, stable=True)
|
| 303 |
+
box_tensor = box_tensor[order]
|
| 304 |
+
score_tensor = score_tensor[order]
|
| 305 |
+
mask_tensor = mask_tensor[order]
|
| 306 |
+
raw_indices = raw_indices[order]
|
| 307 |
+
keep = self._stable_nms(box_tensor, float(self.nms_threshold))[: self.max_regions]
|
| 308 |
+
return DetectorOutput(
|
| 309 |
+
box_tensor[keep],
|
| 310 |
+
score_tensor[keep],
|
| 311 |
+
mask_tensor[keep],
|
| 312 |
+
original_hw,
|
| 313 |
+
raw_indices[keep],
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@dataclass
|
| 318 |
+
class DetectorOutput:
|
| 319 |
+
"""Post-threshold, post-NMS RF-DETR output in original-image coordinates."""
|
| 320 |
+
|
| 321 |
+
boxes_xyxy: torch.Tensor
|
| 322 |
+
scores: torch.Tensor
|
| 323 |
+
masks: torch.Tensor
|
| 324 |
+
original_hw: tuple[int, int]
|
| 325 |
+
proposal_ids: torch.Tensor | None = None
|
| 326 |
+
|
| 327 |
+
def to(self, device: torch.device | str) -> DetectorOutput:
|
| 328 |
+
return DetectorOutput(
|
| 329 |
+
self.boxes_xyxy.to(device),
|
| 330 |
+
self.scores.to(device),
|
| 331 |
+
self.masks.to(device),
|
| 332 |
+
self.original_hw,
|
| 333 |
+
None if self.proposal_ids is None else self.proposal_ids.to(device),
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _models() -> dict[str, type]:
|
| 338 |
+
# Albumentations otherwise performs a PyPI version check during RF-DETR import.
|
| 339 |
+
os.environ.setdefault("NO_ALBUMENTATIONS_UPDATE", "1")
|
| 340 |
+
try:
|
| 341 |
+
from rfdetr import (
|
| 342 |
+
RFDETRSeg2XLarge,
|
| 343 |
+
RFDETRSegLarge,
|
| 344 |
+
RFDETRSegMedium,
|
| 345 |
+
RFDETRSegNano,
|
| 346 |
+
RFDETRSegSmall,
|
| 347 |
+
RFDETRSegXLarge,
|
| 348 |
+
)
|
| 349 |
+
except ImportError as error:
|
| 350 |
+
raise ImportError(
|
| 351 |
+
"Install the pinned environment with `conda env create -f environment.yml`"
|
| 352 |
+
) from error
|
| 353 |
+
return {
|
| 354 |
+
"seg-nano": RFDETRSegNano,
|
| 355 |
+
"seg-small": RFDETRSegSmall,
|
| 356 |
+
"seg-medium": RFDETRSegMedium,
|
| 357 |
+
"seg-large": RFDETRSegLarge,
|
| 358 |
+
"seg-xlarge": RFDETRSegXLarge,
|
| 359 |
+
"seg-2xlarge": RFDETRSeg2XLarge,
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class RFDETRDetector:
|
| 364 |
+
"""Frozen RF-DETR with a portable, weights-free construction path."""
|
| 365 |
+
|
| 366 |
+
def __init__(
|
| 367 |
+
self,
|
| 368 |
+
checkpoint: str | Path | None = None,
|
| 369 |
+
variant: str = "seg-2xlarge",
|
| 370 |
+
device: str = "cuda",
|
| 371 |
+
score_threshold: float | None = None,
|
| 372 |
+
nms_threshold: float | None = None,
|
| 373 |
+
max_regions: int | None = None,
|
| 374 |
+
mask_threshold: float | None = None,
|
| 375 |
+
resolution: int | None = None,
|
| 376 |
+
proposal_policy: ProposalPolicy | None = None,
|
| 377 |
+
model_config: RFDETRModelConfig | None = None,
|
| 378 |
+
model: Any | None = None,
|
| 379 |
+
):
|
| 380 |
+
if variant not in VARIANTS:
|
| 381 |
+
raise ValueError(f"Unknown RF-DETR variant: {variant}")
|
| 382 |
+
if model_config is not None and resolution is not None:
|
| 383 |
+
if model_config.resolution != resolution:
|
| 384 |
+
raise ValueError("resolution conflicts with model_config.resolution")
|
| 385 |
+
resolved_model_config = model_config or RFDETRModelConfig(resolution=resolution)
|
| 386 |
+
# This validation is intentionally before _models() so an incompatible
|
| 387 |
+
# request cannot trigger RF-DETR construction or weight download.
|
| 388 |
+
resolved_model_config.resolved_resolution(variant)
|
| 389 |
+
|
| 390 |
+
legacy_values = {
|
| 391 |
+
"score_threshold": score_threshold,
|
| 392 |
+
"nms_threshold": nms_threshold,
|
| 393 |
+
"max_regions": max_regions,
|
| 394 |
+
"mask_threshold": mask_threshold,
|
| 395 |
+
}
|
| 396 |
+
if proposal_policy is None:
|
| 397 |
+
proposal_policy = ProposalPolicy(
|
| 398 |
+
score_threshold=0.15 if score_threshold is None else score_threshold,
|
| 399 |
+
nms_threshold=0.6 if nms_threshold is None else nms_threshold,
|
| 400 |
+
max_regions=100 if max_regions is None else max_regions,
|
| 401 |
+
mask_threshold=0.5 if mask_threshold is None else mask_threshold,
|
| 402 |
+
)
|
| 403 |
+
else:
|
| 404 |
+
if not isinstance(proposal_policy, ProposalPolicy):
|
| 405 |
+
raise TypeError("proposal_policy must be a ProposalPolicy")
|
| 406 |
+
for name, value in legacy_values.items():
|
| 407 |
+
if value is not None and value != getattr(proposal_policy, name):
|
| 408 |
+
raise ValueError(f"{name} conflicts with proposal_policy.{name}")
|
| 409 |
+
if model is None:
|
| 410 |
+
kwargs: dict[str, Any] = {"device": device}
|
| 411 |
+
if checkpoint is not None:
|
| 412 |
+
kwargs["pretrain_weights"] = str(Path(checkpoint).expanduser().resolve())
|
| 413 |
+
kwargs.update(resolved_model_config.constructor_kwargs(variant))
|
| 414 |
+
model = _models()[variant](**kwargs)
|
| 415 |
+
self.model = model
|
| 416 |
+
self.variant = variant
|
| 417 |
+
self.model_config = resolved_model_config
|
| 418 |
+
self.proposal_policy = proposal_policy
|
| 419 |
+
# Keep the original scalar attributes for downstream compatibility.
|
| 420 |
+
self.score_threshold = proposal_policy.score_threshold
|
| 421 |
+
self.nms_threshold = proposal_policy.nms_threshold
|
| 422 |
+
self.max_regions = proposal_policy.max_regions
|
| 423 |
+
self.mask_threshold = proposal_policy.mask_threshold
|
| 424 |
+
self.backend_config = self._backend_config(model, variant, resolved_model_config)
|
| 425 |
+
|
| 426 |
+
@property
|
| 427 |
+
def torch_model(self) -> torch.nn.Module:
|
| 428 |
+
"""The module to register in a complete FALCON state dictionary."""
|
| 429 |
+
|
| 430 |
+
return self.model.model.model
|
| 431 |
+
|
| 432 |
+
def export_config(self) -> dict[str, Any]:
|
| 433 |
+
"""Export architecture and proposal settings without machine-local paths."""
|
| 434 |
+
|
| 435 |
+
architecture = self.model.model_config.model_dump()
|
| 436 |
+
architecture.pop("pretrain_weights", None)
|
| 437 |
+
architecture.pop("device", None)
|
| 438 |
+
# RF-DETR may replace its classification head while loading a checkpoint
|
| 439 |
+
# without updating model_config.num_classes.
|
| 440 |
+
architecture["num_classes"] = int(self.torch_model.class_embed.bias.shape[0]) - 1
|
| 441 |
+
return {
|
| 442 |
+
"variant": self.variant,
|
| 443 |
+
"architecture": architecture,
|
| 444 |
+
"proposal_policy": asdict(self.proposal_policy),
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
@classmethod
|
| 448 |
+
def from_config(
|
| 449 |
+
cls,
|
| 450 |
+
detector_config: Mapping[str, Any],
|
| 451 |
+
device: str | torch.device = "cpu",
|
| 452 |
+
) -> RFDETRDetector:
|
| 453 |
+
"""Construct the pinned segmentation architecture without loading weights.
|
| 454 |
+
|
| 455 |
+
Unlike RF-DETR's training constructor, this also supports Transformers'
|
| 456 |
+
meta-device initialization for low-memory checkpoint loading.
|
| 457 |
+
"""
|
| 458 |
+
|
| 459 |
+
if not isinstance(detector_config, Mapping) or set(detector_config) != {
|
| 460 |
+
"variant", "architecture", "proposal_policy"
|
| 461 |
+
}:
|
| 462 |
+
raise ValueError("Invalid portable RF-DETR configuration")
|
| 463 |
+
variant = detector_config["variant"]
|
| 464 |
+
if variant not in VARIANTS:
|
| 465 |
+
raise ValueError(f"Unknown RF-DETR variant: {variant}")
|
| 466 |
+
architecture = dict(detector_config["architecture"])
|
| 467 |
+
if "pretrain_weights" in architecture or "device" in architecture:
|
| 468 |
+
raise ValueError("Portable detector architecture cannot name weights or devices")
|
| 469 |
+
# In pinned RF-DETR, patch_size != 14 disables the otherwise implicit
|
| 470 |
+
# DINOv2 Hub initialization. All supported segmentation variants use 12.
|
| 471 |
+
if architecture.get("patch_size") != 12 or architecture.get("encoder") not in {
|
| 472 |
+
"dinov2_windowed_small", "dinov2_windowed_base"
|
| 473 |
+
} or architecture.get("segmentation_head") is not True:
|
| 474 |
+
raise ValueError("Unsupported portable RF-DETR segmentation architecture")
|
| 475 |
+
policy = ProposalPolicy(**detector_config["proposal_policy"])
|
| 476 |
+
model_options = RFDETRModelConfig(
|
| 477 |
+
resolution=architecture.get("resolution"), amp=architecture.get("amp")
|
| 478 |
+
)
|
| 479 |
+
model_options.resolved_resolution(variant)
|
| 480 |
+
model_class = _models()[variant]
|
| 481 |
+
from rfdetr.main import populate_args
|
| 482 |
+
from rfdetr.models.lwdetr import PostProcess, build_model
|
| 483 |
+
|
| 484 |
+
class ConfigOnlyModel(model_class):
|
| 485 |
+
def maybe_download_pretrain_weights(self):
|
| 486 |
+
# Both the detector and its DINO encoder come from FALCON's
|
| 487 |
+
# state dictionary; no external initialization is needed.
|
| 488 |
+
pass
|
| 489 |
+
|
| 490 |
+
def get_model(self, config):
|
| 491 |
+
args = populate_args(**config.model_dump())
|
| 492 |
+
# Accelerate intercepts parameter registration but RF-DETR also
|
| 493 |
+
# creates plain tensors for its initial classification bias.
|
| 494 |
+
# Keep those on the same initialization device as parameters.
|
| 495 |
+
initialization_device = torch.nn.Embedding(1, 1).weight.device
|
| 496 |
+
with torch.device(initialization_device):
|
| 497 |
+
module = build_model(args)
|
| 498 |
+
return SimpleNamespace(
|
| 499 |
+
args=args,
|
| 500 |
+
resolution=args.resolution,
|
| 501 |
+
model=module,
|
| 502 |
+
device=torch.device(device),
|
| 503 |
+
postprocess=PostProcess(num_select=args.num_select),
|
| 504 |
+
class_names=[],
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
backend = ConfigOnlyModel(**architecture, pretrain_weights=None, device="cpu")
|
| 508 |
+
detector = cls(
|
| 509 |
+
variant=variant,
|
| 510 |
+
model=backend,
|
| 511 |
+
model_config=model_options,
|
| 512 |
+
proposal_policy=policy,
|
| 513 |
+
)
|
| 514 |
+
detector.torch_model.requires_grad_(False)
|
| 515 |
+
if not next(detector.torch_model.parameters()).is_meta:
|
| 516 |
+
detector.to(device)
|
| 517 |
+
return detector.eval()
|
| 518 |
+
|
| 519 |
+
def to(self, device: str | torch.device) -> RFDETRDetector:
|
| 520 |
+
self.torch_model.to(device)
|
| 521 |
+
self.model.model.device = torch.device(device)
|
| 522 |
+
return self
|
| 523 |
+
|
| 524 |
+
def eval(self) -> RFDETRDetector:
|
| 525 |
+
self.torch_model.eval()
|
| 526 |
+
return self
|
| 527 |
+
|
| 528 |
+
@staticmethod
|
| 529 |
+
def _backend_config(
|
| 530 |
+
model: Any,
|
| 531 |
+
variant: str,
|
| 532 |
+
requested: RFDETRModelConfig,
|
| 533 |
+
) -> RFDETRBackendConfig:
|
| 534 |
+
fallback = VARIANT_SPECS[variant]
|
| 535 |
+
value = getattr(model, "model_config", None)
|
| 536 |
+
|
| 537 |
+
def read(name: str, default: int) -> int:
|
| 538 |
+
item = getattr(value, name, default)
|
| 539 |
+
if isinstance(item, bool) or not isinstance(item, int) or item < 1:
|
| 540 |
+
raise RuntimeError(f"RF-DETR backend reported invalid {name}: {item!r}")
|
| 541 |
+
return item
|
| 542 |
+
|
| 543 |
+
amp = getattr(value, "amp", True if requested.amp is None else requested.amp)
|
| 544 |
+
if not isinstance(amp, bool):
|
| 545 |
+
raise RuntimeError(f"RF-DETR backend reported invalid amp: {amp!r}")
|
| 546 |
+
|
| 547 |
+
effective = RFDETRBackendConfig(
|
| 548 |
+
variant=variant,
|
| 549 |
+
resolution=read("resolution", requested.resolved_resolution(variant)),
|
| 550 |
+
patch_size=read("patch_size", fallback.patch_size),
|
| 551 |
+
num_windows=read("num_windows", fallback.num_windows),
|
| 552 |
+
decoder_layers=read("dec_layers", fallback.decoder_layers),
|
| 553 |
+
num_queries=read("num_queries", fallback.num_queries),
|
| 554 |
+
num_select=read("num_select", fallback.num_select),
|
| 555 |
+
amp=amp,
|
| 556 |
+
)
|
| 557 |
+
if effective.resolution % (effective.patch_size * effective.num_windows):
|
| 558 |
+
raise RuntimeError("constructed RF-DETR backend has an incompatible input resolution")
|
| 559 |
+
return effective
|
| 560 |
+
|
| 561 |
+
@staticmethod
|
| 562 |
+
def _image_hw(image: Any) -> tuple[int, int]:
|
| 563 |
+
if isinstance(image, str | Path):
|
| 564 |
+
with Image.open(image) as opened:
|
| 565 |
+
return int(opened.height), int(opened.width)
|
| 566 |
+
if isinstance(image, Image.Image):
|
| 567 |
+
return int(image.height), int(image.width)
|
| 568 |
+
shape = tuple(image.shape) if hasattr(image, "shape") else ()
|
| 569 |
+
if len(shape) == 2:
|
| 570 |
+
return int(shape[0]), int(shape[1])
|
| 571 |
+
if len(shape) != 3:
|
| 572 |
+
raise ValueError("RF-DETR input must be one PIL, NumPy, or tensor image")
|
| 573 |
+
if shape[0] in (1, 3, 4):
|
| 574 |
+
return int(shape[-2]), int(shape[-1])
|
| 575 |
+
return int(shape[0]), int(shape[1])
|
| 576 |
+
|
| 577 |
+
def _masks(
|
| 578 |
+
self,
|
| 579 |
+
value: Any,
|
| 580 |
+
*,
|
| 581 |
+
count: int,
|
| 582 |
+
original_hw: tuple[int, int],
|
| 583 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 584 |
+
if value is None:
|
| 585 |
+
if count:
|
| 586 |
+
raise RuntimeError("RF-DETR segmentation output did not contain masks")
|
| 587 |
+
return (
|
| 588 |
+
torch.empty((0, *original_hw), dtype=torch.bool),
|
| 589 |
+
torch.empty(0, dtype=torch.bool),
|
| 590 |
+
)
|
| 591 |
+
array = (
|
| 592 |
+
value.detach().cpu().numpy()
|
| 593 |
+
if isinstance(value, torch.Tensor)
|
| 594 |
+
else np.asarray(value)
|
| 595 |
+
)
|
| 596 |
+
if count == 0 and array.size == 0:
|
| 597 |
+
return (
|
| 598 |
+
torch.empty((0, *original_hw), dtype=torch.bool),
|
| 599 |
+
torch.empty(0, dtype=torch.bool),
|
| 600 |
+
)
|
| 601 |
+
if count == 1 and array.ndim == 2:
|
| 602 |
+
array = array[None]
|
| 603 |
+
if array.ndim != 3 or array.shape[0] != count:
|
| 604 |
+
raise RuntimeError("Invalid RF-DETR mask shape")
|
| 605 |
+
masks = torch.as_tensor(array)
|
| 606 |
+
if masks.dtype == torch.bool:
|
| 607 |
+
finite = torch.ones(count, dtype=torch.bool)
|
| 608 |
+
else:
|
| 609 |
+
masks = masks.to(dtype=torch.float32)
|
| 610 |
+
finite = torch.isfinite(masks).flatten(1).all(dim=1)
|
| 611 |
+
# ProposalPolicy v1 deliberately preserves the existing >= mask
|
| 612 |
+
# threshold semantics. Non-finite proposals are rejected later.
|
| 613 |
+
masks = masks >= self.mask_threshold
|
| 614 |
+
if tuple(masks.shape[-2:]) != original_hw:
|
| 615 |
+
masks = F.interpolate(
|
| 616 |
+
masks.to(dtype=torch.float32).unsqueeze(1),
|
| 617 |
+
size=original_hw,
|
| 618 |
+
mode="nearest",
|
| 619 |
+
).squeeze(1) > 0.5
|
| 620 |
+
return masks, finite
|
| 621 |
+
|
| 622 |
+
@torch.inference_mode()
|
| 623 |
+
def __call__(self, image: Any) -> DetectorOutput:
|
| 624 |
+
# An enclosing PreTrainedModel may have moved the registered module.
|
| 625 |
+
# RF-DETR also caches the input device outside that module.
|
| 626 |
+
backend = getattr(self.model, "model", None)
|
| 627 |
+
module = getattr(backend, "model", None)
|
| 628 |
+
if isinstance(module, torch.nn.Module):
|
| 629 |
+
backend.device = next(module.parameters()).device
|
| 630 |
+
module.eval()
|
| 631 |
+
detection = self.model.predict(image, threshold=BACKEND_SCORE_THRESHOLD)
|
| 632 |
+
if isinstance(detection, list | tuple):
|
| 633 |
+
if len(detection) != 1:
|
| 634 |
+
raise RuntimeError("RFDETRDetector accepts exactly one image per call")
|
| 635 |
+
detection = detection[0]
|
| 636 |
+
original_hw = self._image_hw(image)
|
| 637 |
+
boxes = torch.as_tensor(detection.xyxy).detach().to("cpu", torch.float32).reshape(-1, 4)
|
| 638 |
+
confidence = detection.confidence
|
| 639 |
+
if confidence is None:
|
| 640 |
+
if len(boxes):
|
| 641 |
+
raise RuntimeError("RF-DETR output did not contain confidence scores")
|
| 642 |
+
scores = torch.empty(0, dtype=torch.float32)
|
| 643 |
+
else:
|
| 644 |
+
scores = torch.as_tensor(confidence).detach().to("cpu", torch.float32).reshape(-1)
|
| 645 |
+
masks, mask_finite = self._masks(
|
| 646 |
+
detection.mask,
|
| 647 |
+
count=len(boxes),
|
| 648 |
+
original_hw=original_hw,
|
| 649 |
+
)
|
| 650 |
+
if len(boxes) != len(scores):
|
| 651 |
+
raise RuntimeError("RF-DETR box and score counts differ")
|
| 652 |
+
return self.proposal_policy.select(
|
| 653 |
+
boxes,
|
| 654 |
+
scores,
|
| 655 |
+
masks,
|
| 656 |
+
original_hw,
|
| 657 |
+
mask_finite=mask_finite,
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def _protect_stage1_output(output_root: Path, *, resume: bool) -> None:
|
| 662 |
+
if resume:
|
| 663 |
+
if not output_root.is_dir():
|
| 664 |
+
raise ValueError("Stage-1 resume requires its existing output directory")
|
| 665 |
+
extra_weights = [
|
| 666 |
+
path.name
|
| 667 |
+
for path in output_root.iterdir()
|
| 668 |
+
if path.suffix in {".pt", ".pth", ".safetensors"} and path.name != "last.pth"
|
| 669 |
+
]
|
| 670 |
+
if extra_weights:
|
| 671 |
+
raise ValueError(
|
| 672 |
+
"Stage-1 resume expects only last.pth; move other weights out of the "
|
| 673 |
+
f"output directory first: {', '.join(sorted(extra_weights))}"
|
| 674 |
+
)
|
| 675 |
+
return
|
| 676 |
+
if output_root.exists():
|
| 677 |
+
if not output_root.is_dir():
|
| 678 |
+
raise ValueError("Stage-1 output path exists and is not a directory")
|
| 679 |
+
# Keep the lock inode stable across processes.
|
| 680 |
+
unexpected = next(
|
| 681 |
+
(entry for entry in output_root.iterdir() if entry.name != STAGE1_LOCK_NAME),
|
| 682 |
+
None,
|
| 683 |
+
)
|
| 684 |
+
if unexpected is not None:
|
| 685 |
+
raise ValueError(
|
| 686 |
+
"Stage-1 output directory is not empty; choose a new directory to avoid "
|
| 687 |
+
"overwriting prior run artifacts"
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
@contextmanager
|
| 692 |
+
def _stage1_output_lock(output_root: Path) -> Iterator[None]:
|
| 693 |
+
"""Hold the cooperative Stage-1 output lock for the complete run lifetime."""
|
| 694 |
+
|
| 695 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 696 |
+
lock_path = output_root / STAGE1_LOCK_NAME
|
| 697 |
+
flags = os.O_RDWR | os.O_CREAT
|
| 698 |
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
| 699 |
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
| 700 |
+
try:
|
| 701 |
+
descriptor = os.open(lock_path, flags, 0o600)
|
| 702 |
+
except OSError as exc:
|
| 703 |
+
raise ValueError(f"cannot safely open Stage-1 output lock: {exc}") from exc
|
| 704 |
+
locked = False
|
| 705 |
+
try:
|
| 706 |
+
opened = os.fstat(descriptor)
|
| 707 |
+
linked = lock_path.lstat()
|
| 708 |
+
if (
|
| 709 |
+
not stat.S_ISREG(opened.st_mode)
|
| 710 |
+
or opened.st_dev != linked.st_dev
|
| 711 |
+
or opened.st_ino != linked.st_ino
|
| 712 |
+
or opened.st_nlink != 1
|
| 713 |
+
):
|
| 714 |
+
raise ValueError("Stage-1 output lock is not a private regular file")
|
| 715 |
+
try:
|
| 716 |
+
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 717 |
+
except BlockingIOError as exc:
|
| 718 |
+
raise ValueError(
|
| 719 |
+
"Stage-1 output is locked by another training process"
|
| 720 |
+
) from exc
|
| 721 |
+
locked = True
|
| 722 |
+
yield
|
| 723 |
+
finally:
|
| 724 |
+
if locked:
|
| 725 |
+
fcntl.flock(descriptor, fcntl.LOCK_UN)
|
| 726 |
+
os.close(descriptor)
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
def _expected_backend_config(
|
| 730 |
+
variant: str,
|
| 731 |
+
requested: RFDETRModelConfig,
|
| 732 |
+
) -> RFDETRBackendConfig:
|
| 733 |
+
spec = VARIANT_SPECS[variant]
|
| 734 |
+
return RFDETRBackendConfig(
|
| 735 |
+
variant=variant,
|
| 736 |
+
resolution=requested.resolved_resolution(variant),
|
| 737 |
+
patch_size=spec.patch_size,
|
| 738 |
+
num_windows=spec.num_windows,
|
| 739 |
+
decoder_layers=spec.decoder_layers,
|
| 740 |
+
num_queries=spec.num_queries,
|
| 741 |
+
num_select=spec.num_select,
|
| 742 |
+
amp=True if requested.amp is None else requested.amp,
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
def _read_stage1_manifest(path: Path) -> dict[str, Any]:
|
| 747 |
+
try:
|
| 748 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 749 |
+
except FileNotFoundError as exc:
|
| 750 |
+
raise ValueError("Stage-1 resume requires stage1_run.json in its output directory") from exc
|
| 751 |
+
except (OSError, json.JSONDecodeError) as exc:
|
| 752 |
+
raise ValueError(f"cannot read Stage-1 run manifest: {exc}") from exc
|
| 753 |
+
if not isinstance(payload, dict) or payload.get("stage") != 1:
|
| 754 |
+
raise ValueError("Stage-1 resume requires Stage-1 run metadata")
|
| 755 |
+
if not isinstance(payload.get("config"), dict):
|
| 756 |
+
raise ValueError("Stage-1 run metadata is missing its configuration")
|
| 757 |
+
return payload
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
def _validate_resume_checkpoint(
|
| 761 |
+
output_root: Path,
|
| 762 |
+
checkpoint: str | Path,
|
| 763 |
+
) -> Path:
|
| 764 |
+
path = Path(checkpoint).expanduser().resolve(strict=True)
|
| 765 |
+
if not path.is_file() or path != output_root / "last.pth":
|
| 766 |
+
raise ValueError("Stage-1 --resume must point to last.pth inside its output directory")
|
| 767 |
+
return path
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
@contextmanager
|
| 771 |
+
def _rolling_checkpoint(
|
| 772 |
+
output_root: Path,
|
| 773 |
+
manifest_path: Path,
|
| 774 |
+
manifest: dict[str, Any],
|
| 775 |
+
) -> Iterator[None]:
|
| 776 |
+
"""Redirect RF-DETR 1.5.2 epoch saves to one atomic, resumable checkpoint."""
|
| 777 |
+
|
| 778 |
+
import rfdetr.main as backend
|
| 779 |
+
|
| 780 |
+
original_save = backend.save_on_master
|
| 781 |
+
|
| 782 |
+
def save_checkpoint(weights: Any, filename: Any, *args: Any, **kwargs: Any) -> None:
|
| 783 |
+
path = Path(filename)
|
| 784 |
+
if path.parent != output_root or not path.name.startswith("checkpoint"):
|
| 785 |
+
original_save(weights, filename, *args, **kwargs)
|
| 786 |
+
return
|
| 787 |
+
if path.name != "checkpoint.pth" or not backend.is_main_process():
|
| 788 |
+
return
|
| 789 |
+
temporary = None
|
| 790 |
+
try:
|
| 791 |
+
with tempfile.NamedTemporaryFile(dir=output_root, suffix=".tmp", delete=False) as stream:
|
| 792 |
+
temporary = Path(stream.name)
|
| 793 |
+
torch.save(weights, stream, *args, **kwargs)
|
| 794 |
+
stream.flush()
|
| 795 |
+
os.fsync(stream.fileno())
|
| 796 |
+
os.replace(temporary, output_root / "last.pth")
|
| 797 |
+
finally:
|
| 798 |
+
if temporary is not None:
|
| 799 |
+
temporary.unlink(missing_ok=True)
|
| 800 |
+
manifest["last_completed_epoch"] = weights["epoch"]
|
| 801 |
+
atomic_json(manifest_path, manifest)
|
| 802 |
+
|
| 803 |
+
# Epoch callbacks run after duplicate saves, so intercept the backend writer.
|
| 804 |
+
backend.save_on_master = save_checkpoint
|
| 805 |
+
try:
|
| 806 |
+
yield
|
| 807 |
+
finally:
|
| 808 |
+
backend.save_on_master = original_save
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
def _train_detector(
|
| 812 |
+
args: argparse.Namespace,
|
| 813 |
+
*,
|
| 814 |
+
data_root: Path,
|
| 815 |
+
output_root: Path,
|
| 816 |
+
) -> None:
|
| 817 |
+
config_path = getattr(args, "config", None)
|
| 818 |
+
loaded_config = None
|
| 819 |
+
if config_path is None:
|
| 820 |
+
detector_values: dict[str, Any] = {}
|
| 821 |
+
stage_values: dict[str, Any] = {}
|
| 822 |
+
else:
|
| 823 |
+
from .config import (
|
| 824 |
+
apply_stage_overrides,
|
| 825 |
+
load_config,
|
| 826 |
+
resolve_stage_config,
|
| 827 |
+
validate_config,
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
loaded_config = load_config(config_path)
|
| 831 |
+
override_names = (
|
| 832 |
+
"epochs",
|
| 833 |
+
"batch_size",
|
| 834 |
+
"gradient_accumulation",
|
| 835 |
+
"learning_rate",
|
| 836 |
+
"encoder_learning_rate",
|
| 837 |
+
"weight_decay",
|
| 838 |
+
"lr_scheduler",
|
| 839 |
+
"warmup_epochs",
|
| 840 |
+
"multi_scale",
|
| 841 |
+
"expanded_scales",
|
| 842 |
+
"precision",
|
| 843 |
+
"tf32",
|
| 844 |
+
)
|
| 845 |
+
stage_overrides = {
|
| 846 |
+
name: getattr(args, name)
|
| 847 |
+
for name in override_names
|
| 848 |
+
if getattr(args, name, None) is not None
|
| 849 |
+
}
|
| 850 |
+
loaded_config = apply_stage_overrides(loaded_config, 1, stage_overrides)
|
| 851 |
+
if getattr(args, "variant", None) is not None:
|
| 852 |
+
loaded_config["detector"]["variant"] = args.variant
|
| 853 |
+
if getattr(args, "resolution", None) is not None:
|
| 854 |
+
loaded_config["detector"]["resolution"] = args.resolution
|
| 855 |
+
validate_config(loaded_config)
|
| 856 |
+
detector_values = loaded_config["detector"]
|
| 857 |
+
stage_values = resolve_stage_config(loaded_config, 1)
|
| 858 |
+
|
| 859 |
+
def option(name: str, fallback: Any, *, config_name: str | None = None) -> Any:
|
| 860 |
+
value = getattr(args, name, None)
|
| 861 |
+
if value is not None:
|
| 862 |
+
return value
|
| 863 |
+
return stage_values.get(config_name or name, fallback)
|
| 864 |
+
|
| 865 |
+
variant = getattr(args, "variant", None) or detector_values.get("variant", "seg-2xlarge")
|
| 866 |
+
resolution = getattr(args, "resolution", None)
|
| 867 |
+
if resolution is None:
|
| 868 |
+
resolution = detector_values.get("resolution")
|
| 869 |
+
precision = option("precision", "backend_mixed")
|
| 870 |
+
if precision not in ("backend_mixed", "fp32"):
|
| 871 |
+
raise ValueError("Stage-1 precision must be 'backend_mixed' or 'fp32'")
|
| 872 |
+
tf32 = option("tf32", True)
|
| 873 |
+
if not isinstance(tf32, bool):
|
| 874 |
+
raise TypeError("Stage-1 tf32 must be boolean")
|
| 875 |
+
model_options = RFDETRModelConfig(
|
| 876 |
+
resolution=resolution,
|
| 877 |
+
amp=precision != "fp32",
|
| 878 |
+
)
|
| 879 |
+
# Fail before importing/constructing RF-DETR, which can otherwise download
|
| 880 |
+
# weights as part of its constructor.
|
| 881 |
+
model_options.resolved_resolution(variant)
|
| 882 |
+
train_options = RFDETRTrainConfig(
|
| 883 |
+
epochs=option("epochs", 12),
|
| 884 |
+
batch_size=option("batch_size", 1),
|
| 885 |
+
gradient_accumulation=option("gradient_accumulation", 16),
|
| 886 |
+
workers=option("workers", 4, config_name="workers"),
|
| 887 |
+
seed=42 if loaded_config is None else loaded_config["training"]["seed"],
|
| 888 |
+
learning_rate=option("learning_rate", 1e-4),
|
| 889 |
+
encoder_learning_rate=option("encoder_learning_rate", 1.5e-4),
|
| 890 |
+
weight_decay=option("weight_decay", 1e-4),
|
| 891 |
+
lr_scheduler=option("lr_scheduler", "step"),
|
| 892 |
+
warmup_epochs=option("warmup_epochs", 0.0),
|
| 893 |
+
multi_scale=option("multi_scale", True),
|
| 894 |
+
expanded_scales=option("expanded_scales", True),
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
resume_argument = getattr(args, "resume", None)
|
| 898 |
+
weights_path = getattr(args, "weights", None)
|
| 899 |
+
resolved_weights = None
|
| 900 |
+
if weights_path and resume_argument is None:
|
| 901 |
+
resolved_weights = Path(weights_path).expanduser().resolve(strict=True)
|
| 902 |
+
if not resolved_weights.is_file():
|
| 903 |
+
raise ValueError("Stage-1 --weights must point to a checkpoint file")
|
| 904 |
+
expected_backend = _expected_backend_config(variant, model_options)
|
| 905 |
+
run_config = {
|
| 906 |
+
"data_dir": str(data_root),
|
| 907 |
+
"backend": asdict(expected_backend),
|
| 908 |
+
"training": asdict(train_options),
|
| 909 |
+
"precision": precision,
|
| 910 |
+
"tf32": tf32,
|
| 911 |
+
}
|
| 912 |
+
manifest_path = output_root / "stage1_run.json"
|
| 913 |
+
resume_checkpoint = None
|
| 914 |
+
if resume_argument is None:
|
| 915 |
+
manifest = {
|
| 916 |
+
"stage": 1,
|
| 917 |
+
"status": "initialized",
|
| 918 |
+
"config": run_config,
|
| 919 |
+
"initial_weights": None if resolved_weights is None else str(resolved_weights),
|
| 920 |
+
"checkpoint": "last.pth",
|
| 921 |
+
"last_completed_epoch": None,
|
| 922 |
+
}
|
| 923 |
+
atomic_json(manifest_path, manifest)
|
| 924 |
+
else:
|
| 925 |
+
manifest = _read_stage1_manifest(manifest_path)
|
| 926 |
+
if manifest["config"] != run_config:
|
| 927 |
+
raise ValueError(
|
| 928 |
+
"Stage-1 resume settings differ from stage1_run.json; use the original "
|
| 929 |
+
"dataset, model configuration, and training options"
|
| 930 |
+
)
|
| 931 |
+
resume_checkpoint = _validate_resume_checkpoint(output_root, resume_argument)
|
| 932 |
+
manifest.pop("error", None)
|
| 933 |
+
|
| 934 |
+
try:
|
| 935 |
+
model_cls = _models()[variant]
|
| 936 |
+
# RF-DETR imports torch before configuring its engine; set the requested
|
| 937 |
+
# backend policy after import and before constructing/training the model.
|
| 938 |
+
torch.backends.cuda.matmul.allow_tf32 = tf32
|
| 939 |
+
torch.backends.cudnn.allow_tf32 = tf32
|
| 940 |
+
kwargs: dict[str, Any] = {"device": args.device}
|
| 941 |
+
kwargs.update(model_options.constructor_kwargs(variant))
|
| 942 |
+
if resume_checkpoint is not None:
|
| 943 |
+
kwargs["pretrain_weights"] = None
|
| 944 |
+
elif resolved_weights is not None:
|
| 945 |
+
kwargs["pretrain_weights"] = str(resolved_weights)
|
| 946 |
+
random.seed(train_options.seed)
|
| 947 |
+
np.random.seed(train_options.seed)
|
| 948 |
+
torch.manual_seed(train_options.seed)
|
| 949 |
+
model = model_cls(**kwargs)
|
| 950 |
+
backend_config = RFDETRDetector._backend_config(model, variant, model_options)
|
| 951 |
+
if backend_config != expected_backend:
|
| 952 |
+
raise RuntimeError("RF-DETR backend disagrees with the requested configuration")
|
| 953 |
+
manifest["status"] = "running"
|
| 954 |
+
atomic_json(manifest_path, manifest)
|
| 955 |
+
backend_kwargs = {
|
| 956 |
+
"dataset_dir": str(data_root),
|
| 957 |
+
"output_dir": str(output_root),
|
| 958 |
+
**train_options.backend_kwargs(),
|
| 959 |
+
"device": args.device,
|
| 960 |
+
"tensorboard": False,
|
| 961 |
+
"wandb": False,
|
| 962 |
+
"run_test": False,
|
| 963 |
+
}
|
| 964 |
+
if resume_checkpoint is not None:
|
| 965 |
+
backend_kwargs["resume"] = str(resume_checkpoint)
|
| 966 |
+
# RF-DETR reinitializes the class head before its own training seed call.
|
| 967 |
+
random.seed(train_options.seed)
|
| 968 |
+
np.random.seed(train_options.seed)
|
| 969 |
+
torch.manual_seed(train_options.seed)
|
| 970 |
+
with _rolling_checkpoint(output_root, manifest_path, manifest):
|
| 971 |
+
model.train(**backend_kwargs)
|
| 972 |
+
if not (output_root / "last.pth").is_file():
|
| 973 |
+
raise RuntimeError("RF-DETR training finished without saving last.pth")
|
| 974 |
+
manifest["status"] = "completed"
|
| 975 |
+
atomic_json(manifest_path, manifest)
|
| 976 |
+
except BaseException as error:
|
| 977 |
+
manifest["status"] = "failed"
|
| 978 |
+
manifest["error"] = f"{type(error).__name__}: {error}"
|
| 979 |
+
try:
|
| 980 |
+
atomic_json(manifest_path, manifest)
|
| 981 |
+
except OSError:
|
| 982 |
+
pass
|
| 983 |
+
raise
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
def train_detector(args: argparse.Namespace) -> None:
|
| 987 |
+
"""Train Stage 1 and retain one rolling last.pth checkpoint."""
|
| 988 |
+
|
| 989 |
+
data_root = Path(args.data_dir).expanduser().resolve(strict=True)
|
| 990 |
+
output_root = external_output(args.output_dir, data_root)
|
| 991 |
+
resume = getattr(args, "resume", None) is not None
|
| 992 |
+
_protect_stage1_output(output_root, resume=resume)
|
| 993 |
+
with _stage1_output_lock(output_root):
|
| 994 |
+
_protect_stage1_output(output_root, resume=resume)
|
| 995 |
+
_train_detector(args, data_root=data_root, output_root=output_root)
|
| 996 |
+
|
| 997 |
+
|
| 998 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 999 |
+
parser = argparse.ArgumentParser(description="Train the frozen Falcon RF-DETR stage")
|
| 1000 |
+
parser.add_argument("--data-dir", required=True, help="RF-DETR COCO dataset root")
|
| 1001 |
+
parser.add_argument(
|
| 1002 |
+
"--output-dir",
|
| 1003 |
+
required=True,
|
| 1004 |
+
help="New/empty output, or the existing Stage-1 directory for resume",
|
| 1005 |
+
)
|
| 1006 |
+
parser.add_argument("--weights", help="RF-DETR segmentation weights")
|
| 1007 |
+
parser.add_argument(
|
| 1008 |
+
"--resume",
|
| 1009 |
+
help="Resume from OUTPUT_DIR/last.pth at the next epoch",
|
| 1010 |
+
)
|
| 1011 |
+
parser.add_argument("--config", help="Optional Falcon YAML; explicit CLI options win")
|
| 1012 |
+
parser.add_argument("--variant", choices=VARIANTS)
|
| 1013 |
+
parser.add_argument("--resolution", type=int)
|
| 1014 |
+
parser.add_argument("--device", default="cuda")
|
| 1015 |
+
parser.add_argument("--epochs", type=int)
|
| 1016 |
+
parser.add_argument("--batch-size", type=int)
|
| 1017 |
+
parser.add_argument("--gradient-accumulation", type=int)
|
| 1018 |
+
parser.add_argument("--workers", type=int)
|
| 1019 |
+
parser.add_argument("--learning-rate", type=float)
|
| 1020 |
+
parser.add_argument("--encoder-learning-rate", type=float)
|
| 1021 |
+
parser.add_argument("--weight-decay", type=float)
|
| 1022 |
+
parser.add_argument("--lr-scheduler", choices=("cosine", "step"))
|
| 1023 |
+
parser.add_argument("--warmup-epochs", type=float)
|
| 1024 |
+
parser.add_argument("--precision", choices=("backend_mixed", "fp32"))
|
| 1025 |
+
parser.add_argument(
|
| 1026 |
+
"--tf32",
|
| 1027 |
+
action=argparse.BooleanOptionalAction,
|
| 1028 |
+
default=None,
|
| 1029 |
+
)
|
| 1030 |
+
parser.add_argument(
|
| 1031 |
+
"--multi-scale",
|
| 1032 |
+
action=argparse.BooleanOptionalAction,
|
| 1033 |
+
default=None,
|
| 1034 |
+
)
|
| 1035 |
+
parser.add_argument(
|
| 1036 |
+
"--expanded-scales",
|
| 1037 |
+
action=argparse.BooleanOptionalAction,
|
| 1038 |
+
default=None,
|
| 1039 |
+
)
|
| 1040 |
+
return parser
|
| 1041 |
+
|
| 1042 |
+
|
| 1043 |
+
def main() -> None:
|
| 1044 |
+
train_detector(build_parser().parse_args())
|
| 1045 |
+
|
| 1046 |
+
|
| 1047 |
+
if __name__ == "__main__":
|
| 1048 |
+
main()
|
generation_config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"transformers_version": "4.49.0"
|
| 4 |
+
}
|
grounding.py
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Grounding targets and prediction resolution for Falcon region tokens.
|
| 2 |
+
|
| 3 |
+
Ground-truth masks are accepted only by :func:`match_proposals`, which is a
|
| 4 |
+
training/scoring diagnostic. Prediction resolvers consume only generated text
|
| 5 |
+
and the detector's already-selected proposals. This separation prevents a
|
| 6 |
+
ground-truth mask from changing the proposal set seen by the model.
|
| 7 |
+
|
| 8 |
+
The module intentionally keeps torch and scipy imports inside the matcher so
|
| 9 |
+
transport parsing and inference-result serialization remain lightweight.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import re
|
| 17 |
+
from collections.abc import Collection, Iterable, Mapping, Sequence
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from enum import Enum
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
SEG_MARKER = "<SEG>"
|
| 25 |
+
PANOPTIC_MARKER = "<PANOPTIC>"
|
| 26 |
+
TRANSPORT_VERSION = "falcon-region-transport/v1"
|
| 27 |
+
MATCHING_POLICY_VERSION = "falcon-grounding-match/cardinality-iou-v1"
|
| 28 |
+
_REGION = re.compile(r"region_([0-9]{3})\Z")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class GroundingTransportError(ValueError):
|
| 32 |
+
"""Raised when generated grounding transport is malformed or ambiguous."""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class GroundingResolutionError(ValueError):
|
| 36 |
+
"""Raised when valid transport cannot be resolved against detector output."""
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class UnmatchedGroundingError(RuntimeError):
|
| 40 |
+
"""Raised when a positive target lacks a complete detector assignment."""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class GroundingMatchStatus(str, Enum):
|
| 44 |
+
"""Exhaustive distinction between negatives and positive match outcomes."""
|
| 45 |
+
|
| 46 |
+
TRUE_NEGATIVE = "true_negative"
|
| 47 |
+
FULL_MATCH = "full_match"
|
| 48 |
+
PARTIAL_MATCH = "partial_match"
|
| 49 |
+
UNMATCHED_POSITIVE = "unmatched_positive"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass(frozen=True)
|
| 53 |
+
class GroundTruthInstance:
|
| 54 |
+
"""One immutable ground-truth instance supplied to the diagnostic matcher."""
|
| 55 |
+
|
| 56 |
+
instance_id: int | str
|
| 57 |
+
category_id: int
|
| 58 |
+
mask: Any
|
| 59 |
+
|
| 60 |
+
def __post_init__(self) -> None:
|
| 61 |
+
if isinstance(self.instance_id, bool) or not isinstance(self.instance_id, int | str):
|
| 62 |
+
raise TypeError("instance_id must be an integer or string")
|
| 63 |
+
if isinstance(self.instance_id, str) and not self.instance_id:
|
| 64 |
+
raise ValueError("instance_id must not be empty")
|
| 65 |
+
if isinstance(self.category_id, bool) or not isinstance(self.category_id, int):
|
| 66 |
+
raise TypeError("category_id must be an integer")
|
| 67 |
+
if self.category_id < 1:
|
| 68 |
+
raise ValueError("category_id must be positive")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@dataclass(frozen=True)
|
| 72 |
+
class ProposalMatch:
|
| 73 |
+
"""One threshold-qualified one-to-one GT/proposal assignment."""
|
| 74 |
+
|
| 75 |
+
gt_index: int
|
| 76 |
+
proposal_index: int
|
| 77 |
+
iou: float
|
| 78 |
+
instance_id: int | str
|
| 79 |
+
category_id: int
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@dataclass(frozen=True)
|
| 83 |
+
class GroundingMatch:
|
| 84 |
+
"""Complete matching diagnostic; never conflates a miss with a negative."""
|
| 85 |
+
|
| 86 |
+
status: GroundingMatchStatus
|
| 87 |
+
ground_truth_count: int
|
| 88 |
+
proposal_count: int
|
| 89 |
+
matches: tuple[ProposalMatch, ...]
|
| 90 |
+
unmatched_gt_indices: tuple[int, ...]
|
| 91 |
+
unmatched_proposal_indices: tuple[int, ...]
|
| 92 |
+
policy_version: str = MATCHING_POLICY_VERSION
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def region_indices(self) -> tuple[int, ...]:
|
| 96 |
+
"""Matched proposal indices in stable proposal order."""
|
| 97 |
+
|
| 98 |
+
return tuple(sorted(item.proposal_index for item in self.matches))
|
| 99 |
+
|
| 100 |
+
def require_transportable(self) -> None:
|
| 101 |
+
"""Reject incomplete positive supervision before answer construction."""
|
| 102 |
+
|
| 103 |
+
if self.status in (
|
| 104 |
+
GroundingMatchStatus.PARTIAL_MATCH,
|
| 105 |
+
GroundingMatchStatus.UNMATCHED_POSITIVE,
|
| 106 |
+
):
|
| 107 |
+
raise UnmatchedGroundingError(
|
| 108 |
+
f"cannot encode {self.status.value}: "
|
| 109 |
+
f"{len(self.unmatched_gt_indices)} of {self.ground_truth_count} targets unmatched"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@dataclass(frozen=True)
|
| 114 |
+
class SegmentationTransport:
|
| 115 |
+
"""Parsed SEG transport. An empty tuple is an explicit valid negative."""
|
| 116 |
+
|
| 117 |
+
region_indices: tuple[int, ...]
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@dataclass(frozen=True)
|
| 121 |
+
class PanopticTransportInstance:
|
| 122 |
+
region_index: int
|
| 123 |
+
category_id: int
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@dataclass(frozen=True)
|
| 127 |
+
class PanopticTransport:
|
| 128 |
+
instances: tuple[PanopticTransportInstance, ...]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@dataclass(frozen=True)
|
| 132 |
+
class SegmentationPrediction:
|
| 133 |
+
"""Resolved detector masks in generated region-reference order."""
|
| 134 |
+
|
| 135 |
+
region_indices: tuple[int, ...]
|
| 136 |
+
masks: np.ndarray
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@dataclass(frozen=True)
|
| 140 |
+
class PanopticInstance:
|
| 141 |
+
"""One generated class assignment resolved to a live detector proposal."""
|
| 142 |
+
|
| 143 |
+
region_index: int
|
| 144 |
+
category_id: int
|
| 145 |
+
score: float
|
| 146 |
+
box_xyxy: tuple[float, float, float, float]
|
| 147 |
+
mask: np.ndarray
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@dataclass(frozen=True)
|
| 151 |
+
class PanopticPrediction:
|
| 152 |
+
"""Instance predictions and their deterministic non-overlapping raster."""
|
| 153 |
+
|
| 154 |
+
instances: tuple[PanopticInstance, ...]
|
| 155 |
+
id_map: np.ndarray
|
| 156 |
+
segments_info: tuple[dict[str, Any], ...]
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _validate_max_regions(max_regions: int) -> None:
|
| 160 |
+
if isinstance(max_regions, bool) or not isinstance(max_regions, int) or max_regions < 1:
|
| 161 |
+
raise ValueError("max_regions must be a positive integer")
|
| 162 |
+
if max_regions > 1000:
|
| 163 |
+
raise ValueError("v1 region transport supports at most 1000 regions")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _validate_region_index(index: Any, max_regions: int) -> int:
|
| 167 |
+
_validate_max_regions(max_regions)
|
| 168 |
+
if isinstance(index, bool) or not isinstance(index, int):
|
| 169 |
+
raise GroundingTransportError("region index must be an integer")
|
| 170 |
+
if not 0 <= index < max_regions:
|
| 171 |
+
raise GroundingTransportError(
|
| 172 |
+
f"region index {index} lies outside [0, {max_regions})"
|
| 173 |
+
)
|
| 174 |
+
return index
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _region_name(index: Any, max_regions: int) -> str:
|
| 178 |
+
return f"region_{_validate_region_index(index, max_regions):03d}"
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _parse_region_name(value: Any, max_regions: int) -> int:
|
| 182 |
+
if not isinstance(value, str):
|
| 183 |
+
raise GroundingTransportError("region reference must be a string")
|
| 184 |
+
match = _REGION.fullmatch(value)
|
| 185 |
+
if match is None:
|
| 186 |
+
raise GroundingTransportError(f"invalid region reference {value!r}")
|
| 187 |
+
return _validate_region_index(int(match.group(1)), max_regions)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _payload(text: str, marker: str) -> Mapping[str, Any]:
|
| 191 |
+
if not isinstance(text, str):
|
| 192 |
+
raise GroundingTransportError("grounding transport must be text")
|
| 193 |
+
stripped = text.strip()
|
| 194 |
+
if not stripped.startswith(marker):
|
| 195 |
+
raise GroundingTransportError(f"prediction must begin with {marker}")
|
| 196 |
+
encoded = stripped[len(marker) :]
|
| 197 |
+
if not encoded:
|
| 198 |
+
raise GroundingTransportError(f"bare {marker} is not valid transport")
|
| 199 |
+
try:
|
| 200 |
+
value = json.loads(encoded)
|
| 201 |
+
except json.JSONDecodeError as exc:
|
| 202 |
+
raise GroundingTransportError(f"invalid JSON after {marker}") from exc
|
| 203 |
+
if not isinstance(value, Mapping):
|
| 204 |
+
raise GroundingTransportError(f"{marker} payload must be an object")
|
| 205 |
+
return value
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def encode_seg_transport(
|
| 209 |
+
region_indices: Iterable[int],
|
| 210 |
+
*,
|
| 211 |
+
max_regions: int = 100,
|
| 212 |
+
) -> str:
|
| 213 |
+
"""Encode strict SEG transport; an empty iterable is a valid negative."""
|
| 214 |
+
|
| 215 |
+
indices = tuple(_validate_region_index(index, max_regions) for index in region_indices)
|
| 216 |
+
if len(indices) != len(set(indices)):
|
| 217 |
+
raise GroundingTransportError("SEG transport contains duplicate regions")
|
| 218 |
+
payload = {"regions": [_region_name(index, max_regions) for index in indices]}
|
| 219 |
+
return SEG_MARKER + json.dumps(payload, separators=(",", ":"))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def parse_seg_transport(text: str, *, max_regions: int = 100) -> SegmentationTransport:
|
| 223 |
+
"""Parse exact SEG transport, rejecting a bare marker and extra fields."""
|
| 224 |
+
|
| 225 |
+
value = _payload(text, SEG_MARKER)
|
| 226 |
+
if set(value) != {"regions"}:
|
| 227 |
+
raise GroundingTransportError("SEG payload must contain only 'regions'")
|
| 228 |
+
raw = value["regions"]
|
| 229 |
+
if isinstance(raw, str | bytes) or not isinstance(raw, Sequence):
|
| 230 |
+
raise GroundingTransportError("SEG regions must be an array")
|
| 231 |
+
indices = tuple(_parse_region_name(item, max_regions) for item in raw)
|
| 232 |
+
if len(indices) != len(set(indices)):
|
| 233 |
+
raise GroundingTransportError("SEG transport contains duplicate regions")
|
| 234 |
+
return SegmentationTransport(indices)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _category_set(category_ids: Collection[int]) -> frozenset[int]:
|
| 238 |
+
categories = frozenset(category_ids)
|
| 239 |
+
if not categories:
|
| 240 |
+
raise ValueError("category_ids must not be empty")
|
| 241 |
+
if any(isinstance(item, bool) or not isinstance(item, int) or item < 1 for item in categories):
|
| 242 |
+
raise ValueError("category_ids must contain positive integers")
|
| 243 |
+
return categories
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def encode_panoptic_transport(
|
| 247 |
+
instances: Iterable[PanopticTransportInstance],
|
| 248 |
+
*,
|
| 249 |
+
category_ids: Collection[int],
|
| 250 |
+
max_regions: int = 100,
|
| 251 |
+
) -> str:
|
| 252 |
+
"""Encode region/category pairs without introducing detector class logits."""
|
| 253 |
+
|
| 254 |
+
allowed = _category_set(category_ids)
|
| 255 |
+
values = tuple(instances)
|
| 256 |
+
regions: set[int] = set()
|
| 257 |
+
encoded = []
|
| 258 |
+
for item in values:
|
| 259 |
+
if not isinstance(item, PanopticTransportInstance):
|
| 260 |
+
raise TypeError("instances must contain PanopticTransportInstance values")
|
| 261 |
+
region_index = _validate_region_index(item.region_index, max_regions)
|
| 262 |
+
if region_index in regions:
|
| 263 |
+
raise GroundingTransportError("PANOPTIC transport contains duplicate regions")
|
| 264 |
+
regions.add(region_index)
|
| 265 |
+
if item.category_id not in allowed:
|
| 266 |
+
raise GroundingTransportError(f"unknown category_id {item.category_id}")
|
| 267 |
+
encoded.append(
|
| 268 |
+
{"region": _region_name(region_index, max_regions), "category_id": item.category_id}
|
| 269 |
+
)
|
| 270 |
+
payload = {"instances": encoded}
|
| 271 |
+
return PANOPTIC_MARKER + json.dumps(payload, separators=(",", ":"))
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def parse_panoptic_transport(
|
| 275 |
+
text: str,
|
| 276 |
+
*,
|
| 277 |
+
category_ids: Collection[int],
|
| 278 |
+
max_regions: int = 100,
|
| 279 |
+
) -> PanopticTransport:
|
| 280 |
+
"""Parse exact PANOPTIC transport using manifest-provided categories."""
|
| 281 |
+
|
| 282 |
+
allowed = _category_set(category_ids)
|
| 283 |
+
value = _payload(text, PANOPTIC_MARKER)
|
| 284 |
+
if set(value) != {"instances"}:
|
| 285 |
+
raise GroundingTransportError("PANOPTIC payload must contain only 'instances'")
|
| 286 |
+
raw = value["instances"]
|
| 287 |
+
if isinstance(raw, str | bytes) or not isinstance(raw, Sequence):
|
| 288 |
+
raise GroundingTransportError("PANOPTIC instances must be an array")
|
| 289 |
+
|
| 290 |
+
instances = []
|
| 291 |
+
regions: set[int] = set()
|
| 292 |
+
for position, entry in enumerate(raw):
|
| 293 |
+
if not isinstance(entry, Mapping) or set(entry) != {"region", "category_id"}:
|
| 294 |
+
raise GroundingTransportError(
|
| 295 |
+
f"PANOPTIC instances[{position}] must contain region and category_id"
|
| 296 |
+
)
|
| 297 |
+
region_index = _parse_region_name(entry["region"], max_regions)
|
| 298 |
+
if region_index in regions:
|
| 299 |
+
raise GroundingTransportError("PANOPTIC transport contains duplicate regions")
|
| 300 |
+
regions.add(region_index)
|
| 301 |
+
category_id = entry["category_id"]
|
| 302 |
+
if isinstance(category_id, bool) or not isinstance(category_id, int):
|
| 303 |
+
raise GroundingTransportError("PANOPTIC category_id must be an integer")
|
| 304 |
+
if category_id not in allowed:
|
| 305 |
+
raise GroundingTransportError(f"unknown category_id {category_id}")
|
| 306 |
+
instances.append(PanopticTransportInstance(region_index, category_id))
|
| 307 |
+
return PanopticTransport(tuple(instances))
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _binary_cpu_tensor(value: Any, name: str, *, dimensions: int) -> Any:
|
| 311 |
+
import torch
|
| 312 |
+
|
| 313 |
+
tensor = value.detach().to("cpu") if isinstance(value, torch.Tensor) else torch.as_tensor(value)
|
| 314 |
+
if tensor.ndim != dimensions:
|
| 315 |
+
raise ValueError(f"{name} must have {dimensions} dimensions")
|
| 316 |
+
if tensor.dtype != torch.bool:
|
| 317 |
+
if not torch.isfinite(tensor).all():
|
| 318 |
+
raise ValueError(f"{name} contains non-finite values")
|
| 319 |
+
if not torch.logical_or(tensor == 0, tensor == 1).all():
|
| 320 |
+
raise ValueError(f"{name} must be binary")
|
| 321 |
+
tensor = tensor.to(dtype=torch.bool)
|
| 322 |
+
return tensor
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _qualified_hungarian_assignment(
|
| 326 |
+
overlaps: np.ndarray,
|
| 327 |
+
threshold: float,
|
| 328 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 329 |
+
"""Maximize qualified match count, then total IoU among equal-cardinality plans."""
|
| 330 |
+
|
| 331 |
+
from scipy.optimize import linear_sum_assignment
|
| 332 |
+
|
| 333 |
+
if overlaps.ndim != 2:
|
| 334 |
+
raise ValueError("overlaps must be a two-dimensional matrix")
|
| 335 |
+
assignment_size = min(overlaps.shape)
|
| 336 |
+
# A cardinality bonus larger than the greatest possible aggregate IoU
|
| 337 |
+
# difference makes a threshold-qualified edge the primary objective.
|
| 338 |
+
reward = (overlaps >= threshold) * (assignment_size + 1.0) + overlaps
|
| 339 |
+
return linear_sum_assignment(-reward)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def match_proposals(
|
| 343 |
+
ground_truth: Sequence[GroundTruthInstance],
|
| 344 |
+
proposal_masks: Any,
|
| 345 |
+
*,
|
| 346 |
+
iou_threshold: float = 0.5,
|
| 347 |
+
) -> GroundingMatch:
|
| 348 |
+
"""Hungarian mask-IoU diagnostic over an unchanged proposal set.
|
| 349 |
+
|
| 350 |
+
The returned status explicitly distinguishes an annotation-level negative
|
| 351 |
+
from a detector miss. No target box or mask is ever inserted into the
|
| 352 |
+
proposal set.
|
| 353 |
+
"""
|
| 354 |
+
|
| 355 |
+
import torch
|
| 356 |
+
if isinstance(iou_threshold, bool) or not isinstance(iou_threshold, int | float):
|
| 357 |
+
raise TypeError("iou_threshold must be numeric")
|
| 358 |
+
threshold = float(iou_threshold)
|
| 359 |
+
if not math.isfinite(threshold) or not 0.0 <= threshold <= 1.0:
|
| 360 |
+
raise ValueError("iou_threshold must lie in [0, 1]")
|
| 361 |
+
if any(not isinstance(item, GroundTruthInstance) for item in ground_truth):
|
| 362 |
+
raise TypeError("ground_truth must contain GroundTruthInstance values")
|
| 363 |
+
|
| 364 |
+
proposals = _binary_cpu_tensor(proposal_masks, "proposal_masks", dimensions=3)
|
| 365 |
+
proposal_count = int(proposals.shape[0])
|
| 366 |
+
ground_truth_count = len(ground_truth)
|
| 367 |
+
if ground_truth_count == 0:
|
| 368 |
+
return GroundingMatch(
|
| 369 |
+
GroundingMatchStatus.TRUE_NEGATIVE,
|
| 370 |
+
0,
|
| 371 |
+
proposal_count,
|
| 372 |
+
(),
|
| 373 |
+
(),
|
| 374 |
+
tuple(range(proposal_count)),
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
targets = torch.stack(
|
| 378 |
+
[_binary_cpu_tensor(item.mask, "ground-truth mask", dimensions=2) for item in ground_truth]
|
| 379 |
+
)
|
| 380 |
+
if tuple(targets.shape[-2:]) != tuple(proposals.shape[-2:]):
|
| 381 |
+
raise ValueError("ground-truth and proposal masks must share spatial dimensions")
|
| 382 |
+
if not targets.flatten(1).any(dim=1).all():
|
| 383 |
+
raise ValueError("ground-truth instance masks must not be empty")
|
| 384 |
+
if proposal_count == 0:
|
| 385 |
+
return GroundingMatch(
|
| 386 |
+
GroundingMatchStatus.UNMATCHED_POSITIVE,
|
| 387 |
+
ground_truth_count,
|
| 388 |
+
0,
|
| 389 |
+
(),
|
| 390 |
+
tuple(range(ground_truth_count)),
|
| 391 |
+
(),
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
intersections = torch.logical_and(targets[:, None], proposals[None]).sum(dim=(-2, -1))
|
| 395 |
+
unions = torch.logical_or(targets[:, None], proposals[None]).sum(dim=(-2, -1))
|
| 396 |
+
overlaps = intersections.to(torch.float64) / unions.clamp_min(1).to(torch.float64)
|
| 397 |
+
rows, columns = _qualified_hungarian_assignment(overlaps.numpy(), threshold)
|
| 398 |
+
|
| 399 |
+
matches = []
|
| 400 |
+
for row, column in zip(rows.tolist(), columns.tolist(), strict=True):
|
| 401 |
+
iou = float(overlaps[row, column])
|
| 402 |
+
if iou >= threshold:
|
| 403 |
+
target = ground_truth[row]
|
| 404 |
+
matches.append(
|
| 405 |
+
ProposalMatch(row, column, iou, target.instance_id, target.category_id)
|
| 406 |
+
)
|
| 407 |
+
matches.sort(key=lambda item: item.gt_index)
|
| 408 |
+
matched_gt = {item.gt_index for item in matches}
|
| 409 |
+
matched_proposals = {item.proposal_index for item in matches}
|
| 410 |
+
unmatched_gt = tuple(index for index in range(ground_truth_count) if index not in matched_gt)
|
| 411 |
+
unmatched_proposals = tuple(
|
| 412 |
+
index for index in range(proposal_count) if index not in matched_proposals
|
| 413 |
+
)
|
| 414 |
+
if not matches:
|
| 415 |
+
status = GroundingMatchStatus.UNMATCHED_POSITIVE
|
| 416 |
+
elif unmatched_gt:
|
| 417 |
+
status = GroundingMatchStatus.PARTIAL_MATCH
|
| 418 |
+
else:
|
| 419 |
+
status = GroundingMatchStatus.FULL_MATCH
|
| 420 |
+
return GroundingMatch(
|
| 421 |
+
status,
|
| 422 |
+
ground_truth_count,
|
| 423 |
+
proposal_count,
|
| 424 |
+
tuple(matches),
|
| 425 |
+
unmatched_gt,
|
| 426 |
+
unmatched_proposals,
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def encode_segmentation_match(match: GroundingMatch, *, max_regions: int = 100) -> str:
|
| 431 |
+
"""Encode only a true negative or fully matched positive."""
|
| 432 |
+
|
| 433 |
+
match.require_transportable()
|
| 434 |
+
return encode_seg_transport(match.region_indices, max_regions=max_regions)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def encode_panoptic_match(
|
| 438 |
+
match: GroundingMatch,
|
| 439 |
+
*,
|
| 440 |
+
category_ids: Collection[int],
|
| 441 |
+
max_regions: int = 100,
|
| 442 |
+
) -> str:
|
| 443 |
+
"""Encode category assignments only after a complete diagnostic match."""
|
| 444 |
+
|
| 445 |
+
match.require_transportable()
|
| 446 |
+
instances = tuple(
|
| 447 |
+
PanopticTransportInstance(item.proposal_index, item.category_id)
|
| 448 |
+
for item in sorted(match.matches, key=lambda item: item.proposal_index)
|
| 449 |
+
)
|
| 450 |
+
return encode_panoptic_transport(
|
| 451 |
+
instances,
|
| 452 |
+
category_ids=category_ids,
|
| 453 |
+
max_regions=max_regions,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def _field(value: Any, name: str) -> Any:
|
| 458 |
+
return value.get(name) if isinstance(value, Mapping) else getattr(value, name, None)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _to_numpy(value: Any) -> np.ndarray:
|
| 462 |
+
if hasattr(value, "detach"):
|
| 463 |
+
value = value.detach()
|
| 464 |
+
if hasattr(value, "cpu"):
|
| 465 |
+
value = value.cpu()
|
| 466 |
+
if hasattr(value, "numpy"):
|
| 467 |
+
value = value.numpy()
|
| 468 |
+
return np.asarray(value)
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def _detector_arrays(
|
| 472 |
+
detector_output: Any,
|
| 473 |
+
*,
|
| 474 |
+
batch_index: int = 0,
|
| 475 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, int, int]:
|
| 476 |
+
"""Return one detector image without changing proposal indices.
|
| 477 |
+
|
| 478 |
+
Both the public unbatched :class:`DetectorOutput` and the model's padded
|
| 479 |
+
``DetectorBatch`` are accepted. A padded batch must use a contiguous valid
|
| 480 |
+
prefix: otherwise compressing it here would silently renumber the region
|
| 481 |
+
tokens generated by the language model.
|
| 482 |
+
"""
|
| 483 |
+
|
| 484 |
+
if isinstance(batch_index, bool) or not isinstance(batch_index, int) or batch_index < 0:
|
| 485 |
+
raise GroundingResolutionError("batch_index must be a nonnegative integer")
|
| 486 |
+
boxes_value = _field(detector_output, "boxes_xyxy")
|
| 487 |
+
if boxes_value is None:
|
| 488 |
+
boxes_value = _field(detector_output, "boxes")
|
| 489 |
+
scores_value = _field(detector_output, "scores")
|
| 490 |
+
masks_value = _field(detector_output, "masks")
|
| 491 |
+
original_hw = _field(detector_output, "original_hw")
|
| 492 |
+
if original_hw is None:
|
| 493 |
+
original_hw = _field(detector_output, "image_sizes")
|
| 494 |
+
mask_sizes = _field(detector_output, "mask_sizes")
|
| 495 |
+
valid_value = _field(detector_output, "valid")
|
| 496 |
+
if any(item is None for item in (boxes_value, scores_value, masks_value, original_hw)):
|
| 497 |
+
raise GroundingResolutionError(
|
| 498 |
+
"detector output must provide boxes, scores, masks, and original image sizes"
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
boxes = _to_numpy(boxes_value)
|
| 502 |
+
scores = _to_numpy(scores_value)
|
| 503 |
+
masks = _to_numpy(masks_value)
|
| 504 |
+
original_sizes = _to_numpy(original_hw)
|
| 505 |
+
if boxes.ndim == 3:
|
| 506 |
+
batch_size, proposal_count = boxes.shape[:2]
|
| 507 |
+
if batch_index >= boxes.shape[0]:
|
| 508 |
+
raise GroundingResolutionError("batch_index is outside the detector batch")
|
| 509 |
+
if scores.ndim != 2 or scores.shape != boxes.shape[:2]:
|
| 510 |
+
raise GroundingResolutionError("batched detector scores must have shape [B, N]")
|
| 511 |
+
if masks.ndim != 4 or masks.shape[:2] != boxes.shape[:2]:
|
| 512 |
+
raise GroundingResolutionError("batched detector masks must have shape [B, N, H, W]")
|
| 513 |
+
if original_sizes.shape != (boxes.shape[0], 2):
|
| 514 |
+
raise GroundingResolutionError("image_sizes must have shape [B, 2]")
|
| 515 |
+
boxes = boxes[batch_index]
|
| 516 |
+
scores = scores[batch_index]
|
| 517 |
+
masks = masks[batch_index]
|
| 518 |
+
original_sizes = original_sizes[batch_index]
|
| 519 |
+
if mask_sizes is not None:
|
| 520 |
+
sizes = _to_numpy(mask_sizes)
|
| 521 |
+
if sizes.shape != (batch_size, 2):
|
| 522 |
+
raise GroundingResolutionError("mask_sizes must have shape [B, 2]")
|
| 523 |
+
mask_height, mask_width = (int(value) for value in sizes[batch_index])
|
| 524 |
+
if mask_height < 1 or mask_width < 1 or (
|
| 525 |
+
mask_height > masks.shape[-2] or mask_width > masks.shape[-1]
|
| 526 |
+
):
|
| 527 |
+
raise GroundingResolutionError("mask_sizes exceed the padded mask canvas")
|
| 528 |
+
masks = masks[:, :mask_height, :mask_width]
|
| 529 |
+
if valid_value is not None:
|
| 530 |
+
valid = _to_numpy(valid_value)
|
| 531 |
+
if valid.shape != (batch_size, proposal_count):
|
| 532 |
+
raise GroundingResolutionError("detector valid mask must have shape [B, N]")
|
| 533 |
+
valid = valid[batch_index].astype(bool)
|
| 534 |
+
count = int(np.count_nonzero(valid))
|
| 535 |
+
if not np.array_equal(valid, np.arange(len(valid)) < count):
|
| 536 |
+
raise GroundingResolutionError(
|
| 537 |
+
"detector valid proposals must form a contiguous prefix"
|
| 538 |
+
)
|
| 539 |
+
boxes, scores, masks = boxes[:count], scores[:count], masks[:count]
|
| 540 |
+
elif batch_index != 0:
|
| 541 |
+
raise GroundingResolutionError("batch_index is outside the unbatched detector output")
|
| 542 |
+
|
| 543 |
+
if boxes.ndim != 2 or boxes.shape[-1] != 4:
|
| 544 |
+
raise GroundingResolutionError("detector boxes must have shape [proposals, 4]")
|
| 545 |
+
if scores.ndim != 1 or scores.shape[0] != boxes.shape[0]:
|
| 546 |
+
raise GroundingResolutionError("detector scores must have shape [proposals]")
|
| 547 |
+
if masks.ndim != 3 or masks.shape[0] != boxes.shape[0]:
|
| 548 |
+
raise GroundingResolutionError("detector masks must have shape [proposals, H, W]")
|
| 549 |
+
if not np.isfinite(boxes).all() or not np.isfinite(scores).all():
|
| 550 |
+
raise GroundingResolutionError("selected detector boxes/scores must be finite")
|
| 551 |
+
if masks.dtype != np.bool_:
|
| 552 |
+
if not np.isfinite(masks).all() or not np.logical_or(masks == 0, masks == 1).all():
|
| 553 |
+
raise GroundingResolutionError("selected detector masks must be binary")
|
| 554 |
+
masks = masks.astype(bool)
|
| 555 |
+
try:
|
| 556 |
+
height, width = (int(value) for value in original_sizes)
|
| 557 |
+
except (TypeError, ValueError) as exc:
|
| 558 |
+
raise GroundingResolutionError("original image size must contain height and width") from exc
|
| 559 |
+
if height < 1 or width < 1 or masks.shape[-2:] != (height, width):
|
| 560 |
+
raise GroundingResolutionError("detector masks do not match positive original_hw")
|
| 561 |
+
return boxes.astype(np.float32), scores.astype(np.float32), masks, height, width
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def resolve_segmentation(
|
| 565 |
+
text: str,
|
| 566 |
+
detector_output: Any,
|
| 567 |
+
*,
|
| 568 |
+
max_regions: int = 100,
|
| 569 |
+
batch_index: int = 0,
|
| 570 |
+
) -> SegmentationPrediction:
|
| 571 |
+
"""Resolve generated SEG references without any ground-truth input."""
|
| 572 |
+
|
| 573 |
+
transport = parse_seg_transport(text, max_regions=max_regions)
|
| 574 |
+
_boxes, _scores, masks, height, width = _detector_arrays(
|
| 575 |
+
detector_output,
|
| 576 |
+
batch_index=batch_index,
|
| 577 |
+
)
|
| 578 |
+
for index in transport.region_indices:
|
| 579 |
+
if index >= len(masks):
|
| 580 |
+
raise GroundingResolutionError(f"generated region_{index:03d} was not proposed")
|
| 581 |
+
if not masks[index].any():
|
| 582 |
+
raise GroundingResolutionError(f"generated region_{index:03d} has an empty mask")
|
| 583 |
+
selected = (
|
| 584 |
+
np.stack([masks[index] for index in transport.region_indices]).astype(bool, copy=True)
|
| 585 |
+
if transport.region_indices
|
| 586 |
+
else np.empty((0, height, width), dtype=bool)
|
| 587 |
+
)
|
| 588 |
+
return SegmentationPrediction(transport.region_indices, selected)
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
def _mask_bbox(mask: np.ndarray) -> list[int]:
|
| 592 |
+
rows, columns = np.nonzero(mask)
|
| 593 |
+
x_min, x_max = int(columns.min()), int(columns.max())
|
| 594 |
+
y_min, y_max = int(rows.min()), int(rows.max())
|
| 595 |
+
return [x_min, y_min, x_max - x_min + 1, y_max - y_min + 1]
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def resolve_panoptic(
|
| 599 |
+
text: str,
|
| 600 |
+
detector_output: Any,
|
| 601 |
+
*,
|
| 602 |
+
category_ids: Collection[int],
|
| 603 |
+
max_regions: int = 100,
|
| 604 |
+
batch_index: int = 0,
|
| 605 |
+
) -> PanopticPrediction:
|
| 606 |
+
"""Resolve generated classes and live proposal masks into panoptic output."""
|
| 607 |
+
|
| 608 |
+
transport = parse_panoptic_transport(
|
| 609 |
+
text,
|
| 610 |
+
category_ids=category_ids,
|
| 611 |
+
max_regions=max_regions,
|
| 612 |
+
)
|
| 613 |
+
boxes, scores, masks, height, width = _detector_arrays(
|
| 614 |
+
detector_output,
|
| 615 |
+
batch_index=batch_index,
|
| 616 |
+
)
|
| 617 |
+
instances = []
|
| 618 |
+
for item in transport.instances:
|
| 619 |
+
index = item.region_index
|
| 620 |
+
if index >= len(masks):
|
| 621 |
+
raise GroundingResolutionError(f"generated region_{index:03d} was not proposed")
|
| 622 |
+
if not masks[index].any():
|
| 623 |
+
raise GroundingResolutionError(f"generated region_{index:03d} has an empty mask")
|
| 624 |
+
instances.append(
|
| 625 |
+
PanopticInstance(
|
| 626 |
+
region_index=index,
|
| 627 |
+
category_id=item.category_id,
|
| 628 |
+
score=float(scores[index]),
|
| 629 |
+
box_xyxy=tuple(float(value) for value in boxes[index]),
|
| 630 |
+
mask=masks[index].astype(bool, copy=True),
|
| 631 |
+
)
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
priority = sorted(instances, key=lambda item: (-item.score, item.region_index))
|
| 635 |
+
id_map = np.zeros((height, width), dtype=np.int32)
|
| 636 |
+
segments = []
|
| 637 |
+
for item in priority:
|
| 638 |
+
visible = np.logical_and(item.mask, id_map == 0)
|
| 639 |
+
if not visible.any():
|
| 640 |
+
continue
|
| 641 |
+
segment_id = item.region_index + 1
|
| 642 |
+
id_map[visible] = segment_id
|
| 643 |
+
segments.append(
|
| 644 |
+
{
|
| 645 |
+
"id": segment_id,
|
| 646 |
+
"category_id": item.category_id,
|
| 647 |
+
"iscrowd": 0,
|
| 648 |
+
"area": int(visible.sum()),
|
| 649 |
+
"bbox": _mask_bbox(visible),
|
| 650 |
+
}
|
| 651 |
+
)
|
| 652 |
+
segments.sort(key=lambda item: item["id"])
|
| 653 |
+
return PanopticPrediction(tuple(instances), id_map, tuple(segments))
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def select_panoptic_category(
|
| 657 |
+
prediction: PanopticPrediction, category_id: int
|
| 658 |
+
) -> PanopticPrediction:
|
| 659 |
+
"""Select a query category using predicted classes, retaining instance IDs."""
|
| 660 |
+
|
| 661 |
+
if isinstance(category_id, bool) or not isinstance(category_id, int) or category_id < 1:
|
| 662 |
+
raise ValueError("query category_id must be a positive integer")
|
| 663 |
+
segments = tuple(
|
| 664 |
+
segment for segment in prediction.segments_info if segment["category_id"] == category_id
|
| 665 |
+
)
|
| 666 |
+
identifiers = [segment["id"] for segment in segments]
|
| 667 |
+
selected = np.isin(prediction.id_map, identifiers)
|
| 668 |
+
return PanopticPrediction(
|
| 669 |
+
tuple(item for item in prediction.instances if item.category_id == category_id),
|
| 670 |
+
np.where(selected, prediction.id_map, 0),
|
| 671 |
+
segments,
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def panoptic_id_map_to_rgb(id_map: Any) -> np.ndarray:
|
| 676 |
+
"""Encode a nonnegative integer segment-ID map as lossless COCO RGB pixels."""
|
| 677 |
+
|
| 678 |
+
values = _to_numpy(id_map)
|
| 679 |
+
if values.ndim != 2 or not np.issubdtype(values.dtype, np.integer):
|
| 680 |
+
raise ValueError("id_map must be a two-dimensional integer array")
|
| 681 |
+
if np.any(values < 0) or np.any(values > 0xFFFFFF):
|
| 682 |
+
raise ValueError("panoptic IDs must lie in [0, 16777215]")
|
| 683 |
+
unsigned = values.astype(np.uint32, copy=False)
|
| 684 |
+
return np.stack(
|
| 685 |
+
(
|
| 686 |
+
unsigned % 256,
|
| 687 |
+
(unsigned // 256) % 256,
|
| 688 |
+
(unsigned // 65536) % 256,
|
| 689 |
+
),
|
| 690 |
+
axis=-1,
|
| 691 |
+
).astype(np.uint8)
|
model-00001-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f24d222c5e8058ec64cde5553df8d507b276a009269e058953d6341fb53ca1bd
|
| 3 |
+
size 3989033704
|
model-00002-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c309e09cb581b0a5a1f7025426c8a4126e49c1eadbdc127995b79cedba7fef06
|
| 3 |
+
size 3910313088
|
model-00003-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a355618af4d84e2ad3e06bfa87d5ce12c44cdd2d888b673e26f1c929c1bd086c
|
| 3 |
+
size 3978488256
|
model-00004-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9fd406feb1c2f96644553b25f844d2e77bf3675912a206adf468177fc839c9e0
|
| 3 |
+
size 3135479168
|
model.py
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Clean Falcon model core with live, injectable perception backends.
|
| 2 |
+
|
| 3 |
+
The module deliberately contains no dataset paths, cache paths, or legacy Groma
|
| 4 |
+
imports. A live detector can be attached for one-process inference; tests and
|
| 5 |
+
training may pass a same-process in-memory :class:`DetectorBatch` to avoid
|
| 6 |
+
rerunning a frozen detector for every task on one image.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import math
|
| 12 |
+
import re
|
| 13 |
+
from collections.abc import Mapping, Sequence
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from torch import Tensor, nn
|
| 19 |
+
|
| 20 |
+
from .capabilities import ALL_SAFETY_HEADS, SafetyCapabilities
|
| 21 |
+
from .regions import MaskAwareRegionEncoder, PatchTokenAggregator
|
| 22 |
+
from .safety import SafetyOutput, StructuredSafetyAdapter
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass(frozen=True)
|
| 26 |
+
class FalconConfig:
|
| 27 |
+
vision_dim: int
|
| 28 |
+
language_dim: int
|
| 29 |
+
region_dim: int = 1024
|
| 30 |
+
image_size: int = 448
|
| 31 |
+
patch_size: int = 14
|
| 32 |
+
vision_prefix_tokens: int = 1
|
| 33 |
+
roi_size: int = 4
|
| 34 |
+
max_regions: int = 100
|
| 35 |
+
max_text_tokens: int = 256
|
| 36 |
+
text_overflow_policy: str = "error"
|
| 37 |
+
mask_probability_threshold: float = 0.5
|
| 38 |
+
risk_loss_weight: float = 1.0
|
| 39 |
+
presence_loss_weight: float = 1.0
|
| 40 |
+
link_loss_weight: float = 1.0
|
| 41 |
+
safety_capabilities: SafetyCapabilities = SafetyCapabilities()
|
| 42 |
+
|
| 43 |
+
def __post_init__(self) -> None:
|
| 44 |
+
if self.vision_dim < 1 or self.language_dim < 1 or self.region_dim < 1:
|
| 45 |
+
raise ValueError("model dimensions must be positive")
|
| 46 |
+
if self.image_size < 1 or self.patch_size < 1 or self.roi_size < 1:
|
| 47 |
+
raise ValueError("image_size, patch_size, and roi_size must be positive")
|
| 48 |
+
if self.image_size % (2 * self.patch_size):
|
| 49 |
+
raise ValueError("image_size must be divisible by twice patch_size")
|
| 50 |
+
if self.vision_prefix_tokens < 0:
|
| 51 |
+
raise ValueError("vision_prefix_tokens cannot be negative")
|
| 52 |
+
if self.max_regions < 1:
|
| 53 |
+
raise ValueError("max_regions must be positive")
|
| 54 |
+
if self.max_text_tokens < 3:
|
| 55 |
+
raise ValueError("max_text_tokens must allow a prompt token, answer token, and EOS")
|
| 56 |
+
if self.text_overflow_policy not in ("error", "truncate"):
|
| 57 |
+
raise ValueError("text_overflow_policy must be error or truncate")
|
| 58 |
+
if not 0.0 <= self.mask_probability_threshold <= 1.0:
|
| 59 |
+
raise ValueError("mask_probability_threshold must lie in [0, 1]")
|
| 60 |
+
for value in (
|
| 61 |
+
self.risk_loss_weight,
|
| 62 |
+
self.presence_loss_weight,
|
| 63 |
+
self.link_loss_weight,
|
| 64 |
+
):
|
| 65 |
+
if not math.isfinite(value) or value < 0:
|
| 66 |
+
raise ValueError("structured loss weights must be finite and nonnegative")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclass
|
| 70 |
+
class DetectorBatch:
|
| 71 |
+
"""Batched live-detector output.
|
| 72 |
+
|
| 73 |
+
Boxes are absolute ``xyxy`` coordinates in the original images. Masks have
|
| 74 |
+
shape ``[batch, proposals, height, width]`` and are explicitly declared as
|
| 75 |
+
logits or probabilities. ``valid`` marks padding; proposal order is owned by
|
| 76 |
+
the detector and must already be deterministic (normally score descending).
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
boxes: Tensor
|
| 80 |
+
masks: Tensor
|
| 81 |
+
valid: Tensor | None = None
|
| 82 |
+
scores: Tensor | None = None
|
| 83 |
+
image_sizes: Tensor | None = None
|
| 84 |
+
masks_are_logits: bool = False
|
| 85 |
+
mask_sizes: Tensor | None = None
|
| 86 |
+
|
| 87 |
+
def batched(self) -> DetectorBatch:
|
| 88 |
+
boxes = self.boxes.unsqueeze(0) if self.boxes.ndim == 2 else self.boxes
|
| 89 |
+
masks = self.masks.unsqueeze(0) if self.masks.ndim == 3 else self.masks
|
| 90 |
+
valid = self.valid
|
| 91 |
+
scores = self.scores
|
| 92 |
+
image_sizes = self.image_sizes
|
| 93 |
+
mask_sizes = self.mask_sizes
|
| 94 |
+
if valid is not None and valid.ndim == 1:
|
| 95 |
+
valid = valid.unsqueeze(0)
|
| 96 |
+
if scores is not None and scores.ndim == 1:
|
| 97 |
+
scores = scores.unsqueeze(0)
|
| 98 |
+
if image_sizes is not None and image_sizes.ndim == 1:
|
| 99 |
+
image_sizes = image_sizes.unsqueeze(0)
|
| 100 |
+
if mask_sizes is not None and mask_sizes.ndim == 1:
|
| 101 |
+
mask_sizes = mask_sizes.unsqueeze(0)
|
| 102 |
+
if boxes.ndim != 3 or boxes.shape[-1] != 4:
|
| 103 |
+
raise ValueError("detector boxes must have shape [batch, proposals, 4]")
|
| 104 |
+
if masks.ndim != 4 or masks.shape[:2] != boxes.shape[:2]:
|
| 105 |
+
raise ValueError("detector masks must have shape [batch, proposals, height, width]")
|
| 106 |
+
if valid is None:
|
| 107 |
+
valid = torch.ones(boxes.shape[:2], dtype=torch.bool, device=boxes.device)
|
| 108 |
+
elif valid.shape != boxes.shape[:2]:
|
| 109 |
+
raise ValueError("detector valid mask must have shape [batch, proposals]")
|
| 110 |
+
if scores is not None and scores.shape != boxes.shape[:2]:
|
| 111 |
+
raise ValueError("detector scores must have shape [batch, proposals]")
|
| 112 |
+
if image_sizes is not None and image_sizes.shape != (boxes.shape[0], 2):
|
| 113 |
+
raise ValueError("detector image_sizes must have shape [batch, 2]")
|
| 114 |
+
if mask_sizes is None:
|
| 115 |
+
mask_sizes = torch.tensor(
|
| 116 |
+
[list(masks.shape[-2:])] * boxes.shape[0], device=masks.device, dtype=torch.long
|
| 117 |
+
)
|
| 118 |
+
if mask_sizes.shape != (boxes.shape[0], 2) or bool((mask_sizes <= 0).any()):
|
| 119 |
+
raise ValueError("detector mask_sizes must have shape [batch, 2] and be positive")
|
| 120 |
+
if bool((mask_sizes > mask_sizes.new_tensor(masks.shape[-2:])).any()):
|
| 121 |
+
raise ValueError("mask_sizes cannot exceed the padded mask canvas")
|
| 122 |
+
return DetectorBatch(
|
| 123 |
+
boxes=boxes,
|
| 124 |
+
masks=masks,
|
| 125 |
+
valid=valid.to(dtype=torch.bool),
|
| 126 |
+
scores=scores,
|
| 127 |
+
image_sizes=image_sizes,
|
| 128 |
+
masks_are_logits=self.masks_are_logits,
|
| 129 |
+
mask_sizes=mask_sizes,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
def to(self, device: torch.device) -> DetectorBatch:
|
| 133 |
+
return DetectorBatch(
|
| 134 |
+
boxes=self.boxes.to(device),
|
| 135 |
+
masks=self.masks.to(device),
|
| 136 |
+
valid=None if self.valid is None else self.valid.to(device),
|
| 137 |
+
scores=None if self.scores is None else self.scores.to(device),
|
| 138 |
+
image_sizes=None if self.image_sizes is None else self.image_sizes.to(device),
|
| 139 |
+
masks_are_logits=self.masks_are_logits,
|
| 140 |
+
mask_sizes=None if self.mask_sizes is None else self.mask_sizes.to(device),
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
@classmethod
|
| 144 |
+
def pack(cls, outputs: Sequence[DetectorBatch]) -> DetectorBatch:
|
| 145 |
+
"""Pad proposals and canvases without resampling original instance masks."""
|
| 146 |
+
if not outputs:
|
| 147 |
+
raise ValueError("cannot pack an empty detector batch")
|
| 148 |
+
outputs = [output.batched() for output in outputs]
|
| 149 |
+
if any(output.boxes.shape[0] != 1 for output in outputs):
|
| 150 |
+
raise ValueError("pack expects single-image detector outputs")
|
| 151 |
+
if len(outputs) == 1:
|
| 152 |
+
return outputs[0]
|
| 153 |
+
first = outputs[0]
|
| 154 |
+
if any(output.masks_are_logits != first.masks_are_logits for output in outputs):
|
| 155 |
+
raise ValueError("all masks in a batch must use the same representation")
|
| 156 |
+
if any((output.scores is None) != (first.scores is None) for output in outputs):
|
| 157 |
+
raise ValueError("all outputs must consistently include proposal scores")
|
| 158 |
+
if any(output.image_sizes is None for output in outputs):
|
| 159 |
+
raise ValueError("original image sizes are required for a multi-image batch")
|
| 160 |
+
count = max(output.boxes.shape[1] for output in outputs)
|
| 161 |
+
height = max(output.masks.shape[-2] for output in outputs)
|
| 162 |
+
width = max(output.masks.shape[-1] for output in outputs)
|
| 163 |
+
boxes = first.boxes.new_zeros((len(outputs), count, 4))
|
| 164 |
+
masks = first.masks.new_zeros((len(outputs), count, height, width))
|
| 165 |
+
valid = torch.zeros((len(outputs), count), device=first.boxes.device, dtype=torch.bool)
|
| 166 |
+
scores = None if first.scores is None else first.scores.new_zeros((len(outputs), count))
|
| 167 |
+
for index, output in enumerate(outputs):
|
| 168 |
+
n, h, w = output.masks.shape[1:]
|
| 169 |
+
boxes[index, :n] = output.boxes[0].to(boxes)
|
| 170 |
+
masks[index, :n, :h, :w] = output.masks[0].to(masks)
|
| 171 |
+
valid[index, :n] = output.valid[0].to(valid)
|
| 172 |
+
if scores is not None:
|
| 173 |
+
scores[index, :n] = output.scores[0].to(scores)
|
| 174 |
+
return cls(
|
| 175 |
+
boxes,
|
| 176 |
+
masks,
|
| 177 |
+
valid,
|
| 178 |
+
scores,
|
| 179 |
+
torch.cat([output.image_sizes.to(boxes.device) for output in outputs]),
|
| 180 |
+
first.masks_are_logits,
|
| 181 |
+
torch.cat([output.mask_sizes.to(boxes.device) for output in outputs]),
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@dataclass
|
| 186 |
+
class VisionFeatures:
|
| 187 |
+
"""Frozen DINO patch tokens that may be reused for tasks on one image."""
|
| 188 |
+
|
| 189 |
+
patch_tokens: Tensor
|
| 190 |
+
grid_size: tuple[int, int]
|
| 191 |
+
|
| 192 |
+
def to(self, device: torch.device) -> VisionFeatures:
|
| 193 |
+
return VisionFeatures(self.patch_tokens.to(device), self.grid_size)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@dataclass
|
| 197 |
+
class PrefixBatch:
|
| 198 |
+
inputs_embeds: Tensor
|
| 199 |
+
attention_mask: Tensor
|
| 200 |
+
labels: Tensor | None
|
| 201 |
+
image_token_count: int
|
| 202 |
+
region_token_count: int
|
| 203 |
+
safety_token_count: int
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@dataclass
|
| 207 |
+
class FalconEncoding:
|
| 208 |
+
prefix: PrefixBatch
|
| 209 |
+
safety: SafetyOutput
|
| 210 |
+
region_embeddings: Tensor
|
| 211 |
+
detector_output: DetectorBatch
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@dataclass
|
| 215 |
+
class FalconOutput:
|
| 216 |
+
loss: Tensor | None
|
| 217 |
+
language_loss: Tensor | None
|
| 218 |
+
structured_losses: dict[str, Tensor]
|
| 219 |
+
language_output: Any
|
| 220 |
+
safety: SafetyOutput
|
| 221 |
+
region_embeddings: Tensor
|
| 222 |
+
detector_output: DetectorBatch
|
| 223 |
+
prefix: PrefixBatch
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
@dataclass
|
| 227 |
+
class ResolvedRegion:
|
| 228 |
+
reference: str
|
| 229 |
+
proposal_index: int
|
| 230 |
+
box_xyxy: Tensor
|
| 231 |
+
mask: Tensor
|
| 232 |
+
score: Tensor | None
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
_REGION_REFERENCE = re.compile(r"(?<![A-Za-z0-9_])region_(\d{3})(?![A-Za-z0-9_])")
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def parse_region_references(text: str, *, max_regions: int = 100) -> tuple[int, ...]:
|
| 239 |
+
"""Parse unique ``region_NNN`` references in first-occurrence order."""
|
| 240 |
+
|
| 241 |
+
found: list[int] = []
|
| 242 |
+
seen: set[int] = set()
|
| 243 |
+
for match in _REGION_REFERENCE.finditer(text):
|
| 244 |
+
index = int(match.group(1))
|
| 245 |
+
if index < max_regions and index not in seen:
|
| 246 |
+
found.append(index)
|
| 247 |
+
seen.add(index)
|
| 248 |
+
return tuple(found)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _field(value: Any, name: str) -> Any:
|
| 252 |
+
if isinstance(value, Mapping):
|
| 253 |
+
return value.get(name)
|
| 254 |
+
return getattr(value, name, None)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class FalconModel(nn.Module):
|
| 258 |
+
"""DINOv2 + live RF-DETR proposals + SSA + a Vicuna/Llama decoder."""
|
| 259 |
+
|
| 260 |
+
def __init__(
|
| 261 |
+
self,
|
| 262 |
+
config: FalconConfig,
|
| 263 |
+
*,
|
| 264 |
+
vision_encoder: nn.Module,
|
| 265 |
+
language_model: nn.Module,
|
| 266 |
+
detector: Any | None = None,
|
| 267 |
+
image_processor: Any | None = None,
|
| 268 |
+
tokenizer: Any | None = None,
|
| 269 |
+
) -> None:
|
| 270 |
+
super().__init__()
|
| 271 |
+
self.config = config
|
| 272 |
+
self.vision_encoder = vision_encoder
|
| 273 |
+
self.language_model = language_model
|
| 274 |
+
self.detector = detector
|
| 275 |
+
self.image_processor = image_processor
|
| 276 |
+
self.tokenizer = tokenizer
|
| 277 |
+
|
| 278 |
+
self.patch_aggregator = PatchTokenAggregator(config.vision_dim, config.region_dim)
|
| 279 |
+
self.region_encoder = MaskAwareRegionEncoder(
|
| 280 |
+
config.region_dim,
|
| 281 |
+
config.region_dim,
|
| 282 |
+
roi_size=config.roi_size,
|
| 283 |
+
probability_threshold=config.mask_probability_threshold,
|
| 284 |
+
)
|
| 285 |
+
self.safety_adapter = StructuredSafetyAdapter(
|
| 286 |
+
config.region_dim, config.language_dim, capabilities=config.safety_capabilities
|
| 287 |
+
)
|
| 288 |
+
self.image_projection = nn.Linear(config.region_dim, config.language_dim)
|
| 289 |
+
self.region_projection = nn.Linear(config.region_dim, config.language_dim)
|
| 290 |
+
self.region_index_embedding = nn.Embedding(config.max_regions, config.language_dim)
|
| 291 |
+
self.training_stage = 2
|
| 292 |
+
self.set_training_stage(2)
|
| 293 |
+
|
| 294 |
+
@classmethod
|
| 295 |
+
def from_pretrained(
|
| 296 |
+
cls,
|
| 297 |
+
*,
|
| 298 |
+
vision_model_name_or_path: str,
|
| 299 |
+
language_model_name_or_path: str,
|
| 300 |
+
detector: Any | None = None,
|
| 301 |
+
region_dim: int = 1024,
|
| 302 |
+
image_size: int = 448,
|
| 303 |
+
patch_size: int = 14,
|
| 304 |
+
roi_size: int = 4,
|
| 305 |
+
max_regions: int = 100,
|
| 306 |
+
max_text_tokens: int = 256,
|
| 307 |
+
text_overflow_policy: str = "error",
|
| 308 |
+
risk_loss_weight: float = 1.0,
|
| 309 |
+
presence_loss_weight: float = 1.0,
|
| 310 |
+
link_loss_weight: float = 1.0,
|
| 311 |
+
safety_capabilities: SafetyCapabilities = ALL_SAFETY_HEADS,
|
| 312 |
+
vision_kwargs: Mapping[str, Any] | None = None,
|
| 313 |
+
language_kwargs: Mapping[str, Any] | None = None,
|
| 314 |
+
) -> FalconModel:
|
| 315 |
+
"""Construct the two pretrained encoders without hard-coded model paths."""
|
| 316 |
+
|
| 317 |
+
from transformers import (
|
| 318 |
+
AutoImageProcessor,
|
| 319 |
+
AutoModel,
|
| 320 |
+
AutoModelForCausalLM,
|
| 321 |
+
AutoTokenizer,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
vision_encoder = AutoModel.from_pretrained(
|
| 325 |
+
vision_model_name_or_path,
|
| 326 |
+
**dict(vision_kwargs or {}),
|
| 327 |
+
)
|
| 328 |
+
language_model = AutoModelForCausalLM.from_pretrained(
|
| 329 |
+
language_model_name_or_path,
|
| 330 |
+
**dict(language_kwargs or {}),
|
| 331 |
+
)
|
| 332 |
+
image_processor = AutoImageProcessor.from_pretrained(
|
| 333 |
+
vision_model_name_or_path,
|
| 334 |
+
size={"height": image_size, "width": image_size},
|
| 335 |
+
crop_size={"height": image_size, "width": image_size},
|
| 336 |
+
use_fast=False,
|
| 337 |
+
)
|
| 338 |
+
tokenizer = AutoTokenizer.from_pretrained(language_model_name_or_path, use_fast=False)
|
| 339 |
+
if tokenizer.pad_token_id is None:
|
| 340 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 341 |
+
|
| 342 |
+
vision_dim = int(vision_encoder.config.hidden_size)
|
| 343 |
+
language_dim = int(language_model.config.hidden_size)
|
| 344 |
+
return cls(
|
| 345 |
+
FalconConfig(
|
| 346 |
+
vision_dim=vision_dim,
|
| 347 |
+
language_dim=language_dim,
|
| 348 |
+
region_dim=region_dim,
|
| 349 |
+
image_size=image_size,
|
| 350 |
+
patch_size=patch_size,
|
| 351 |
+
roi_size=roi_size,
|
| 352 |
+
max_regions=max_regions,
|
| 353 |
+
max_text_tokens=max_text_tokens,
|
| 354 |
+
text_overflow_policy=text_overflow_policy,
|
| 355 |
+
risk_loss_weight=risk_loss_weight,
|
| 356 |
+
presence_loss_weight=presence_loss_weight,
|
| 357 |
+
link_loss_weight=link_loss_weight,
|
| 358 |
+
safety_capabilities=safety_capabilities,
|
| 359 |
+
),
|
| 360 |
+
vision_encoder=vision_encoder,
|
| 361 |
+
language_model=language_model,
|
| 362 |
+
detector=detector,
|
| 363 |
+
image_processor=image_processor,
|
| 364 |
+
tokenizer=tokenizer,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
@property
|
| 368 |
+
def device(self) -> torch.device:
|
| 369 |
+
return next(self.patch_aggregator.parameters()).device
|
| 370 |
+
|
| 371 |
+
@staticmethod
|
| 372 |
+
def _as_image_batch(images: Any) -> list[Any]:
|
| 373 |
+
if isinstance(images, list | tuple):
|
| 374 |
+
return list(images)
|
| 375 |
+
if isinstance(images, Tensor) and images.ndim == 4:
|
| 376 |
+
return list(images.unbind(0))
|
| 377 |
+
if hasattr(images, "ndim") and images.ndim == 4:
|
| 378 |
+
return [images[index] for index in range(len(images))]
|
| 379 |
+
return [images]
|
| 380 |
+
|
| 381 |
+
@staticmethod
|
| 382 |
+
def _image_size(image: Any) -> tuple[int, int]:
|
| 383 |
+
# PIL exposes (width, height); NumPy and torch expose a shape.
|
| 384 |
+
if hasattr(image, "size") and isinstance(image.size, tuple):
|
| 385 |
+
width, height = image.size
|
| 386 |
+
return int(height), int(width)
|
| 387 |
+
shape = tuple(image.shape) if hasattr(image, "shape") else ()
|
| 388 |
+
if len(shape) < 2:
|
| 389 |
+
raise ValueError("cannot infer original image size")
|
| 390 |
+
if len(shape) == 2:
|
| 391 |
+
return int(shape[0]), int(shape[1])
|
| 392 |
+
if shape[0] in (1, 3, 4):
|
| 393 |
+
return int(shape[-2]), int(shape[-1])
|
| 394 |
+
return int(shape[0]), int(shape[1])
|
| 395 |
+
|
| 396 |
+
@staticmethod
|
| 397 |
+
def _normalise_text_batch(
|
| 398 |
+
texts: str | Sequence[str] | None,
|
| 399 |
+
batch_size: int,
|
| 400 |
+
name: str,
|
| 401 |
+
) -> list[str] | None:
|
| 402 |
+
if texts is None:
|
| 403 |
+
return None
|
| 404 |
+
values = [texts] * batch_size if isinstance(texts, str) else list(texts)
|
| 405 |
+
if len(values) != batch_size:
|
| 406 |
+
raise ValueError(f"{name} must contain one string per image")
|
| 407 |
+
return values
|
| 408 |
+
|
| 409 |
+
def _process_images(self, images: list[Any]) -> Tensor:
|
| 410 |
+
if self.image_processor is None:
|
| 411 |
+
raise RuntimeError("an AutoImageProcessor-compatible object is required for raw images")
|
| 412 |
+
processed = self.image_processor(images=images, return_tensors="pt")
|
| 413 |
+
pixel_values = _field(processed, "pixel_values")
|
| 414 |
+
if pixel_values is None:
|
| 415 |
+
raise ValueError("image processor did not return pixel_values")
|
| 416 |
+
if tuple(pixel_values.shape[-2:]) != (self.config.image_size, self.config.image_size):
|
| 417 |
+
raise ValueError(
|
| 418 |
+
"image processor returned "
|
| 419 |
+
f"{tuple(pixel_values.shape[-2:])}; expected "
|
| 420 |
+
f"{self.config.image_size}x{self.config.image_size}"
|
| 421 |
+
)
|
| 422 |
+
return pixel_values.to(self.device)
|
| 423 |
+
|
| 424 |
+
def _encode_one_text(self, text: str, *, special_tokens: bool) -> list[int]:
|
| 425 |
+
if self.tokenizer is None:
|
| 426 |
+
raise RuntimeError("a Vicuna/Llama tokenizer is required for string prompts")
|
| 427 |
+
encoded = self.tokenizer(
|
| 428 |
+
text,
|
| 429 |
+
add_special_tokens=special_tokens,
|
| 430 |
+
return_attention_mask=False,
|
| 431 |
+
)
|
| 432 |
+
token_ids = _field(encoded, "input_ids")
|
| 433 |
+
if isinstance(token_ids, Tensor):
|
| 434 |
+
token_ids = token_ids.reshape(-1).tolist()
|
| 435 |
+
if token_ids and isinstance(token_ids[0], list):
|
| 436 |
+
token_ids = token_ids[0]
|
| 437 |
+
return list(token_ids)
|
| 438 |
+
|
| 439 |
+
def _answer_token_ids(self, answer: str) -> list[int]:
|
| 440 |
+
"""Use the same nonempty-answer/EOS contract for preflight and training."""
|
| 441 |
+
eos_id = getattr(self.tokenizer, "eos_token_id", None)
|
| 442 |
+
if eos_id is None:
|
| 443 |
+
raise ValueError("the tokenizer must define eos_token_id for supervised answers")
|
| 444 |
+
answer_ids = self._encode_one_text(answer, special_tokens=False)
|
| 445 |
+
content = answer_ids[:-1] if answer_ids and answer_ids[-1] == eos_id else answer_ids
|
| 446 |
+
if not content:
|
| 447 |
+
raise ValueError("a supervised answer must contain at least one token")
|
| 448 |
+
return content + [eos_id]
|
| 449 |
+
|
| 450 |
+
def text_token_counts(self, prompt: str, answer: str | None = None) -> dict[str, int]:
|
| 451 |
+
"""Count untruncated text on the CPU without running either backbone.
|
| 452 |
+
|
| 453 |
+
``answer=None`` measures a query whose eventual proposal-dependent target
|
| 454 |
+
is unknown. It must not be interpreted as a full training-length bound.
|
| 455 |
+
"""
|
| 456 |
+
prompt_count = len(self._encode_one_text(prompt, special_tokens=True))
|
| 457 |
+
answer_count = 0 if answer is None else len(self._answer_token_ids(answer))
|
| 458 |
+
return {
|
| 459 |
+
"prompt_tokens": prompt_count,
|
| 460 |
+
"answer_tokens": answer_count,
|
| 461 |
+
"total_tokens": prompt_count + answer_count,
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
def _tokenize(
|
| 465 |
+
self,
|
| 466 |
+
prompts: list[str],
|
| 467 |
+
answers: list[str] | None,
|
| 468 |
+
supervision: Sequence[bool] | None = None,
|
| 469 |
+
) -> tuple[Tensor, Tensor, Tensor | None]:
|
| 470 |
+
sequences: list[list[int]] = []
|
| 471 |
+
label_sequences: list[list[int]] | None = [] if answers is not None else None
|
| 472 |
+
eos_id = getattr(self.tokenizer, "eos_token_id", None)
|
| 473 |
+
|
| 474 |
+
def truncate_prompt(token_ids: list[int], budget: int) -> list[int]:
|
| 475 |
+
if len(token_ids) <= budget:
|
| 476 |
+
return token_ids
|
| 477 |
+
if self.config.text_overflow_policy == "error":
|
| 478 |
+
raise ValueError(
|
| 479 |
+
f"Prompt needs {len(token_ids)} tokens but only {budget} fit; "
|
| 480 |
+
"increase max_text_tokens instead of silently removing query context"
|
| 481 |
+
)
|
| 482 |
+
if budget <= 0:
|
| 483 |
+
return []
|
| 484 |
+
if budget == 1:
|
| 485 |
+
return token_ids[:1]
|
| 486 |
+
# Retain BOS and the recent USER/ASSISTANT suffix.
|
| 487 |
+
return token_ids[:1] + token_ids[-(budget - 1) :]
|
| 488 |
+
|
| 489 |
+
for index, prompt in enumerate(prompts):
|
| 490 |
+
prompt_ids = self._encode_one_text(prompt, special_tokens=True)
|
| 491 |
+
if answers is None or (supervision is not None and not supervision[index]):
|
| 492 |
+
prompt_sequence = truncate_prompt(prompt_ids, self.config.max_text_tokens)
|
| 493 |
+
sequences.append(prompt_sequence)
|
| 494 |
+
if label_sequences is not None:
|
| 495 |
+
label_sequences.append([-100] * len(prompt_sequence))
|
| 496 |
+
continue
|
| 497 |
+
answer_ids = self._answer_token_ids(answers[index])
|
| 498 |
+
if (
|
| 499 |
+
self.config.text_overflow_policy == "error"
|
| 500 |
+
and len(prompt_ids) + len(answer_ids) > self.config.max_text_tokens
|
| 501 |
+
):
|
| 502 |
+
raise ValueError(
|
| 503 |
+
f"Prompt+answer needs {len(prompt_ids) + len(answer_ids)} tokens "
|
| 504 |
+
f"({len(prompt_ids)} prompt, {len(answer_ids)} answer including EOS), "
|
| 505 |
+
f"exceeding max_text_tokens={self.config.max_text_tokens}; increase "
|
| 506 |
+
"the budget to preserve the full query and target"
|
| 507 |
+
)
|
| 508 |
+
answer_budget = self.config.max_text_tokens - 1
|
| 509 |
+
if len(answer_ids) > answer_budget:
|
| 510 |
+
answer_ids = answer_ids[: answer_budget - 1] + [eos_id]
|
| 511 |
+
prompt_budget = self.config.max_text_tokens - len(answer_ids)
|
| 512 |
+
prompt_ids = truncate_prompt(prompt_ids, prompt_budget)
|
| 513 |
+
sequences.append(prompt_ids + answer_ids)
|
| 514 |
+
assert label_sequences is not None
|
| 515 |
+
label_sequences.append([-100] * len(prompt_ids) + answer_ids)
|
| 516 |
+
|
| 517 |
+
pad_id = getattr(self.tokenizer, "pad_token_id", None)
|
| 518 |
+
if pad_id is None:
|
| 519 |
+
pad_id = eos_id if eos_id is not None else 0
|
| 520 |
+
max_length = max(len(sequence) for sequence in sequences)
|
| 521 |
+
input_ids = torch.full((len(sequences), max_length), pad_id, dtype=torch.long)
|
| 522 |
+
attention_mask = torch.zeros_like(input_ids)
|
| 523 |
+
labels = torch.full_like(input_ids, -100) if label_sequences is not None else None
|
| 524 |
+
for row, sequence in enumerate(sequences):
|
| 525 |
+
length = len(sequence)
|
| 526 |
+
input_ids[row, :length] = torch.as_tensor(sequence)
|
| 527 |
+
attention_mask[row, :length] = 1
|
| 528 |
+
if labels is not None and label_sequences is not None:
|
| 529 |
+
labels[row, :length] = torch.as_tensor(label_sequences[row])
|
| 530 |
+
return (
|
| 531 |
+
input_ids.to(self.device),
|
| 532 |
+
attention_mask.to(self.device),
|
| 533 |
+
(None if labels is None else labels.to(self.device)),
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
@staticmethod
|
| 537 |
+
def _coerce_detector_output(value: Any) -> DetectorBatch:
|
| 538 |
+
if isinstance(value, list | tuple):
|
| 539 |
+
return DetectorBatch.pack([FalconModel._coerce_detector_output(item) for item in value])
|
| 540 |
+
if isinstance(value, DetectorBatch):
|
| 541 |
+
return value.batched()
|
| 542 |
+
boxes = _field(value, "boxes")
|
| 543 |
+
if boxes is None:
|
| 544 |
+
boxes = _field(value, "boxes_xyxy")
|
| 545 |
+
masks = _field(value, "masks")
|
| 546 |
+
if boxes is None or masks is None:
|
| 547 |
+
raise TypeError("detector output must expose boxes/boxes_xyxy and masks")
|
| 548 |
+
image_sizes = _field(value, "image_sizes")
|
| 549 |
+
if image_sizes is None:
|
| 550 |
+
image_sizes = _field(value, "original_hw")
|
| 551 |
+
valid = _field(value, "valid")
|
| 552 |
+
scores = _field(value, "scores")
|
| 553 |
+
return DetectorBatch(
|
| 554 |
+
boxes=torch.as_tensor(boxes),
|
| 555 |
+
masks=torch.as_tensor(masks),
|
| 556 |
+
valid=None if valid is None else torch.as_tensor(valid),
|
| 557 |
+
scores=None if scores is None else torch.as_tensor(scores),
|
| 558 |
+
image_sizes=None if image_sizes is None else torch.as_tensor(image_sizes),
|
| 559 |
+
masks_are_logits=bool(_field(value, "masks_are_logits") or False),
|
| 560 |
+
mask_sizes=_field(value, "mask_sizes"),
|
| 561 |
+
).batched()
|
| 562 |
+
|
| 563 |
+
def _detect(self, images: list[Any]) -> DetectorBatch:
|
| 564 |
+
if self.detector is None:
|
| 565 |
+
raise RuntimeError("attach a live detector or provide detector_outputs")
|
| 566 |
+
if isinstance(self.detector, nn.Module):
|
| 567 |
+
self.detector.eval()
|
| 568 |
+
with torch.no_grad():
|
| 569 |
+
outputs = [self._coerce_detector_output(self.detector(image)) for image in images]
|
| 570 |
+
for image, output in zip(images, outputs, strict=False):
|
| 571 |
+
if output.image_sizes is None:
|
| 572 |
+
output.image_sizes = torch.as_tensor([self._image_size(image)])
|
| 573 |
+
return DetectorBatch.pack(outputs)
|
| 574 |
+
|
| 575 |
+
def _extract_vision_features(self, pixel_values: Tensor) -> VisionFeatures:
|
| 576 |
+
self.vision_encoder.eval()
|
| 577 |
+
with torch.no_grad():
|
| 578 |
+
encoded = self.vision_encoder(pixel_values=pixel_values, return_dict=True)
|
| 579 |
+
hidden_state = _field(encoded, "last_hidden_state")
|
| 580 |
+
if hidden_state is None:
|
| 581 |
+
raise ValueError("vision encoder did not return last_hidden_state")
|
| 582 |
+
patch_tokens = hidden_state[:, self.config.vision_prefix_tokens :].detach()
|
| 583 |
+
pixel_height, pixel_width = pixel_values.shape[-2:]
|
| 584 |
+
grid_size = (
|
| 585 |
+
pixel_height // self.config.patch_size,
|
| 586 |
+
pixel_width // self.config.patch_size,
|
| 587 |
+
)
|
| 588 |
+
return VisionFeatures(patch_tokens, grid_size)
|
| 589 |
+
|
| 590 |
+
def extract_vision_features(self, images: Any) -> VisionFeatures:
|
| 591 |
+
"""Run frozen DINO once for one or more raw images."""
|
| 592 |
+
|
| 593 |
+
image_batch = self._as_image_batch(images)
|
| 594 |
+
return self._extract_vision_features(self._process_images(image_batch))
|
| 595 |
+
|
| 596 |
+
def encode_vision_features(self, features: VisionFeatures) -> tuple[Tensor, Tensor]:
|
| 597 |
+
"""Apply the trainable aggregation and image projection layers."""
|
| 598 |
+
|
| 599 |
+
features = features.to(self.device)
|
| 600 |
+
feature_map = self.patch_aggregator(features.patch_tokens, features.grid_size)
|
| 601 |
+
image_tokens = feature_map.flatten(start_dim=2).transpose(1, 2)
|
| 602 |
+
return feature_map, self.image_projection(image_tokens)
|
| 603 |
+
|
| 604 |
+
def encode_images(self, pixel_values: Tensor) -> tuple[Tensor, Tensor]:
|
| 605 |
+
"""Return the aggregated feature map and projected DINO image tokens."""
|
| 606 |
+
|
| 607 |
+
return self.encode_vision_features(self._extract_vision_features(pixel_values))
|
| 608 |
+
|
| 609 |
+
def build_prefix(
|
| 610 |
+
self,
|
| 611 |
+
*,
|
| 612 |
+
image_tokens: Tensor,
|
| 613 |
+
region_tokens: Tensor,
|
| 614 |
+
region_valid: Tensor,
|
| 615 |
+
safety_tokens: Tensor,
|
| 616 |
+
input_ids: Tensor,
|
| 617 |
+
attention_mask: Tensor,
|
| 618 |
+
labels: Tensor | None,
|
| 619 |
+
) -> PrefixBatch:
|
| 620 |
+
"""Build ``[image | region | SSA | text]`` inputs for Vicuna/Llama."""
|
| 621 |
+
|
| 622 |
+
text_embeddings = self.language_model.get_input_embeddings()(input_ids)
|
| 623 |
+
dtype = text_embeddings.dtype
|
| 624 |
+
image_tokens = image_tokens.to(dtype=dtype)
|
| 625 |
+
region_tokens = region_tokens.to(dtype=dtype)
|
| 626 |
+
safety_tokens = safety_tokens.to(dtype=dtype)
|
| 627 |
+
inputs_embeds = torch.cat(
|
| 628 |
+
(image_tokens, region_tokens, safety_tokens, text_embeddings),
|
| 629 |
+
dim=1,
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
batch = input_ids.shape[0]
|
| 633 |
+
image_mask = torch.ones(
|
| 634 |
+
(batch, image_tokens.shape[1]),
|
| 635 |
+
dtype=attention_mask.dtype,
|
| 636 |
+
device=attention_mask.device,
|
| 637 |
+
)
|
| 638 |
+
safety_mask = torch.ones(
|
| 639 |
+
(batch, safety_tokens.shape[1]),
|
| 640 |
+
dtype=attention_mask.dtype,
|
| 641 |
+
device=attention_mask.device,
|
| 642 |
+
)
|
| 643 |
+
prefix_attention = torch.cat(
|
| 644 |
+
(image_mask, region_valid.to(attention_mask.dtype), safety_mask, attention_mask),
|
| 645 |
+
dim=1,
|
| 646 |
+
)
|
| 647 |
+
prefix_labels = None
|
| 648 |
+
if labels is not None:
|
| 649 |
+
ignored = torch.full(
|
| 650 |
+
(batch, image_tokens.shape[1] + region_tokens.shape[1] + safety_tokens.shape[1]),
|
| 651 |
+
-100,
|
| 652 |
+
dtype=labels.dtype,
|
| 653 |
+
device=labels.device,
|
| 654 |
+
)
|
| 655 |
+
prefix_labels = torch.cat((ignored, labels), dim=1)
|
| 656 |
+
return PrefixBatch(
|
| 657 |
+
inputs_embeds=inputs_embeds,
|
| 658 |
+
attention_mask=prefix_attention,
|
| 659 |
+
labels=prefix_labels,
|
| 660 |
+
image_token_count=image_tokens.shape[1],
|
| 661 |
+
region_token_count=region_tokens.shape[1],
|
| 662 |
+
safety_token_count=safety_tokens.shape[1],
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
def prepare_multimodal_inputs(
|
| 666 |
+
self,
|
| 667 |
+
*,
|
| 668 |
+
images: Any,
|
| 669 |
+
prompts: str | Sequence[str],
|
| 670 |
+
answers: str | Sequence[str] | None = None,
|
| 671 |
+
detector_outputs: DetectorBatch | Mapping[str, Any] | Any | None = None,
|
| 672 |
+
vision_features: VisionFeatures | None = None,
|
| 673 |
+
language_supervision: Tensor | Sequence[bool] | None = None,
|
| 674 |
+
) -> FalconEncoding:
|
| 675 |
+
image_batch = self._as_image_batch(images)
|
| 676 |
+
prompts_batch = self._normalise_text_batch(prompts, len(image_batch), "prompts")
|
| 677 |
+
answers_batch = self._normalise_text_batch(answers, len(image_batch), "answers")
|
| 678 |
+
assert prompts_batch is not None
|
| 679 |
+
|
| 680 |
+
supervision = None
|
| 681 |
+
if language_supervision is not None:
|
| 682 |
+
supplied = torch.as_tensor(language_supervision)
|
| 683 |
+
if supplied.dtype != torch.bool or supplied.shape != (len(image_batch),):
|
| 684 |
+
raise ValueError("language_supervision must contain one boolean per example")
|
| 685 |
+
if answers_batch is None:
|
| 686 |
+
raise ValueError("language_supervision requires answer labels")
|
| 687 |
+
supervision = supplied.tolist()
|
| 688 |
+
input_ids, attention_mask, labels = self._tokenize(
|
| 689 |
+
prompts_batch, answers_batch, supervision
|
| 690 |
+
)
|
| 691 |
+
detected = (
|
| 692 |
+
self._detect(image_batch)
|
| 693 |
+
if detector_outputs is None
|
| 694 |
+
else self._coerce_detector_output(detector_outputs)
|
| 695 |
+
).to(self.device)
|
| 696 |
+
if detected.image_sizes is None:
|
| 697 |
+
detected.image_sizes = torch.as_tensor(
|
| 698 |
+
[self._image_size(image) for image in image_batch],
|
| 699 |
+
device=self.device,
|
| 700 |
+
)
|
| 701 |
+
if detected.boxes.shape[0] != len(image_batch):
|
| 702 |
+
raise ValueError("detector output batch size does not match the image batch")
|
| 703 |
+
|
| 704 |
+
features = (
|
| 705 |
+
self.extract_vision_features(image_batch)
|
| 706 |
+
if vision_features is None
|
| 707 |
+
else vision_features.to(self.device)
|
| 708 |
+
)
|
| 709 |
+
if features.patch_tokens.shape[0] != len(image_batch):
|
| 710 |
+
raise ValueError("vision feature batch size does not match the image batch")
|
| 711 |
+
feature_map, image_tokens = self.encode_vision_features(features)
|
| 712 |
+
assert detected.valid is not None and detected.image_sizes is not None
|
| 713 |
+
region_embeddings = self.region_encoder(
|
| 714 |
+
feature_map,
|
| 715 |
+
detected.boxes,
|
| 716 |
+
detected.masks,
|
| 717 |
+
detected.image_sizes,
|
| 718 |
+
detected.valid,
|
| 719 |
+
masks_are_logits=detected.masks_are_logits,
|
| 720 |
+
mask_sizes=detected.mask_sizes,
|
| 721 |
+
)
|
| 722 |
+
safety = self.safety_adapter(region_embeddings, detected.valid)
|
| 723 |
+
region_tokens = self.region_projection(region_embeddings)
|
| 724 |
+
proposal_count = region_tokens.shape[1]
|
| 725 |
+
if proposal_count > self.config.max_regions:
|
| 726 |
+
raise ValueError(
|
| 727 |
+
f"detector returned {proposal_count} proposals; "
|
| 728 |
+
f"max_regions={self.config.max_regions}"
|
| 729 |
+
)
|
| 730 |
+
region_indices = torch.arange(proposal_count, device=self.device)
|
| 731 |
+
region_tokens = region_tokens + self.region_index_embedding(region_indices).unsqueeze(0)
|
| 732 |
+
prefix = self.build_prefix(
|
| 733 |
+
image_tokens=image_tokens,
|
| 734 |
+
region_tokens=region_tokens,
|
| 735 |
+
region_valid=detected.valid,
|
| 736 |
+
safety_tokens=safety.tokens,
|
| 737 |
+
input_ids=input_ids,
|
| 738 |
+
attention_mask=attention_mask,
|
| 739 |
+
labels=labels,
|
| 740 |
+
)
|
| 741 |
+
self._validate_context_budget(prefix)
|
| 742 |
+
return FalconEncoding(
|
| 743 |
+
prefix=prefix,
|
| 744 |
+
safety=safety,
|
| 745 |
+
region_embeddings=region_embeddings,
|
| 746 |
+
detector_output=detected,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
def _validate_context_budget(self, prefix: PrefixBatch, *, additional_tokens: int = 0) -> None:
|
| 750 |
+
"""Respect the loaded language model's declared positional context."""
|
| 751 |
+
language_config = getattr(self.language_model, "config", None)
|
| 752 |
+
limit = getattr(language_config, "max_position_embeddings", None)
|
| 753 |
+
if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1:
|
| 754 |
+
return
|
| 755 |
+
needed = prefix.inputs_embeds.shape[1] + additional_tokens
|
| 756 |
+
if needed > limit:
|
| 757 |
+
raise ValueError(
|
| 758 |
+
f"Multimodal context needs {needed} positions including image/region/safety "
|
| 759 |
+
f"tokens and {additional_tokens} requested new tokens, but the language model "
|
| 760 |
+
f"declares max_position_embeddings={limit}; reduce the text/generation budget "
|
| 761 |
+
"or use a verified longer-context backbone"
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
def forward(
|
| 765 |
+
self,
|
| 766 |
+
*,
|
| 767 |
+
images: Any,
|
| 768 |
+
prompts: str | Sequence[str],
|
| 769 |
+
answers: str | Sequence[str] | None = None,
|
| 770 |
+
targets: Mapping[str, Any] | None = None,
|
| 771 |
+
detector_outputs: DetectorBatch | Mapping[str, Any] | Any | None = None,
|
| 772 |
+
vision_features: VisionFeatures | None = None,
|
| 773 |
+
language_supervision: Tensor | Sequence[bool] | None = None,
|
| 774 |
+
) -> FalconOutput:
|
| 775 |
+
"""Run raw images through live detection, DINOv2, SSA, and Vicuna/Llama."""
|
| 776 |
+
|
| 777 |
+
encoded = self.prepare_multimodal_inputs(
|
| 778 |
+
images=images,
|
| 779 |
+
prompts=prompts,
|
| 780 |
+
answers=answers,
|
| 781 |
+
detector_outputs=detector_outputs,
|
| 782 |
+
vision_features=vision_features,
|
| 783 |
+
language_supervision=language_supervision,
|
| 784 |
+
)
|
| 785 |
+
prefix = encoded.prefix
|
| 786 |
+
if language_supervision is not None:
|
| 787 |
+
supervision = torch.as_tensor(language_supervision, device=self.device)
|
| 788 |
+
if supervision.dtype != torch.bool or supervision.shape != (
|
| 789 |
+
prefix.inputs_embeds.shape[0],
|
| 790 |
+
):
|
| 791 |
+
raise ValueError("language_supervision must contain one boolean per example")
|
| 792 |
+
if prefix.labels is None:
|
| 793 |
+
raise ValueError("language_supervision requires answer labels")
|
| 794 |
+
prefix.labels = prefix.labels.masked_fill(~supervision[:, None], -100)
|
| 795 |
+
# Causal-LM cross entropy over zero labels is NaN in common backends.
|
| 796 |
+
# A batch with no transportable targets still trains available SSA heads.
|
| 797 |
+
if not bool(supervision.any()):
|
| 798 |
+
prefix.labels = None
|
| 799 |
+
language_output = self.language_model(
|
| 800 |
+
inputs_embeds=prefix.inputs_embeds,
|
| 801 |
+
attention_mask=prefix.attention_mask,
|
| 802 |
+
labels=prefix.labels,
|
| 803 |
+
return_dict=True,
|
| 804 |
+
)
|
| 805 |
+
language_loss = _field(language_output, "loss")
|
| 806 |
+
structured_losses: dict[str, Tensor] = {}
|
| 807 |
+
total_loss = language_loss
|
| 808 |
+
if targets is not None:
|
| 809 |
+
missing = {"risk", "presence", "links"}.difference(targets)
|
| 810 |
+
if missing:
|
| 811 |
+
raise ValueError(f"structured targets are missing: {sorted(missing)}")
|
| 812 |
+
structured_losses = self.safety_adapter.loss(
|
| 813 |
+
encoded.safety,
|
| 814 |
+
risk=self._target_tensor(targets["risk"]),
|
| 815 |
+
presence=self._target_tensor(targets["presence"]),
|
| 816 |
+
links=self._target_tensor(targets["links"]),
|
| 817 |
+
)
|
| 818 |
+
structured_total = (
|
| 819 |
+
self.config.risk_loss_weight * structured_losses["risk"]
|
| 820 |
+
+ self.config.presence_loss_weight * structured_losses["presence"]
|
| 821 |
+
+ self.config.link_loss_weight * structured_losses["links"]
|
| 822 |
+
)
|
| 823 |
+
total_loss = structured_total if total_loss is None else total_loss + structured_total
|
| 824 |
+
if total_loss is not None and self.training and not total_loss.requires_grad:
|
| 825 |
+
# An ablation can disable every structured head while a detector miss
|
| 826 |
+
# masks all language targets. Keep a genuine differentiable zero,
|
| 827 |
+
# not an empty-mask label or NaN, for the distributed training step.
|
| 828 |
+
total_loss = total_loss + prefix.inputs_embeds.sum() * 0.0
|
| 829 |
+
return FalconOutput(
|
| 830 |
+
loss=total_loss,
|
| 831 |
+
language_loss=language_loss,
|
| 832 |
+
structured_losses=structured_losses,
|
| 833 |
+
language_output=language_output,
|
| 834 |
+
safety=encoded.safety,
|
| 835 |
+
region_embeddings=encoded.region_embeddings,
|
| 836 |
+
detector_output=encoded.detector_output,
|
| 837 |
+
prefix=prefix,
|
| 838 |
+
)
|
| 839 |
+
|
| 840 |
+
@torch.no_grad()
|
| 841 |
+
def generate(
|
| 842 |
+
self,
|
| 843 |
+
*,
|
| 844 |
+
images: Any,
|
| 845 |
+
prompts: str | Sequence[str],
|
| 846 |
+
detector_outputs: DetectorBatch | Mapping[str, Any] | Any | None = None,
|
| 847 |
+
vision_features: VisionFeatures | None = None,
|
| 848 |
+
**generation_kwargs: Any,
|
| 849 |
+
) -> Tensor:
|
| 850 |
+
encoded = self.prepare_multimodal_inputs(
|
| 851 |
+
images=images,
|
| 852 |
+
prompts=prompts,
|
| 853 |
+
detector_outputs=detector_outputs,
|
| 854 |
+
vision_features=vision_features,
|
| 855 |
+
)
|
| 856 |
+
new_tokens = generation_kwargs.get("max_new_tokens")
|
| 857 |
+
if new_tokens is not None:
|
| 858 |
+
if isinstance(new_tokens, bool) or not isinstance(new_tokens, int) or new_tokens < 1:
|
| 859 |
+
raise ValueError("max_new_tokens must be a positive integer")
|
| 860 |
+
self._validate_context_budget(encoded.prefix, additional_tokens=new_tokens)
|
| 861 |
+
return self.language_model.generate(
|
| 862 |
+
inputs_embeds=encoded.prefix.inputs_embeds,
|
| 863 |
+
attention_mask=encoded.prefix.attention_mask,
|
| 864 |
+
**generation_kwargs,
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
def enable_lora(
|
| 868 |
+
self,
|
| 869 |
+
*,
|
| 870 |
+
rank: int = 16,
|
| 871 |
+
alpha: int = 32,
|
| 872 |
+
dropout: float = 0.05,
|
| 873 |
+
target_modules: Sequence[str] = ("q_proj", "k_proj", "v_proj", "o_proj"),
|
| 874 |
+
) -> None:
|
| 875 |
+
"""Attach PEFT LoRA adapters to Vicuna/Llama attention projections."""
|
| 876 |
+
|
| 877 |
+
from peft import LoraConfig, TaskType, get_peft_model
|
| 878 |
+
|
| 879 |
+
if any("lora_" in name for name, _ in self.language_model.named_parameters()):
|
| 880 |
+
raise RuntimeError("LoRA adapters are already attached")
|
| 881 |
+
lora_config = LoraConfig(
|
| 882 |
+
task_type=TaskType.CAUSAL_LM,
|
| 883 |
+
r=rank,
|
| 884 |
+
lora_alpha=alpha,
|
| 885 |
+
lora_dropout=dropout,
|
| 886 |
+
bias="none",
|
| 887 |
+
target_modules=list(target_modules),
|
| 888 |
+
)
|
| 889 |
+
self.language_model = get_peft_model(self.language_model, lora_config)
|
| 890 |
+
# Attaching PEFT makes adapters trainable by default. Preserve the
|
| 891 |
+
# current paper stage until the caller explicitly enters Stage 3.
|
| 892 |
+
self.set_training_stage(self.training_stage)
|
| 893 |
+
|
| 894 |
+
@staticmethod
|
| 895 |
+
def _set_module_trainable(module: nn.Module, trainable: bool) -> None:
|
| 896 |
+
for parameter in module.parameters():
|
| 897 |
+
parameter.requires_grad = trainable
|
| 898 |
+
|
| 899 |
+
def set_training_stage(self, stage: int) -> None:
|
| 900 |
+
"""Apply paper freezing: Stage 2 projections+SSA; Stage 3 adds LoRA."""
|
| 901 |
+
|
| 902 |
+
if stage not in (2, 3):
|
| 903 |
+
raise ValueError("the multimodal model supports training stage 2 or 3")
|
| 904 |
+
for parameter in self.parameters():
|
| 905 |
+
parameter.requires_grad = False
|
| 906 |
+
for module in (
|
| 907 |
+
self.patch_aggregator,
|
| 908 |
+
self.region_encoder,
|
| 909 |
+
self.safety_adapter,
|
| 910 |
+
self.image_projection,
|
| 911 |
+
self.region_projection,
|
| 912 |
+
self.region_index_embedding,
|
| 913 |
+
):
|
| 914 |
+
self._set_module_trainable(module, True)
|
| 915 |
+
self.safety_adapter.freeze_unavailable_heads()
|
| 916 |
+
|
| 917 |
+
if stage == 3:
|
| 918 |
+
lora_parameters = [
|
| 919 |
+
parameter
|
| 920 |
+
for name, parameter in self.language_model.named_parameters()
|
| 921 |
+
if "lora_" in name
|
| 922 |
+
]
|
| 923 |
+
if not lora_parameters:
|
| 924 |
+
raise RuntimeError("Stage 3 requires enable_lora() before set_training_stage(3)")
|
| 925 |
+
for parameter in lora_parameters:
|
| 926 |
+
parameter.requires_grad = True
|
| 927 |
+
self.training_stage = stage
|
| 928 |
+
self.vision_encoder.eval()
|
| 929 |
+
if isinstance(self.detector, nn.Module):
|
| 930 |
+
self.detector.eval()
|
| 931 |
+
if stage == 2:
|
| 932 |
+
self.language_model.eval()
|
| 933 |
+
elif self.training:
|
| 934 |
+
self.language_model.train()
|
| 935 |
+
|
| 936 |
+
def trainable_parameter_names(self) -> tuple[str, ...]:
|
| 937 |
+
return tuple(name for name, parameter in self.named_parameters() if parameter.requires_grad)
|
| 938 |
+
|
| 939 |
+
def train(self, mode: bool = True) -> FalconModel:
|
| 940 |
+
super().train(mode)
|
| 941 |
+
self.vision_encoder.eval()
|
| 942 |
+
if isinstance(self.detector, nn.Module):
|
| 943 |
+
self.detector.eval()
|
| 944 |
+
if self.training_stage == 2:
|
| 945 |
+
self.language_model.eval()
|
| 946 |
+
return self
|
| 947 |
+
|
| 948 |
+
def _target_tensor(self, value: Any) -> Tensor:
|
| 949 |
+
"""Convert nullable structured labels to tensors, representing null as NaN."""
|
| 950 |
+
|
| 951 |
+
def replace_nulls(item: Any) -> Any:
|
| 952 |
+
if item is None:
|
| 953 |
+
return float("nan")
|
| 954 |
+
if isinstance(item, list | tuple):
|
| 955 |
+
return [replace_nulls(child) for child in item]
|
| 956 |
+
return item
|
| 957 |
+
|
| 958 |
+
if isinstance(value, Tensor):
|
| 959 |
+
return value.to(self.device)
|
| 960 |
+
return torch.as_tensor(replace_nulls(value), dtype=torch.float32, device=self.device)
|
| 961 |
+
|
| 962 |
+
def resolve_region_references(
|
| 963 |
+
self,
|
| 964 |
+
text: str,
|
| 965 |
+
detector_output: DetectorBatch | Any,
|
| 966 |
+
*,
|
| 967 |
+
batch_index: int = 0,
|
| 968 |
+
) -> tuple[ResolvedRegion, ...]:
|
| 969 |
+
"""Resolve generated region labels to the exact proposal box and mask."""
|
| 970 |
+
|
| 971 |
+
detected = self._coerce_detector_output(detector_output)
|
| 972 |
+
if not 0 <= batch_index < detected.boxes.shape[0]:
|
| 973 |
+
raise IndexError("batch_index is outside the detector batch")
|
| 974 |
+
assert detected.valid is not None
|
| 975 |
+
resolved = []
|
| 976 |
+
for index in parse_region_references(text, max_regions=self.config.max_regions):
|
| 977 |
+
if index >= detected.boxes.shape[1] or not bool(detected.valid[batch_index, index]):
|
| 978 |
+
continue
|
| 979 |
+
score = None if detected.scores is None else detected.scores[batch_index, index]
|
| 980 |
+
height, width = (int(value) for value in detected.mask_sizes[batch_index])
|
| 981 |
+
resolved.append(
|
| 982 |
+
ResolvedRegion(
|
| 983 |
+
reference=f"region_{index:03d}",
|
| 984 |
+
proposal_index=index,
|
| 985 |
+
box_xyxy=detected.boxes[batch_index, index],
|
| 986 |
+
mask=detected.masks[batch_index, index, :height, :width],
|
| 987 |
+
score=score,
|
| 988 |
+
)
|
| 989 |
+
)
|
| 990 |
+
return tuple(resolved)
|
model.safetensors.index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_falcon.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Self-contained Transformers wrapper around the FALCON architecture."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from dataclasses import asdict, dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoTokenizer
|
| 13 |
+
from transformers import GenerationConfig, PreTrainedModel
|
| 14 |
+
from transformers.models.auto.image_processing_auto import get_image_processor_class_from_name
|
| 15 |
+
from transformers.utils import ModelOutput
|
| 16 |
+
|
| 17 |
+
# Transformers 4.49 copies only direct relative imports for local model folders.
|
| 18 |
+
from .artifacts import atomic_json as atomic_json
|
| 19 |
+
from .capabilities import SafetyCapabilities
|
| 20 |
+
from .config import load_config as load_config
|
| 21 |
+
from .configuration_falcon import FalconConfig
|
| 22 |
+
from .detector import RFDETRDetector
|
| 23 |
+
from .grounding import resolve_segmentation as resolve_segmentation
|
| 24 |
+
from .model import FalconConfig as CoreConfig
|
| 25 |
+
from .model import FalconModel as CoreModel
|
| 26 |
+
from .prediction import predict, predict_panoptic, predict_segmentation
|
| 27 |
+
from .regions import MaskAwareRegionEncoder as MaskAwareRegionEncoder
|
| 28 |
+
from .safety import StructuredSafetyAdapter as StructuredSafetyAdapter
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _portable(value: Any) -> Any:
|
| 32 |
+
if isinstance(value, dict):
|
| 33 |
+
return {
|
| 34 |
+
key: _portable(item)
|
| 35 |
+
for key, item in value.items()
|
| 36 |
+
if key not in {
|
| 37 |
+
"_name_or_path", "name_or_path", "auto_map", "custom_pipelines",
|
| 38 |
+
"base_model_name_or_path",
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
if isinstance(value, (list, tuple)):
|
| 42 |
+
return [_portable(item) for item in value]
|
| 43 |
+
return deepcopy(value)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _backbone_config(values: dict[str, Any]) -> Any:
|
| 47 |
+
values = deepcopy(values)
|
| 48 |
+
model_type = values.pop("model_type")
|
| 49 |
+
return AutoConfig.for_model(model_type, **values)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _dtype(value: str | torch.dtype | None) -> torch.dtype:
|
| 53 |
+
if isinstance(value, torch.dtype):
|
| 54 |
+
return value
|
| 55 |
+
if value not in (None, "float32", "float16", "bfloat16"):
|
| 56 |
+
raise ValueError(f"Unsupported backbone dtype: {value!r}")
|
| 57 |
+
return getattr(torch, value or "float32")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass
|
| 61 |
+
class FalconOutput(ModelOutput):
|
| 62 |
+
loss: Any = None
|
| 63 |
+
language_loss: Any = None
|
| 64 |
+
structured_losses: Any = None
|
| 65 |
+
language_output: Any = None
|
| 66 |
+
safety: Any = None
|
| 67 |
+
region_embeddings: Any = None
|
| 68 |
+
detector_output: Any = None
|
| 69 |
+
prefix: Any = None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class FalconModel(PreTrainedModel):
|
| 73 |
+
config_class = FalconConfig
|
| 74 |
+
base_model_prefix = "core"
|
| 75 |
+
main_input_name = "images"
|
| 76 |
+
|
| 77 |
+
def __init__(self, config: FalconConfig, *, core: CoreModel | None = None) -> None:
|
| 78 |
+
config.validate()
|
| 79 |
+
super().__init__(config)
|
| 80 |
+
if core is None:
|
| 81 |
+
core_values = deepcopy(config.core_config)
|
| 82 |
+
core_values["safety_capabilities"] = SafetyCapabilities.from_dict(
|
| 83 |
+
core_values["safety_capabilities"]
|
| 84 |
+
)
|
| 85 |
+
core_config = CoreConfig(**core_values)
|
| 86 |
+
vision = AutoModel.from_config(
|
| 87 |
+
_backbone_config(config.vision_config),
|
| 88 |
+
torch_dtype=_dtype(config.vision_config.get("torch_dtype")),
|
| 89 |
+
)
|
| 90 |
+
language = AutoModelForCausalLM.from_config(
|
| 91 |
+
_backbone_config(config.language_config),
|
| 92 |
+
torch_dtype=_dtype(config.language_config.get("torch_dtype")),
|
| 93 |
+
)
|
| 94 |
+
processor_values = deepcopy(config.image_processor_config)
|
| 95 |
+
processor_class = get_image_processor_class_from_name(
|
| 96 |
+
processor_values.pop("image_processor_type", "")
|
| 97 |
+
)
|
| 98 |
+
if processor_class is None:
|
| 99 |
+
raise ValueError("Unknown image_processor_type in FALCON package")
|
| 100 |
+
core = CoreModel(
|
| 101 |
+
core_config,
|
| 102 |
+
vision_encoder=vision,
|
| 103 |
+
language_model=language,
|
| 104 |
+
detector=RFDETRDetector.from_config(config.detector_config, device="cpu"),
|
| 105 |
+
image_processor=processor_class.from_dict(processor_values),
|
| 106 |
+
)
|
| 107 |
+
core.enable_lora(**config.lora_config)
|
| 108 |
+
core.set_training_stage(3)
|
| 109 |
+
if config.language_generation_config:
|
| 110 |
+
core.language_model.generation_config = GenerationConfig.from_dict(
|
| 111 |
+
config.language_generation_config
|
| 112 |
+
)
|
| 113 |
+
# This is an explicit saved generation policy, not a policy to
|
| 114 |
+
# regenerate later from the language architecture's defaults.
|
| 115 |
+
core.language_model.generation_config._from_model_config = False
|
| 116 |
+
if core.training_stage != 3:
|
| 117 |
+
raise ValueError("A Hugging Face FALCON model requires Stage 3 weights")
|
| 118 |
+
self.core = core
|
| 119 |
+
# RFDETRDetector is not an nn.Module. Register its actual backend module
|
| 120 |
+
# explicitly so saving, loading, and device movement include every weight.
|
| 121 |
+
self.detector_model = core.detector.torch_model
|
| 122 |
+
self.detector_model.requires_grad_(False).eval()
|
| 123 |
+
self.core.checkpoint_metadata = deepcopy(config.checkpoint_metadata)
|
| 124 |
+
self.core.runtime_config = deepcopy(config.runtime_config)
|
| 125 |
+
self.core.trained_safety_capabilities = core.config.safety_capabilities
|
| 126 |
+
self.core.inference_safety_capabilities = core.config.safety_capabilities
|
| 127 |
+
self.core.inference_ssa_ablation = "none"
|
| 128 |
+
|
| 129 |
+
def __getattr__(self, name: str) -> Any:
|
| 130 |
+
try:
|
| 131 |
+
return super().__getattr__(name)
|
| 132 |
+
except AttributeError:
|
| 133 |
+
core = self.__dict__.get("_modules", {}).get("core")
|
| 134 |
+
if core is not None and hasattr(core, name):
|
| 135 |
+
return getattr(core, name)
|
| 136 |
+
raise
|
| 137 |
+
|
| 138 |
+
@classmethod
|
| 139 |
+
def from_falcon(cls, core: CoreModel) -> FalconModel:
|
| 140 |
+
"""Wrap loaded final weights without reallocating the frozen backbones."""
|
| 141 |
+
source = deepcopy(getattr(core, "checkpoint_metadata", {}))
|
| 142 |
+
if core.training_stage != 3 or source.get("training_complete") is not True:
|
| 143 |
+
raise ValueError("Export requires a completed Stage 3 model")
|
| 144 |
+
if source.get("partial_initialization_allowed") or source.get(
|
| 145 |
+
"legacy_initialization_allowed"
|
| 146 |
+
) or source.get("oracle_detector", "none") != "none":
|
| 147 |
+
raise ValueError("Cannot export partial initialization or oracle models")
|
| 148 |
+
runtime = _portable(core.runtime_config)
|
| 149 |
+
runtime["model"]["vision_model"] = "facebook/dinov2-large"
|
| 150 |
+
runtime["model"]["language_model"] = "lmsys/vicuna-7b-v1.5"
|
| 151 |
+
runtime["experiment"]["name"] = "falcon-x"
|
| 152 |
+
retained = {
|
| 153 |
+
"stage", "training_complete", "checkpoint_format", "safety_capabilities",
|
| 154 |
+
"supervision_capabilities", "ssa_ablation", "require_observed_supervision",
|
| 155 |
+
"observed_supervision_coverage", "dataset_categories",
|
| 156 |
+
"diagnostic_training_reasons", "official_result_eligible", "precision",
|
| 157 |
+
"structured_loss_recipe", "grounding_policy", "grounding_coverage",
|
| 158 |
+
}
|
| 159 |
+
metadata = {key: _portable(value) for key, value in source.items() if key in retained}
|
| 160 |
+
metadata["stage"] = 3
|
| 161 |
+
metadata["config"] = runtime
|
| 162 |
+
vision_config = _portable(core.vision_encoder.config.to_dict())
|
| 163 |
+
language_config = _portable(core.language_model.config.to_dict())
|
| 164 |
+
for values, model in ((vision_config, core.vision_encoder),
|
| 165 |
+
(language_config, core.language_model)):
|
| 166 |
+
values["torch_dtype"] = str(next(model.parameters()).dtype).removeprefix("torch.")
|
| 167 |
+
values["attn_implementation"] = model.config._attn_implementation
|
| 168 |
+
adapters = core.language_model.peft_config
|
| 169 |
+
if set(adapters) != {"default"}:
|
| 170 |
+
raise ValueError("FALCON export requires exactly one default LoRA adapter")
|
| 171 |
+
lora = adapters["default"]
|
| 172 |
+
if lora.bias != "none" or lora.modules_to_save or lora.use_dora or lora.use_rslora:
|
| 173 |
+
raise ValueError("Unsupported LoRA variant in FALCON export")
|
| 174 |
+
config = FalconConfig(
|
| 175 |
+
core_config=asdict(core.config),
|
| 176 |
+
vision_config=vision_config,
|
| 177 |
+
language_config=language_config,
|
| 178 |
+
detector_config=core.detector.export_config(),
|
| 179 |
+
image_processor_config=_portable(core.image_processor.to_dict()),
|
| 180 |
+
lora_config={
|
| 181 |
+
"rank": lora.r, "alpha": lora.lora_alpha,
|
| 182 |
+
"dropout": lora.lora_dropout, "target_modules": sorted(lora.target_modules),
|
| 183 |
+
},
|
| 184 |
+
runtime_config=runtime,
|
| 185 |
+
checkpoint_metadata=metadata,
|
| 186 |
+
language_generation_config=_portable(core.language_model.generation_config.to_dict()),
|
| 187 |
+
)
|
| 188 |
+
return cls(config, core=core)
|
| 189 |
+
|
| 190 |
+
@classmethod
|
| 191 |
+
def from_pretrained(cls, pretrained_model_name_or_path: Any, *args: Any, **kwargs: Any):
|
| 192 |
+
if kwargs.pop("torch_dtype", None) not in (None, "auto"):
|
| 193 |
+
raise ValueError("FALCON preserves each component's saved dtype; omit torch_dtype")
|
| 194 |
+
for name in ("quantization_config", "load_in_8bit", "load_in_4bit", "ignore_mismatched_sizes"):
|
| 195 |
+
if kwargs.get(name):
|
| 196 |
+
raise ValueError(f"FALCON does not support {name}")
|
| 197 |
+
device = kwargs.pop("device", None)
|
| 198 |
+
device_map = kwargs.pop("device_map", None)
|
| 199 |
+
if device_map is not None:
|
| 200 |
+
if isinstance(device_map, dict) and set(device_map) == {""}:
|
| 201 |
+
mapped_device = device_map[""]
|
| 202 |
+
elif isinstance(device_map, str | torch.device | int) and device_map not in (
|
| 203 |
+
"auto", "balanced", "balanced_low_0", "sequential", "disk",
|
| 204 |
+
):
|
| 205 |
+
mapped_device = device_map
|
| 206 |
+
else:
|
| 207 |
+
raise ValueError("FALCON supports one device; use device='cuda' or .to('cuda')")
|
| 208 |
+
if isinstance(mapped_device, int):
|
| 209 |
+
mapped_device = f"cuda:{mapped_device}"
|
| 210 |
+
if device is not None and torch.device(device) != torch.device(mapped_device):
|
| 211 |
+
raise ValueError("device and device_map disagree")
|
| 212 |
+
device = mapped_device
|
| 213 |
+
tokenizer_kwargs = {
|
| 214 |
+
name: kwargs[name] for name in (
|
| 215 |
+
"cache_dir", "force_download", "local_files_only", "token", "revision",
|
| 216 |
+
) if name in kwargs
|
| 217 |
+
}
|
| 218 |
+
package_subfolder = kwargs.get("subfolder", "")
|
| 219 |
+
tokenizer_kwargs["subfolder"] = "/".join(
|
| 220 |
+
part for part in (package_subfolder, "tokenizer") if part
|
| 221 |
+
)
|
| 222 |
+
requested_info = kwargs.pop("output_loading_info", False)
|
| 223 |
+
model, info = super().from_pretrained(
|
| 224 |
+
pretrained_model_name_or_path, *args, output_loading_info=True, **kwargs
|
| 225 |
+
)
|
| 226 |
+
issues = {name: info[name] for name in (
|
| 227 |
+
"missing_keys", "unexpected_keys", "mismatched_keys", "error_msgs"
|
| 228 |
+
) if info.get(name)}
|
| 229 |
+
if issues:
|
| 230 |
+
raise ValueError(f"Incomplete or incompatible FALCON weights: {issues}")
|
| 231 |
+
resolved_revision = getattr(model.config, "_commit_hash", None)
|
| 232 |
+
if resolved_revision:
|
| 233 |
+
tokenizer_kwargs["revision"] = resolved_revision
|
| 234 |
+
model.core.tokenizer = AutoTokenizer.from_pretrained(
|
| 235 |
+
pretrained_model_name_or_path, use_fast=False, trust_remote_code=False,
|
| 236 |
+
**tokenizer_kwargs,
|
| 237 |
+
)
|
| 238 |
+
if model.core.tokenizer.pad_token_id is None:
|
| 239 |
+
model.core.tokenizer.pad_token = model.core.tokenizer.eos_token
|
| 240 |
+
if device is not None:
|
| 241 |
+
model.to(device)
|
| 242 |
+
model.eval()
|
| 243 |
+
return (model, info) if requested_info else model
|
| 244 |
+
|
| 245 |
+
def save_pretrained(self, save_directory: str | Path, **kwargs: Any) -> None:
|
| 246 |
+
if kwargs.get("push_to_hub"):
|
| 247 |
+
raise ValueError("Save the complete folder first, then use push_to_hub() or upload_folder()")
|
| 248 |
+
if self.core.tokenizer is None:
|
| 249 |
+
raise ValueError("A complete FALCON package requires its tokenizer")
|
| 250 |
+
self.config.validate()
|
| 251 |
+
super().save_pretrained(save_directory, **kwargs)
|
| 252 |
+
self.core.tokenizer.save_pretrained(Path(save_directory) / "tokenizer")
|
| 253 |
+
self.core.image_processor.save_pretrained(save_directory)
|
| 254 |
+
|
| 255 |
+
def _apply(self, fn: Any, recurse: bool = True):
|
| 256 |
+
for dtype in (torch.float32, torch.bfloat16):
|
| 257 |
+
if fn(torch.empty(0, dtype=dtype)).dtype != dtype:
|
| 258 |
+
raise ValueError("FALCON uses mixed component dtypes; move devices without casting")
|
| 259 |
+
result = super()._apply(fn, recurse=recurse)
|
| 260 |
+
detector = self.__dict__.get("_modules", {}).get("detector_model")
|
| 261 |
+
if detector is not None:
|
| 262 |
+
self.core.detector.to(next(detector.parameters()).device)
|
| 263 |
+
return result
|
| 264 |
+
|
| 265 |
+
def train(self, mode: bool = True):
|
| 266 |
+
super().train(mode)
|
| 267 |
+
self.detector_model.eval()
|
| 268 |
+
return self
|
| 269 |
+
|
| 270 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 271 |
+
return self.core.language_model.get_input_embeddings()
|
| 272 |
+
|
| 273 |
+
def get_output_embeddings(self) -> nn.Module:
|
| 274 |
+
return self.core.language_model.get_output_embeddings()
|
| 275 |
+
|
| 276 |
+
def forward(self, **kwargs: Any) -> FalconOutput:
|
| 277 |
+
return FalconOutput(**vars(self.core(**kwargs)))
|
| 278 |
+
|
| 279 |
+
def generate(self, **kwargs: Any):
|
| 280 |
+
return self.core.generate(**kwargs)
|
| 281 |
+
|
| 282 |
+
@torch.inference_mode()
|
| 283 |
+
def predict(self, image: Any, prompt: str, *, task: str = "text", **kwargs: Any):
|
| 284 |
+
methods = {
|
| 285 |
+
"text": predict, "segmentation": predict_segmentation, "panoptic": predict_panoptic,
|
| 286 |
+
}
|
| 287 |
+
if task not in methods:
|
| 288 |
+
raise ValueError("task must be text, segmentation, or panoptic")
|
| 289 |
+
return methods[task](self.core, image=image, prompt=prompt, **kwargs)
|
| 290 |
+
|
| 291 |
+
@torch.inference_mode()
|
| 292 |
+
def predict_segmentation(self, image: Any, prompt: str, **kwargs: Any):
|
| 293 |
+
return predict_segmentation(self.core, image=image, prompt=prompt, **kwargs)
|
| 294 |
+
|
| 295 |
+
@torch.inference_mode()
|
| 296 |
+
def predict_panoptic(self, image: Any, prompt: str, **kwargs: Any):
|
| 297 |
+
return predict_panoptic(self.core, image=image, prompt=prompt, **kwargs)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
FalconModel.register_for_auto_class("AutoModel")
|
prediction.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared live prediction for local and Hugging Face FALCON models."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from collections.abc import Collection, Mapping
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
from .capabilities import SafetyCapabilities
|
| 16 |
+
from .grounding import (
|
| 17 |
+
PanopticPrediction,
|
| 18 |
+
SegmentationPrediction,
|
| 19 |
+
resolve_panoptic,
|
| 20 |
+
resolve_segmentation,
|
| 21 |
+
select_panoptic_category,
|
| 22 |
+
)
|
| 23 |
+
from .model import FalconModel
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _candidate_relations_from_answer(text: str) -> list[dict[str, str]] | None:
|
| 27 |
+
"""Extract only explicit observable geometry from a structured model answer.
|
| 28 |
+
|
| 29 |
+
Safety examples are trained to emit a compact JSON object. Risk and
|
| 30 |
+
physical links may be unavailable, so this parser never derives either
|
| 31 |
+
from geometry or component presence.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
payload = json.loads(text)
|
| 36 |
+
except (json.JSONDecodeError, TypeError):
|
| 37 |
+
return None
|
| 38 |
+
if not isinstance(payload, dict) or "candidate_relations" not in payload:
|
| 39 |
+
return None
|
| 40 |
+
raw = payload["candidate_relations"]
|
| 41 |
+
if not isinstance(raw, list):
|
| 42 |
+
raise ValueError("generated candidate_relations must be an array")
|
| 43 |
+
horizontal_values = {
|
| 44 |
+
"left_of",
|
| 45 |
+
"right_of",
|
| 46 |
+
"horizontally_aligned",
|
| 47 |
+
"left of",
|
| 48 |
+
"right of",
|
| 49 |
+
"horizontally aligned",
|
| 50 |
+
}
|
| 51 |
+
vertical_values = {"above", "below", "vertically_aligned", "vertically aligned"}
|
| 52 |
+
parsed: list[dict[str, str]] = []
|
| 53 |
+
for index, relation in enumerate(raw):
|
| 54 |
+
if not isinstance(relation, dict):
|
| 55 |
+
raise ValueError(
|
| 56 |
+
f"generated candidate_relations[{index}] must be an object"
|
| 57 |
+
)
|
| 58 |
+
if relation.get("connection_observed") not in (None, False):
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"generated candidate_relations[{index}] asserts an unavailable "
|
| 61 |
+
"physical connection"
|
| 62 |
+
)
|
| 63 |
+
source = relation.get("src_component")
|
| 64 |
+
target = relation.get("dst_component")
|
| 65 |
+
horizontal = relation.get("horizontal_relation")
|
| 66 |
+
vertical = relation.get("vertical_relation")
|
| 67 |
+
if not all(
|
| 68 |
+
isinstance(value, str) and value.strip() for value in (source, target)
|
| 69 |
+
):
|
| 70 |
+
raise ValueError(
|
| 71 |
+
f"generated candidate_relations[{index}] requires component names"
|
| 72 |
+
)
|
| 73 |
+
if horizontal is not None and horizontal not in horizontal_values:
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"generated candidate_relations[{index}] has unsupported "
|
| 76 |
+
"horizontal geometry"
|
| 77 |
+
)
|
| 78 |
+
if vertical is not None and vertical not in vertical_values:
|
| 79 |
+
raise ValueError(
|
| 80 |
+
f"generated candidate_relations[{index}] has unsupported vertical geometry"
|
| 81 |
+
)
|
| 82 |
+
if horizontal is None and vertical is None:
|
| 83 |
+
raise ValueError(
|
| 84 |
+
f"generated candidate_relations[{index}] requires at least one "
|
| 85 |
+
"supported geometry axis"
|
| 86 |
+
)
|
| 87 |
+
parsed_relation = {
|
| 88 |
+
"src_component": source.strip().casefold(),
|
| 89 |
+
"dst_component": target.strip().casefold(),
|
| 90 |
+
}
|
| 91 |
+
if horizontal is not None:
|
| 92 |
+
parsed_relation["horizontal_relation"] = (
|
| 93 |
+
str(horizontal).replace(" ", "_").casefold()
|
| 94 |
+
)
|
| 95 |
+
if vertical is not None:
|
| 96 |
+
parsed_relation["vertical_relation"] = (
|
| 97 |
+
str(vertical).replace(" ", "_").casefold()
|
| 98 |
+
)
|
| 99 |
+
parsed.append(parsed_relation)
|
| 100 |
+
return parsed
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _safety_prediction(safety: Any) -> dict[str, Any]:
|
| 104 |
+
prediction = getattr(safety, "prediction", None)
|
| 105 |
+
if callable(prediction):
|
| 106 |
+
value = prediction(0)
|
| 107 |
+
if not isinstance(value, Mapping):
|
| 108 |
+
raise TypeError("SafetyOutput.prediction() must return a mapping")
|
| 109 |
+
return dict(value)
|
| 110 |
+
|
| 111 |
+
# Compatibility for lightweight callers/tests predating SafetyOutput's
|
| 112 |
+
# serializer. If capabilities are provided they are still honored; real
|
| 113 |
+
# checkpoint-backed models always take the method path above.
|
| 114 |
+
capabilities = getattr(safety, "capabilities", SafetyCapabilities())
|
| 115 |
+
if not isinstance(capabilities, SafetyCapabilities):
|
| 116 |
+
raise TypeError("safety.capabilities must be SafetyCapabilities")
|
| 117 |
+
risk = (
|
| 118 |
+
float(safety.risk_probability[0].detach().float().cpu())
|
| 119 |
+
if capabilities.risk
|
| 120 |
+
else None
|
| 121 |
+
)
|
| 122 |
+
presence = [
|
| 123 |
+
float(value.detach().float().cpu()) if enabled else None
|
| 124 |
+
for value, enabled in zip(
|
| 125 |
+
safety.presence_probabilities[0], capabilities.presence, strict=True
|
| 126 |
+
)
|
| 127 |
+
]
|
| 128 |
+
links = [
|
| 129 |
+
float(value.detach().float().cpu()) if enabled else None
|
| 130 |
+
for value, enabled in zip(
|
| 131 |
+
safety.link_probabilities[0], capabilities.links, strict=True
|
| 132 |
+
)
|
| 133 |
+
]
|
| 134 |
+
return {
|
| 135 |
+
"risk": risk,
|
| 136 |
+
"presence": presence,
|
| 137 |
+
"links": links,
|
| 138 |
+
"capabilities": capabilities.as_dict(),
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _generate(
|
| 143 |
+
model: FalconModel,
|
| 144 |
+
*,
|
| 145 |
+
image: Any,
|
| 146 |
+
prompt: str,
|
| 147 |
+
max_new_tokens: int = 128,
|
| 148 |
+
temperature: float = 0.0,
|
| 149 |
+
) -> tuple[dict[str, Any], Any]:
|
| 150 |
+
"""Run generation once and retain the exact selected detector proposals."""
|
| 151 |
+
|
| 152 |
+
if max_new_tokens < 1:
|
| 153 |
+
raise ValueError("max_new_tokens must be positive")
|
| 154 |
+
if temperature < 0:
|
| 155 |
+
raise ValueError("temperature cannot be negative")
|
| 156 |
+
if isinstance(image, str | os.PathLike):
|
| 157 |
+
# Transformers image processors consume decoded pixels, not path
|
| 158 |
+
# strings. Detach from the file handle once here so RF-DETR, DINO, and
|
| 159 |
+
# native-size bookkeeping all see the exact same RGB image object.
|
| 160 |
+
with Image.open(Path(image).expanduser()) as opened:
|
| 161 |
+
image = opened.convert("RGB").copy()
|
| 162 |
+
formatted_prompt = f"USER: {prompt}\nASSISTANT:"
|
| 163 |
+
encoded = model.prepare_multimodal_inputs(images=image, prompts=formatted_prompt)
|
| 164 |
+
# We call the language model directly in order to preserve the exact
|
| 165 |
+
# detector output for grounding resolution, so mirror FalconModel.generate's
|
| 166 |
+
# context check before allocating the generation cache.
|
| 167 |
+
model._validate_context_budget(
|
| 168 |
+
encoded.prefix,
|
| 169 |
+
additional_tokens=max_new_tokens,
|
| 170 |
+
)
|
| 171 |
+
generation_kwargs: dict[str, Any] = {
|
| 172 |
+
"max_new_tokens": max_new_tokens,
|
| 173 |
+
"do_sample": temperature > 0,
|
| 174 |
+
"eos_token_id": model.tokenizer.eos_token_id,
|
| 175 |
+
"pad_token_id": model.tokenizer.pad_token_id,
|
| 176 |
+
}
|
| 177 |
+
if temperature > 0:
|
| 178 |
+
generation_kwargs["temperature"] = temperature
|
| 179 |
+
generated = model.language_model.generate(
|
| 180 |
+
inputs_embeds=encoded.prefix.inputs_embeds,
|
| 181 |
+
attention_mask=encoded.prefix.attention_mask,
|
| 182 |
+
**generation_kwargs,
|
| 183 |
+
)
|
| 184 |
+
text = model.tokenizer.batch_decode(generated, skip_special_tokens=True)[0].strip()
|
| 185 |
+
candidate_relations = _candidate_relations_from_answer(text)
|
| 186 |
+
structured_safety = _safety_prediction(encoded.safety)
|
| 187 |
+
structured_safety["inference_ablation"] = {
|
| 188 |
+
"name": getattr(model, "inference_ssa_ablation", "none"),
|
| 189 |
+
"scope": "diagnostic",
|
| 190 |
+
"paper_trained_ablation_claim": False,
|
| 191 |
+
}
|
| 192 |
+
if candidate_relations is not None:
|
| 193 |
+
structured_safety["candidate_relations"] = candidate_relations
|
| 194 |
+
return {
|
| 195 |
+
"prompt": prompt,
|
| 196 |
+
"answer": text,
|
| 197 |
+
"safety": structured_safety,
|
| 198 |
+
}, encoded
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _resolved_region_payload(
|
| 202 |
+
model: FalconModel,
|
| 203 |
+
text: str,
|
| 204 |
+
detector_output: Any,
|
| 205 |
+
) -> tuple[dict[str, Any], np.ndarray]:
|
| 206 |
+
resolved = model.resolve_region_references(text, detector_output)
|
| 207 |
+
region_ids = [region.proposal_index for region in resolved]
|
| 208 |
+
boxes = (
|
| 209 |
+
torch.stack([region.box_xyxy for region in resolved]).float().cpu()
|
| 210 |
+
if resolved
|
| 211 |
+
else torch.empty(0, 4)
|
| 212 |
+
)
|
| 213 |
+
scores = [
|
| 214 |
+
None if region.score is None else float(region.score.float().cpu())
|
| 215 |
+
for region in resolved
|
| 216 |
+
]
|
| 217 |
+
if resolved:
|
| 218 |
+
binary_masks = []
|
| 219 |
+
for region in resolved:
|
| 220 |
+
mask = region.mask.detach()
|
| 221 |
+
if mask.dtype == torch.bool:
|
| 222 |
+
binary = mask
|
| 223 |
+
elif detector_output.masks_are_logits:
|
| 224 |
+
binary = mask >= 0.0
|
| 225 |
+
else:
|
| 226 |
+
binary = mask >= model.config.mask_probability_threshold
|
| 227 |
+
binary_masks.append(binary.to(dtype=torch.bool))
|
| 228 |
+
masks = torch.stack(binary_masks).cpu().numpy()
|
| 229 |
+
else:
|
| 230 |
+
image_sizes = getattr(detector_output, "image_sizes", None)
|
| 231 |
+
if image_sizes is None:
|
| 232 |
+
raise ValueError(
|
| 233 |
+
"detector output lacks native image size for empty prediction"
|
| 234 |
+
)
|
| 235 |
+
sizes = torch.as_tensor(image_sizes).detach().cpu()
|
| 236 |
+
if sizes.shape != (1, 2):
|
| 237 |
+
raise ValueError("single-image inference requires one detector image size")
|
| 238 |
+
height, width = (int(value) for value in sizes[0].tolist())
|
| 239 |
+
if height < 1 or width < 1:
|
| 240 |
+
raise ValueError("detector output contains an invalid native image size")
|
| 241 |
+
masks = np.empty((0, height, width), dtype=bool)
|
| 242 |
+
return {
|
| 243 |
+
"region_ids": region_ids,
|
| 244 |
+
"boxes_xyxy": boxes.tolist(),
|
| 245 |
+
"scores": scores,
|
| 246 |
+
}, masks
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@torch.inference_mode()
|
| 250 |
+
def predict(
|
| 251 |
+
model: FalconModel,
|
| 252 |
+
*,
|
| 253 |
+
image: Any,
|
| 254 |
+
prompt: str,
|
| 255 |
+
max_new_tokens: int = 128,
|
| 256 |
+
temperature: float = 0.0,
|
| 257 |
+
) -> tuple[dict[str, Any], np.ndarray]:
|
| 258 |
+
"""Run one live detector→Falcon→LLM pass without any cache dependency."""
|
| 259 |
+
|
| 260 |
+
result, encoded = _generate(
|
| 261 |
+
model,
|
| 262 |
+
image=image,
|
| 263 |
+
prompt=prompt,
|
| 264 |
+
max_new_tokens=max_new_tokens,
|
| 265 |
+
temperature=temperature,
|
| 266 |
+
)
|
| 267 |
+
regions, masks = _resolved_region_payload(
|
| 268 |
+
model,
|
| 269 |
+
result["answer"],
|
| 270 |
+
encoded.detector_output,
|
| 271 |
+
)
|
| 272 |
+
result.update(regions)
|
| 273 |
+
return result, masks
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
@torch.inference_mode()
|
| 277 |
+
def predict_segmentation(
|
| 278 |
+
model: FalconModel,
|
| 279 |
+
*,
|
| 280 |
+
image: Any,
|
| 281 |
+
prompt: str,
|
| 282 |
+
max_new_tokens: int = 128,
|
| 283 |
+
temperature: float = 0.0,
|
| 284 |
+
) -> tuple[dict[str, Any], SegmentationPrediction]:
|
| 285 |
+
"""Generate and strictly resolve ``<SEG>`` transport against live proposals."""
|
| 286 |
+
|
| 287 |
+
result, encoded = _generate(
|
| 288 |
+
model,
|
| 289 |
+
image=image,
|
| 290 |
+
prompt=prompt,
|
| 291 |
+
max_new_tokens=max_new_tokens,
|
| 292 |
+
temperature=temperature,
|
| 293 |
+
)
|
| 294 |
+
prediction = resolve_segmentation(
|
| 295 |
+
result["answer"],
|
| 296 |
+
encoded.detector_output,
|
| 297 |
+
max_regions=model.config.max_regions,
|
| 298 |
+
)
|
| 299 |
+
result["region_ids"] = list(prediction.region_indices)
|
| 300 |
+
return result, prediction
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@torch.inference_mode()
|
| 304 |
+
def predict_panoptic(
|
| 305 |
+
model: FalconModel,
|
| 306 |
+
*,
|
| 307 |
+
image: Any,
|
| 308 |
+
prompt: str,
|
| 309 |
+
category_ids: Collection[int],
|
| 310 |
+
query_category_id: int | None = None,
|
| 311 |
+
max_new_tokens: int = 128,
|
| 312 |
+
temperature: float = 0.0,
|
| 313 |
+
) -> tuple[dict[str, Any], PanopticPrediction]:
|
| 314 |
+
"""Generate panoptic instances, optionally selecting a queried predicted class."""
|
| 315 |
+
|
| 316 |
+
if query_category_id is not None and (
|
| 317 |
+
isinstance(query_category_id, bool)
|
| 318 |
+
or not isinstance(query_category_id, int)
|
| 319 |
+
or query_category_id not in category_ids
|
| 320 |
+
):
|
| 321 |
+
raise ValueError("query_category_id must belong to the manifest category IDs")
|
| 322 |
+
|
| 323 |
+
result, encoded = _generate(
|
| 324 |
+
model,
|
| 325 |
+
image=image,
|
| 326 |
+
prompt=prompt,
|
| 327 |
+
max_new_tokens=max_new_tokens,
|
| 328 |
+
temperature=temperature,
|
| 329 |
+
)
|
| 330 |
+
prediction = resolve_panoptic(
|
| 331 |
+
result["answer"],
|
| 332 |
+
encoded.detector_output,
|
| 333 |
+
category_ids=category_ids,
|
| 334 |
+
max_regions=model.config.max_regions,
|
| 335 |
+
)
|
| 336 |
+
if query_category_id is not None:
|
| 337 |
+
prediction = select_panoptic_category(prediction, query_category_id)
|
| 338 |
+
result["region_ids"] = [instance.region_index for instance in prediction.instances]
|
| 339 |
+
result["segments_info"] = [dict(segment) for segment in prediction.segments_info]
|
| 340 |
+
return result, prediction
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"crop_size": {
|
| 3 |
+
"height": 448,
|
| 4 |
+
"width": 448
|
| 5 |
+
},
|
| 6 |
+
"do_center_crop": true,
|
| 7 |
+
"do_convert_rgb": true,
|
| 8 |
+
"do_normalize": true,
|
| 9 |
+
"do_rescale": true,
|
| 10 |
+
"do_resize": true,
|
| 11 |
+
"image_mean": [
|
| 12 |
+
0.485,
|
| 13 |
+
0.456,
|
| 14 |
+
0.406
|
| 15 |
+
],
|
| 16 |
+
"image_processor_type": "BitImageProcessor",
|
| 17 |
+
"image_std": [
|
| 18 |
+
0.229,
|
| 19 |
+
0.224,
|
| 20 |
+
0.225
|
| 21 |
+
],
|
| 22 |
+
"resample": 3,
|
| 23 |
+
"rescale_factor": 0.00392156862745098,
|
| 24 |
+
"size": {
|
| 25 |
+
"height": 448,
|
| 26 |
+
"width": 448
|
| 27 |
+
}
|
| 28 |
+
}
|
regions.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Patch aggregation and mask-aware region encoding for Falcon.
|
| 2 |
+
|
| 3 |
+
The detector contract used here is deliberately small: boxes are ``xyxy`` in
|
| 4 |
+
original-image coordinates and masks are either logits or probabilities. The
|
| 5 |
+
module never guesses which representation a mask uses and never applies a
|
| 6 |
+
sigmoid; it makes a deterministic binary mask with the declared threshold.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from collections.abc import Sequence
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from torch import Tensor, nn
|
| 15 |
+
from torch.nn import functional as F
|
| 16 |
+
from torchvision.ops import roi_align
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PatchTokenAggregator(nn.Module):
|
| 20 |
+
"""Concatenate each non-overlapping 2x2 patch block, then project it.
|
| 21 |
+
|
| 22 |
+
DINOv2 emits patch tokens in row-major order. For a block, concatenation
|
| 23 |
+
order is top-left, top-right, bottom-left, bottom-right.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, input_dim: int, output_dim: int) -> None:
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.input_dim = input_dim
|
| 29 |
+
self.output_dim = output_dim
|
| 30 |
+
self.projection = nn.Linear(4 * input_dim, output_dim)
|
| 31 |
+
|
| 32 |
+
def forward(self, patch_tokens: Tensor, grid_size: tuple[int, int]) -> Tensor:
|
| 33 |
+
if patch_tokens.ndim != 3:
|
| 34 |
+
raise ValueError("patch_tokens must have shape [batch, patches, channels]")
|
| 35 |
+
batch, count, channels = patch_tokens.shape
|
| 36 |
+
height, width = grid_size
|
| 37 |
+
if channels != self.input_dim:
|
| 38 |
+
raise ValueError(f"expected {self.input_dim} channels, received {channels}")
|
| 39 |
+
if count != height * width:
|
| 40 |
+
raise ValueError(f"{count} tokens cannot form the declared {height}x{width} grid")
|
| 41 |
+
if height % 2 or width % 2:
|
| 42 |
+
raise ValueError("the DINOv2 patch grid must be even for 2x2 aggregation")
|
| 43 |
+
|
| 44 |
+
grid = patch_tokens.reshape(batch, height // 2, 2, width // 2, 2, channels)
|
| 45 |
+
blocks = grid.permute(0, 1, 3, 2, 4, 5).reshape(
|
| 46 |
+
batch, height // 2, width // 2, 4 * channels
|
| 47 |
+
)
|
| 48 |
+
return self.projection(blocks).permute(0, 3, 1, 2).contiguous()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class MaskAwareRegionEncoder(nn.Module):
|
| 52 |
+
"""Fuse ROI-aligned and binary-mask-pooled features by concatenation.
|
| 53 |
+
|
| 54 |
+
Invalid/padded proposals are returned as exact zero vectors. ``image_sizes``
|
| 55 |
+
contains ``(height, width)`` for each original detector image.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
feature_dim: int,
|
| 61 |
+
output_dim: int,
|
| 62 |
+
*,
|
| 63 |
+
roi_size: int = 4,
|
| 64 |
+
sampling_ratio: int = 2,
|
| 65 |
+
probability_threshold: float = 0.5,
|
| 66 |
+
) -> None:
|
| 67 |
+
super().__init__()
|
| 68 |
+
if not 0.0 <= probability_threshold <= 1.0:
|
| 69 |
+
raise ValueError("probability_threshold must lie in [0, 1]")
|
| 70 |
+
self.feature_dim = feature_dim
|
| 71 |
+
self.output_dim = output_dim
|
| 72 |
+
self.roi_size = roi_size
|
| 73 |
+
self.sampling_ratio = sampling_ratio
|
| 74 |
+
self.probability_threshold = probability_threshold
|
| 75 |
+
self.projection = nn.Linear(2 * feature_dim, output_dim)
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def _validate_inputs(
|
| 79 |
+
feature_map: Tensor,
|
| 80 |
+
boxes: Tensor,
|
| 81 |
+
masks: Tensor,
|
| 82 |
+
image_sizes: Tensor,
|
| 83 |
+
valid: Tensor,
|
| 84 |
+
) -> None:
|
| 85 |
+
if feature_map.ndim != 4:
|
| 86 |
+
raise ValueError("feature_map must have shape [batch, channels, height, width]")
|
| 87 |
+
if boxes.ndim != 3 or boxes.shape[-1] != 4:
|
| 88 |
+
raise ValueError("boxes must have shape [batch, proposals, 4]")
|
| 89 |
+
if masks.ndim != 4 or masks.shape[:2] != boxes.shape[:2]:
|
| 90 |
+
raise ValueError("masks must have shape [batch, proposals, height, width]")
|
| 91 |
+
if image_sizes.shape != (feature_map.shape[0], 2):
|
| 92 |
+
raise ValueError("image_sizes must have shape [batch, 2] in (height, width) order")
|
| 93 |
+
if valid.shape != boxes.shape[:2]:
|
| 94 |
+
raise ValueError("valid must have shape [batch, proposals]")
|
| 95 |
+
if boxes.shape[0] != feature_map.shape[0]:
|
| 96 |
+
raise ValueError("feature_map and detector outputs must have the same batch size")
|
| 97 |
+
|
| 98 |
+
def pool_components(
|
| 99 |
+
self,
|
| 100 |
+
feature_map: Tensor,
|
| 101 |
+
boxes: Tensor,
|
| 102 |
+
masks: Tensor,
|
| 103 |
+
image_sizes: Tensor,
|
| 104 |
+
valid: Tensor | None = None,
|
| 105 |
+
*,
|
| 106 |
+
masks_are_logits: bool = False,
|
| 107 |
+
mask_sizes: Tensor | None = None,
|
| 108 |
+
) -> tuple[Tensor, Tensor, Tensor]:
|
| 109 |
+
"""Return valid proposal indices, ROI vectors, and mask vectors."""
|
| 110 |
+
|
| 111 |
+
if valid is None:
|
| 112 |
+
valid = torch.ones(boxes.shape[:2], dtype=torch.bool, device=boxes.device)
|
| 113 |
+
else:
|
| 114 |
+
valid = valid.to(dtype=torch.bool)
|
| 115 |
+
self._validate_inputs(feature_map, boxes, masks, image_sizes, valid)
|
| 116 |
+
|
| 117 |
+
indices = valid.nonzero(as_tuple=False)
|
| 118 |
+
if indices.numel() == 0:
|
| 119 |
+
empty = feature_map.new_empty((0, self.feature_dim))
|
| 120 |
+
return indices, empty, empty
|
| 121 |
+
|
| 122 |
+
device = feature_map.device
|
| 123 |
+
dtype = feature_map.dtype
|
| 124 |
+
indices = indices.to(device=device)
|
| 125 |
+
boxes = boxes.to(device=device, dtype=dtype)
|
| 126 |
+
masks = masks.to(device=device)
|
| 127 |
+
image_sizes = image_sizes.to(device=device, dtype=dtype)
|
| 128 |
+
|
| 129 |
+
batch_ids, proposal_ids = indices.unbind(dim=1)
|
| 130 |
+
selected_boxes = boxes[batch_ids, proposal_ids].clone()
|
| 131 |
+
source_h = image_sizes[batch_ids, 0].clamp_min(1)
|
| 132 |
+
source_w = image_sizes[batch_ids, 1].clamp_min(1)
|
| 133 |
+
feature_h, feature_w = feature_map.shape[-2:]
|
| 134 |
+
selected_boxes[:, 0::2] *= feature_w / source_w[:, None]
|
| 135 |
+
selected_boxes[:, 1::2] *= feature_h / source_h[:, None]
|
| 136 |
+
rois = torch.cat((batch_ids.to(dtype=dtype).unsqueeze(1), selected_boxes), dim=1)
|
| 137 |
+
|
| 138 |
+
roi_features = roi_align(
|
| 139 |
+
feature_map,
|
| 140 |
+
rois,
|
| 141 |
+
output_size=(self.roi_size, self.roi_size),
|
| 142 |
+
spatial_scale=1.0,
|
| 143 |
+
sampling_ratio=self.sampling_ratio,
|
| 144 |
+
aligned=True,
|
| 145 |
+
)
|
| 146 |
+
roi_vectors = roi_features.mean(dim=(-2, -1))
|
| 147 |
+
|
| 148 |
+
threshold = 0.0 if masks_are_logits else self.probability_threshold
|
| 149 |
+
if mask_sizes is None:
|
| 150 |
+
mask_sizes = torch.tensor([list(masks.shape[-2:])] * feature_map.shape[0])
|
| 151 |
+
if mask_sizes.shape != (feature_map.shape[0], 2):
|
| 152 |
+
raise ValueError("mask_sizes must have shape [batch, 2]")
|
| 153 |
+
binary_masks = feature_map.new_zeros((len(indices), 1, feature_h, feature_w))
|
| 154 |
+
for batch_index in range(feature_map.shape[0]):
|
| 155 |
+
selected = batch_ids == batch_index
|
| 156 |
+
if not bool(selected.any()):
|
| 157 |
+
continue
|
| 158 |
+
height, width = (int(value) for value in mask_sizes[batch_index])
|
| 159 |
+
if not (0 < height <= masks.shape[-2] and 0 < width <= masks.shape[-1]):
|
| 160 |
+
raise ValueError("mask_sizes exceed the padded canvas or are nonpositive")
|
| 161 |
+
selected_masks = masks[batch_index, proposal_ids[selected], :height, :width].unsqueeze(
|
| 162 |
+
1
|
| 163 |
+
)
|
| 164 |
+
binary = (
|
| 165 |
+
selected_masks
|
| 166 |
+
if selected_masks.dtype == torch.bool
|
| 167 |
+
else selected_masks >= threshold
|
| 168 |
+
)
|
| 169 |
+
binary_masks[selected] = F.interpolate(
|
| 170 |
+
binary.to(dtype=dtype), size=(feature_h, feature_w), mode="nearest"
|
| 171 |
+
).to(dtype=dtype)
|
| 172 |
+
selected_features = feature_map.index_select(0, batch_ids)
|
| 173 |
+
denominator = binary_masks.sum(dim=(-2, -1)).clamp_min(1.0)
|
| 174 |
+
mask_vectors = (selected_features * binary_masks).sum(dim=(-2, -1)) / denominator
|
| 175 |
+
return indices, roi_vectors, mask_vectors
|
| 176 |
+
|
| 177 |
+
def forward(
|
| 178 |
+
self,
|
| 179 |
+
feature_map: Tensor,
|
| 180 |
+
boxes: Tensor,
|
| 181 |
+
masks: Tensor,
|
| 182 |
+
image_sizes: Tensor | Sequence[tuple[int, int]],
|
| 183 |
+
valid: Tensor | None = None,
|
| 184 |
+
*,
|
| 185 |
+
masks_are_logits: bool = False,
|
| 186 |
+
mask_sizes: Tensor | None = None,
|
| 187 |
+
) -> Tensor:
|
| 188 |
+
if not isinstance(image_sizes, Tensor):
|
| 189 |
+
image_sizes = torch.as_tensor(image_sizes, device=feature_map.device)
|
| 190 |
+
if valid is None:
|
| 191 |
+
valid = torch.ones(boxes.shape[:2], dtype=torch.bool, device=boxes.device)
|
| 192 |
+
indices, roi_vectors, mask_vectors = self.pool_components(
|
| 193 |
+
feature_map,
|
| 194 |
+
boxes,
|
| 195 |
+
masks,
|
| 196 |
+
image_sizes,
|
| 197 |
+
valid,
|
| 198 |
+
masks_are_logits=masks_are_logits,
|
| 199 |
+
mask_sizes=mask_sizes,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
batch, proposals = boxes.shape[:2]
|
| 203 |
+
flat_output = feature_map.new_zeros((batch * proposals, self.output_dim))
|
| 204 |
+
if indices.numel() == 0:
|
| 205 |
+
return flat_output.reshape(batch, proposals, self.output_dim)
|
| 206 |
+
|
| 207 |
+
fused = self.projection(torch.cat((roi_vectors, mask_vectors), dim=-1))
|
| 208 |
+
flat_indices = indices[:, 0] * proposals + indices[:, 1]
|
| 209 |
+
flat_output = flat_output.index_copy(0, flat_indices, fused)
|
| 210 |
+
return flat_output.reshape(batch, proposals, self.output_dim)
|
safety.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Structured Safety Adapter (SSA) with binary presence supervision."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import Tensor, nn
|
| 10 |
+
from torch.nn import functional as F
|
| 11 |
+
|
| 12 |
+
from .capabilities import ALL_SAFETY_HEADS, SafetyCapabilities
|
| 13 |
+
|
| 14 |
+
# These indices are the canonical Falcon-X annotation order. The paper's
|
| 15 |
+
# "main charge" is represented by the dataset label ``explosive``.
|
| 16 |
+
COMPONENT_TYPES = ("detonator", "explosive", "battery")
|
| 17 |
+
LINK_PAIRS = ((2, 0), (2, 1), (0, 1))
|
| 18 |
+
SAFETY_TOKEN_TYPES = (
|
| 19 |
+
"scene_risk",
|
| 20 |
+
"presence_detonator",
|
| 21 |
+
"presence_explosive",
|
| 22 |
+
"presence_battery",
|
| 23 |
+
"link_battery_detonator",
|
| 24 |
+
"link_battery_explosive",
|
| 25 |
+
"link_detonator_explosive",
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class SafetyOutput:
|
| 31 |
+
slots: Tensor
|
| 32 |
+
attention: Tensor
|
| 33 |
+
presence_logits: Tensor
|
| 34 |
+
presence_probabilities: Tensor
|
| 35 |
+
link_logits: Tensor
|
| 36 |
+
link_probabilities: Tensor
|
| 37 |
+
risk_logit: Tensor
|
| 38 |
+
risk_probability: Tensor
|
| 39 |
+
tokens: Tensor
|
| 40 |
+
capabilities: SafetyCapabilities = SafetyCapabilities()
|
| 41 |
+
|
| 42 |
+
def prediction(self, batch_index: int = 0) -> dict:
|
| 43 |
+
"""Serialize unavailable heads as null, never plausible-looking numbers."""
|
| 44 |
+
return {
|
| 45 |
+
"risk": float(self.risk_probability[batch_index].detach().cpu())
|
| 46 |
+
if self.capabilities.risk
|
| 47 |
+
else None,
|
| 48 |
+
"presence": [
|
| 49 |
+
float(value.detach().cpu()) if enabled else None
|
| 50 |
+
for value, enabled in zip(
|
| 51 |
+
self.presence_probabilities[batch_index],
|
| 52 |
+
self.capabilities.presence,
|
| 53 |
+
strict=True,
|
| 54 |
+
)
|
| 55 |
+
],
|
| 56 |
+
"links": [
|
| 57 |
+
float(value.detach().cpu()) if enabled else None
|
| 58 |
+
for value, enabled in zip(
|
| 59 |
+
self.link_probabilities[batch_index], self.capabilities.links, strict=True
|
| 60 |
+
)
|
| 61 |
+
],
|
| 62 |
+
"capabilities": self.capabilities.as_dict(),
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def scalar_values(self) -> Tensor:
|
| 67 |
+
"""Values in the seven-token paper order: risk, presence, links."""
|
| 68 |
+
|
| 69 |
+
return torch.cat(
|
| 70 |
+
(
|
| 71 |
+
self.risk_probability.unsqueeze(-1),
|
| 72 |
+
self.presence_probabilities,
|
| 73 |
+
self.link_probabilities,
|
| 74 |
+
),
|
| 75 |
+
dim=-1,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class StructuredSafetyAdapter(nn.Module):
|
| 80 |
+
"""Map a variable proposal set to three typed component slots and seven tokens."""
|
| 81 |
+
|
| 82 |
+
def __init__(
|
| 83 |
+
self,
|
| 84 |
+
model_dim: int,
|
| 85 |
+
token_dim: int,
|
| 86 |
+
*,
|
| 87 |
+
head_hidden_dim: int | None = None,
|
| 88 |
+
capabilities: SafetyCapabilities = ALL_SAFETY_HEADS,
|
| 89 |
+
) -> None:
|
| 90 |
+
super().__init__()
|
| 91 |
+
hidden_dim = head_hidden_dim or model_dim
|
| 92 |
+
self.model_dim = model_dim
|
| 93 |
+
self.token_dim = token_dim
|
| 94 |
+
self.capabilities = capabilities
|
| 95 |
+
self.component_queries = nn.Parameter(torch.empty(3, model_dim))
|
| 96 |
+
nn.init.normal_(self.component_queries, std=0.02)
|
| 97 |
+
|
| 98 |
+
self.presence_head = nn.Linear(model_dim, 1)
|
| 99 |
+
self.link_heads = nn.ModuleList(
|
| 100 |
+
[
|
| 101 |
+
nn.Sequential(
|
| 102 |
+
nn.Linear(2 * model_dim, hidden_dim),
|
| 103 |
+
nn.GELU(),
|
| 104 |
+
nn.Linear(hidden_dim, 1),
|
| 105 |
+
)
|
| 106 |
+
for _ in LINK_PAIRS
|
| 107 |
+
]
|
| 108 |
+
)
|
| 109 |
+
self.risk_head = nn.Sequential(
|
| 110 |
+
nn.Linear(3 * model_dim + len(LINK_PAIRS), hidden_dim),
|
| 111 |
+
nn.GELU(),
|
| 112 |
+
nn.Linear(hidden_dim, 1),
|
| 113 |
+
)
|
| 114 |
+
self.value_projection = nn.Linear(1, token_dim)
|
| 115 |
+
self.type_embedding = nn.Embedding(len(SAFETY_TOKEN_TYPES), token_dim)
|
| 116 |
+
self.freeze_unavailable_heads()
|
| 117 |
+
|
| 118 |
+
def freeze_unavailable_heads(self) -> None:
|
| 119 |
+
"""Reapply after a training-stage change has enabled the shared adapter."""
|
| 120 |
+
modules = [
|
| 121 |
+
(self.presence_head, any(self.capabilities.presence)),
|
| 122 |
+
(self.risk_head, self.capabilities.risk),
|
| 123 |
+
]
|
| 124 |
+
modules.extend(zip(self.link_heads, self.capabilities.links, strict=True))
|
| 125 |
+
for module, enabled in modules:
|
| 126 |
+
if not enabled:
|
| 127 |
+
for parameter in module.parameters():
|
| 128 |
+
parameter.requires_grad = False
|
| 129 |
+
|
| 130 |
+
def _component_slots(
|
| 131 |
+
self,
|
| 132 |
+
region_embeddings: Tensor,
|
| 133 |
+
valid: Tensor,
|
| 134 |
+
) -> tuple[Tensor, Tensor]:
|
| 135 |
+
batch, proposals, channels = region_embeddings.shape
|
| 136 |
+
if channels != self.model_dim:
|
| 137 |
+
raise ValueError(f"expected region dimension {self.model_dim}, received {channels}")
|
| 138 |
+
if valid.shape != (batch, proposals):
|
| 139 |
+
raise ValueError("valid must have shape [batch, proposals]")
|
| 140 |
+
if proposals == 0:
|
| 141 |
+
slots = region_embeddings.new_zeros((batch, 3, channels))
|
| 142 |
+
attention = region_embeddings.new_zeros((batch, 3, 0))
|
| 143 |
+
return slots, attention
|
| 144 |
+
|
| 145 |
+
scores = torch.einsum("cd,bnd->bcn", self.component_queries, region_embeddings)
|
| 146 |
+
scores = scores / math.sqrt(float(channels))
|
| 147 |
+
expanded_valid = valid[:, None, :].to(dtype=torch.bool)
|
| 148 |
+
scores = scores.masked_fill(~expanded_valid, torch.finfo(scores.dtype).min)
|
| 149 |
+
attention = scores.softmax(dim=-1) * expanded_valid.to(dtype=scores.dtype)
|
| 150 |
+
attention = attention / attention.sum(dim=-1, keepdim=True).clamp_min(1e-12)
|
| 151 |
+
slots = torch.einsum("bcn,bnd->bcd", attention, region_embeddings)
|
| 152 |
+
return slots, attention
|
| 153 |
+
|
| 154 |
+
def forward(
|
| 155 |
+
self,
|
| 156 |
+
region_embeddings: Tensor,
|
| 157 |
+
valid: Tensor | None = None,
|
| 158 |
+
) -> SafetyOutput:
|
| 159 |
+
if region_embeddings.ndim != 3:
|
| 160 |
+
raise ValueError("region_embeddings must have shape [batch, proposals, channels]")
|
| 161 |
+
if valid is None:
|
| 162 |
+
valid = torch.ones(
|
| 163 |
+
region_embeddings.shape[:2],
|
| 164 |
+
dtype=torch.bool,
|
| 165 |
+
device=region_embeddings.device,
|
| 166 |
+
)
|
| 167 |
+
else:
|
| 168 |
+
valid = valid.to(device=region_embeddings.device, dtype=torch.bool)
|
| 169 |
+
|
| 170 |
+
slots, attention = self._component_slots(region_embeddings, valid)
|
| 171 |
+
presence_logits = (
|
| 172 |
+
self.presence_head(slots).squeeze(-1)
|
| 173 |
+
if any(self.capabilities.presence)
|
| 174 |
+
else slots.new_zeros(slots.shape[:2])
|
| 175 |
+
)
|
| 176 |
+
link_logits = torch.stack(
|
| 177 |
+
[
|
| 178 |
+
head(torch.cat((slots[:, left], slots[:, right]), dim=-1)).squeeze(-1)
|
| 179 |
+
if enabled
|
| 180 |
+
else slots.new_zeros(slots.shape[0])
|
| 181 |
+
for head, (left, right), enabled in zip(
|
| 182 |
+
self.link_heads, LINK_PAIRS, self.capabilities.links, strict=True
|
| 183 |
+
)
|
| 184 |
+
],
|
| 185 |
+
dim=-1,
|
| 186 |
+
)
|
| 187 |
+
link_probabilities = link_logits.sigmoid() * slots.new_tensor(self.capabilities.links)
|
| 188 |
+
|
| 189 |
+
# Risk is explicitly conditioned on every slot and the three inferred links.
|
| 190 |
+
risk_input = torch.cat((slots.flatten(start_dim=1), link_probabilities), dim=-1)
|
| 191 |
+
risk_logit = (
|
| 192 |
+
self.risk_head(risk_input).squeeze(-1)
|
| 193 |
+
if self.capabilities.risk
|
| 194 |
+
else slots.new_zeros(slots.shape[0])
|
| 195 |
+
)
|
| 196 |
+
risk_probability = risk_logit.sigmoid() * int(self.capabilities.risk)
|
| 197 |
+
presence_probabilities = presence_logits.sigmoid() * slots.new_tensor(
|
| 198 |
+
self.capabilities.presence
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
scalar_values = torch.cat(
|
| 202 |
+
(
|
| 203 |
+
risk_probability.unsqueeze(-1),
|
| 204 |
+
presence_probabilities,
|
| 205 |
+
link_probabilities,
|
| 206 |
+
),
|
| 207 |
+
dim=-1,
|
| 208 |
+
)
|
| 209 |
+
type_ids = torch.tensor(
|
| 210 |
+
self.capabilities.token_indices, device=slots.device, dtype=torch.long
|
| 211 |
+
)
|
| 212 |
+
tokens = self.value_projection(scalar_values[:, type_ids].unsqueeze(-1))
|
| 213 |
+
tokens = tokens + self.type_embedding(type_ids).unsqueeze(0)
|
| 214 |
+
return SafetyOutput(
|
| 215 |
+
slots=slots,
|
| 216 |
+
attention=attention,
|
| 217 |
+
presence_logits=presence_logits,
|
| 218 |
+
presence_probabilities=presence_probabilities,
|
| 219 |
+
link_logits=link_logits,
|
| 220 |
+
link_probabilities=link_probabilities,
|
| 221 |
+
risk_logit=risk_logit,
|
| 222 |
+
risk_probability=risk_probability,
|
| 223 |
+
tokens=tokens,
|
| 224 |
+
capabilities=self.capabilities,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def loss(
|
| 229 |
+
output: SafetyOutput,
|
| 230 |
+
*,
|
| 231 |
+
risk: Tensor,
|
| 232 |
+
presence: Tensor,
|
| 233 |
+
links: Tensor,
|
| 234 |
+
) -> dict[str, Tensor]:
|
| 235 |
+
"""Stage-2/3 objective: presence BCE with logits and risk/link mean L1."""
|
| 236 |
+
|
| 237 |
+
risk_target = risk.to(output.risk_probability).reshape_as(output.risk_probability)
|
| 238 |
+
presence_logits = output.presence_logits.float()
|
| 239 |
+
presence_target = presence.to(
|
| 240 |
+
device=presence_logits.device, dtype=torch.float32
|
| 241 |
+
).reshape_as(presence_logits)
|
| 242 |
+
link_target = links.to(output.link_probabilities).reshape_as(output.link_probabilities)
|
| 243 |
+
if not output.capabilities.risk:
|
| 244 |
+
risk_target = torch.full_like(risk_target, float("nan"))
|
| 245 |
+
presence_target = presence_target.masked_fill(
|
| 246 |
+
~torch.tensor(output.capabilities.presence, device=presence_target.device), float("nan")
|
| 247 |
+
)
|
| 248 |
+
link_target = link_target.masked_fill(
|
| 249 |
+
~torch.tensor(output.capabilities.links, device=link_target.device), float("nan")
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def finite_l1(prediction: Tensor, target: Tensor) -> Tensor:
|
| 253 |
+
finite = torch.isfinite(target)
|
| 254 |
+
if finite.any():
|
| 255 |
+
return F.l1_loss(prediction[finite], target[finite])
|
| 256 |
+
# Keep a differentiable zero when a sample has no annotated links.
|
| 257 |
+
return prediction.sum() * 0.0
|
| 258 |
+
|
| 259 |
+
risk_loss = finite_l1(output.risk_probability, risk_target)
|
| 260 |
+
observed_presence = torch.isfinite(presence_target)
|
| 261 |
+
if observed_presence.any():
|
| 262 |
+
presence_loss = F.binary_cross_entropy_with_logits(
|
| 263 |
+
presence_logits[observed_presence], presence_target[observed_presence]
|
| 264 |
+
)
|
| 265 |
+
else:
|
| 266 |
+
presence_loss = presence_logits.sum() * 0.0
|
| 267 |
+
link_loss = finite_l1(output.link_probabilities, link_target)
|
| 268 |
+
return {
|
| 269 |
+
"risk": risk_loss,
|
| 270 |
+
"presence": presence_loss,
|
| 271 |
+
"links": link_loss,
|
| 272 |
+
}
|
tokenizer/special_tokens_map.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token": {
|
| 3 |
+
"content": "<s>",
|
| 4 |
+
"lstrip": false,
|
| 5 |
+
"normalized": false,
|
| 6 |
+
"rstrip": false,
|
| 7 |
+
"single_word": false
|
| 8 |
+
},
|
| 9 |
+
"eos_token": {
|
| 10 |
+
"content": "</s>",
|
| 11 |
+
"lstrip": false,
|
| 12 |
+
"normalized": false,
|
| 13 |
+
"rstrip": false,
|
| 14 |
+
"single_word": false
|
| 15 |
+
},
|
| 16 |
+
"pad_token": {
|
| 17 |
+
"content": "<unk>",
|
| 18 |
+
"lstrip": false,
|
| 19 |
+
"normalized": false,
|
| 20 |
+
"rstrip": false,
|
| 21 |
+
"single_word": false
|
| 22 |
+
},
|
| 23 |
+
"unk_token": {
|
| 24 |
+
"content": "<unk>",
|
| 25 |
+
"lstrip": false,
|
| 26 |
+
"normalized": false,
|
| 27 |
+
"rstrip": false,
|
| 28 |
+
"single_word": false
|
| 29 |
+
}
|
| 30 |
+
}
|
tokenizer/tokenizer.model
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e556afd44213b6bd1be2b850ebbbd98f5481437a8021afaf58ee7fb1818d347
|
| 3 |
+
size 499723
|
tokenizer/tokenizer_config.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_bos_token": true,
|
| 3 |
+
"add_eos_token": false,
|
| 4 |
+
"add_prefix_space": true,
|
| 5 |
+
"added_tokens_decoder": {
|
| 6 |
+
"0": {
|
| 7 |
+
"content": "<unk>",
|
| 8 |
+
"lstrip": false,
|
| 9 |
+
"normalized": false,
|
| 10 |
+
"rstrip": false,
|
| 11 |
+
"single_word": false,
|
| 12 |
+
"special": true
|
| 13 |
+
},
|
| 14 |
+
"1": {
|
| 15 |
+
"content": "<s>",
|
| 16 |
+
"lstrip": false,
|
| 17 |
+
"normalized": false,
|
| 18 |
+
"rstrip": false,
|
| 19 |
+
"single_word": false,
|
| 20 |
+
"special": true
|
| 21 |
+
},
|
| 22 |
+
"2": {
|
| 23 |
+
"content": "</s>",
|
| 24 |
+
"lstrip": false,
|
| 25 |
+
"normalized": false,
|
| 26 |
+
"rstrip": false,
|
| 27 |
+
"single_word": false,
|
| 28 |
+
"special": true
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"bos_token": "<s>",
|
| 32 |
+
"clean_up_tokenization_spaces": false,
|
| 33 |
+
"eos_token": "</s>",
|
| 34 |
+
"extra_special_tokens": {},
|
| 35 |
+
"legacy": false,
|
| 36 |
+
"model_max_length": 4096,
|
| 37 |
+
"pad_token": "<unk>",
|
| 38 |
+
"padding_side": "right",
|
| 39 |
+
"sp_model_kwargs": {},
|
| 40 |
+
"spaces_between_special_tokens": false,
|
| 41 |
+
"tokenizer_class": "LlamaTokenizer",
|
| 42 |
+
"unk_token": "<unk>",
|
| 43 |
+
"use_default_system_prompt": false
|
| 44 |
+
}
|