Image Segmentation
Transformers
Safetensors
concor1
feature-extraction
vision-language-grounding
concept-correspondence
referring-expression-segmentation
phrase-grounding
open-vocabulary-segmentation
custom_code
Instructions to use UWGZQ/ConCor-1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use UWGZQ/ConCor-1 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="UWGZQ/ConCor-1", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| # coding=utf-8 | |
| """Convert a ConCor-1 training checkpoint into this Hugging Face model repository. | |
| The research training code stores a checkpoint directory | |
| step{N}/model.pt # torch state dict of the training model | |
| step{N}/train/rank0.pt # training config + bridge token ids | |
| whose module names follow the internal research code. This script renames them | |
| to the released module names (which follow the paper) and writes a standard HF | |
| repository: `config.json`, `model.safetensors`, tokenizer and processor files. | |
| Renaming performed (research name -> released name): | |
| backbone.model.* -> backbone.* (`lm_head` is dropped: unused) | |
| fine_patch_decoder.branches.* -> vision_segmentation_head.patch_expander.branches.* | |
| subpatch_decoder.blocks.* -> vision_segmentation_head.convolutional_decoder.blocks.* | |
| fine_seg_head.vis_* -> vision_segmentation_head.mask_predictor.feature_proj.* | |
| fine_seg_head.bridge_* -> vision_segmentation_head.mask_predictor.bridge_proj.* | |
| fine_seg_head.bilinear -> vision_segmentation_head.mask_predictor.bilinear | |
| text_head.vis_* -> text_segmentation_head.feature_proj.* | |
| text_head.bridge_* -> text_segmentation_head.bridge_proj.* | |
| text_head.bilinear -> text_segmentation_head.bilinear | |
| presence_head.* -> presence_head.* (unchanged) | |
| Usage: | |
| python convert_original_checkpoint.py \ | |
| --checkpoint /path/to/step100000 \ | |
| --qwen3_5_path /path/to/Qwen3.5-0.8B \ | |
| --output_dir . | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import shutil | |
| from pathlib import Path | |
| from typing import Dict | |
| import torch | |
| from safetensors.torch import save_file | |
| from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5Config | |
| from configuration_concor1 import ConCor1Config | |
| # Architecture flags of the research config that the released code implements. | |
| # Anything else means the checkpoint is a different ablation and must not be | |
| # silently converted. | |
| EXPECTED_TRAINING_FLAGS = { | |
| "query_text_grounding": True, | |
| "presence_loss_type": "raw", | |
| "presence_mlp": True, | |
| "fine_patch_decoding": True, | |
| "use_subpatch_decoder": True, | |
| "use_pixel_decoder": False, | |
| "fine_decoder_swiglu": True, | |
| "add_vit_features": True, | |
| "add_where": "decoder_output", | |
| "vit_feature_layers": None, | |
| "multi_seg_loss": False, | |
| "bridge_self_attention": False, | |
| "bbox_bridge_encoding": False, | |
| "bidirectional_full_attention": True, | |
| "asymmetric_bridge_attention": False, | |
| "num_register_tokens": 0, | |
| "dot_product_similarity": False, | |
| "reset_position": False, | |
| "spatial_grid_assignment": False, | |
| "patch_size": 16, | |
| "merge_size": 2, | |
| "grounding_hidden_dim": 256, | |
| } | |
| TOKENIZER_FILES = [ | |
| "tokenizer.json", | |
| "tokenizer_config.json", | |
| "vocab.json", | |
| "merges.txt", | |
| ] | |
| _PROJECTION_SUFFIX = { | |
| "gate_proj.weight": "gate_proj.weight", | |
| "up_proj.weight": "up_proj.weight", | |
| "norm.weight": "norm.weight", | |
| } | |
| def rename_scorer_key(key: str, prefix: str, new_prefix: str) -> str: | |
| """Map a research bilinear-head key onto the released scorer layout.""" | |
| remainder = key[len(prefix) + 1 :] | |
| if remainder == "bilinear.weight": | |
| return f"{new_prefix}.bilinear.weight" | |
| for side, new_side in (("vis", "feature_proj"), ("bridge", "bridge_proj")): | |
| for old, new in ( | |
| (f"{side}_gate_proj.weight", "gate_proj.weight"), | |
| (f"{side}_up_proj.weight", "up_proj.weight"), | |
| (f"{side}_norm.weight", "norm.weight"), | |
| ): | |
| if remainder == old: | |
| return f"{new_prefix}.{new_side}.{new}" | |
| raise KeyError(f"Unexpected key in {prefix}: {key}") | |
| def convert_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: | |
| """Rename research module names to the released ones.""" | |
| converted: Dict[str, torch.Tensor] = {} | |
| dropped = [] | |
| for key, tensor in state_dict.items(): | |
| if key == "backbone.lm_head.weight": | |
| # ConCor-1 never decodes text, so the tied LM head is not part of the | |
| # released model (it is also what the paper's parameter count reflects). | |
| dropped.append(key) | |
| continue | |
| if key.startswith("subpatch_decoder.block1.") or key.startswith("subpatch_decoder.block2."): | |
| # The research module aliases its two blocks both as `block{1,2}` and as | |
| # `blocks.{0,1}`; keep one copy. | |
| dropped.append(key) | |
| continue | |
| if key.startswith("backbone.model."): | |
| new_key = "backbone." + key[len("backbone.model.") :] | |
| elif key.startswith("fine_patch_decoder.branches."): | |
| new_key = "vision_segmentation_head.patch_expander." + key[len("fine_patch_decoder.") :] | |
| elif key.startswith("subpatch_decoder.blocks."): | |
| new_key = ( | |
| "vision_segmentation_head.convolutional_decoder." + key[len("subpatch_decoder.") :] | |
| ) | |
| elif key.startswith("fine_seg_head."): | |
| new_key = rename_scorer_key( | |
| key, "fine_seg_head", "vision_segmentation_head.mask_predictor" | |
| ) | |
| elif key.startswith("text_head."): | |
| new_key = rename_scorer_key(key, "text_head", "text_segmentation_head") | |
| elif key.startswith("presence_head."): | |
| new_key = key | |
| else: | |
| raise KeyError(f"Unhandled checkpoint key: {key}") | |
| converted[new_key] = tensor | |
| print(f"Renamed {len(converted)} tensors, dropped {len(dropped)}: {sorted(dropped)[:4]} ...") | |
| return converted | |
| def build_config(training_config: dict, bridge_token_ids: list, qwen3_5_path: Path, vocab_size: int) -> ConCor1Config: | |
| """Build the released config from the training config + the Qwen3.5 architecture.""" | |
| for field, expected in EXPECTED_TRAINING_FLAGS.items(): | |
| actual = training_config.get(field, "<missing>") | |
| if actual != expected: | |
| raise ValueError( | |
| f"Training config field {field!r} is {actual!r}, expected {expected!r}. " | |
| "This checkpoint is not the architecture the released code implements." | |
| ) | |
| backbone_config = Qwen3_5Config.from_pretrained(qwen3_5_path) | |
| # The embedding table was grown to host the bridge-token ids. | |
| backbone_config.vocab_size = vocab_size | |
| backbone_config.text_config.vocab_size = vocab_size | |
| # The backbone is used as an encoder, so it is not a Qwen3.5 generator anymore. | |
| backbone_config.architectures = None | |
| grid_levels = training_config["multiscale_bridge_levels"] | |
| num_bridge_tokens = training_config["num_bridges"] | |
| if grid_levels is None or sum(level ** 2 for level in grid_levels) != num_bridge_tokens: | |
| raise ValueError( | |
| f"multiscale_bridge_levels={grid_levels} is inconsistent with num_bridges={num_bridge_tokens}." | |
| ) | |
| if bridge_token_ids != list(range(bridge_token_ids[0], bridge_token_ids[0] + num_bridge_tokens)): | |
| raise ValueError("Bridge token ids are expected to be a contiguous range.") | |
| return ConCor1Config( | |
| backbone_config=backbone_config, | |
| num_bridge_tokens=num_bridge_tokens, | |
| bridge_grid_levels=grid_levels, | |
| bridge_token_id_start=bridge_token_ids[0], | |
| correspondence_dim=training_config["grounding_hidden_dim"], | |
| presence_hidden_dim=training_config["grounding_hidden_dim"], | |
| patch_size=training_config["patch_size"], | |
| merge_size=training_config["merge_size"], | |
| num_mask_upsample_blocks=2, # SubpatchDecoder: two 2x blocks -> 4 px cells | |
| fuse_vision_encoder_features=training_config["add_vit_features"], | |
| bidirectional_full_attention=training_config["bidirectional_full_attention"], | |
| image_min_pixels=training_config["min_pixels"], | |
| image_max_pixels=training_config["max_pixels"], | |
| dtype="bfloat16", | |
| architectures=["ConCor1ForConceptCorrespondence"], | |
| auto_map={ | |
| "AutoConfig": "configuration_concor1.ConCor1Config", | |
| "AutoModel": "modeling_concor1.ConCor1ForConceptCorrespondence", | |
| "AutoProcessor": "processing_concor1.ConCor1Processor", | |
| }, | |
| ) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--checkpoint", required=True, type=Path, help="Training checkpoint directory (step{N})") | |
| parser.add_argument("--qwen3_5_path", required=True, type=Path, help="Qwen3.5-0.8B directory (config + tokenizer)") | |
| parser.add_argument("--output_dir", default=Path("."), type=Path) | |
| parser.add_argument( | |
| "--dtype", | |
| default="keep", | |
| choices=["keep", "bfloat16", "float32"], | |
| help=( | |
| "dtype of the released weights. 'keep' (default) preserves the checkpoint exactly — " | |
| "bf16 backbone, fp32 prediction heads — which is what the released model reproduces " | |
| "via `_keep_in_fp32_modules_strict`. 'bfloat16' halves the head weights' precision " | |
| "(image masks then agree with the checkpoint at ~0.995 IoU)." | |
| ), | |
| ) | |
| args = parser.parse_args() | |
| train_state_path = args.checkpoint / "train" / "rank0.pt" | |
| if not train_state_path.exists(): | |
| train_state_path = args.checkpoint / "train_state.pt" | |
| train_state = torch.load(train_state_path, map_location="cpu", weights_only=False) | |
| training_config = train_state["config"] | |
| bridge_token_ids = train_state["bridge_token_ids"] | |
| print( | |
| f"Loaded training state from {train_state_path} " | |
| f"(step {train_state.get('global_step')}, {len(bridge_token_ids)} bridge tokens)" | |
| ) | |
| state_dict = torch.load(args.checkpoint / "model.pt", map_location="cpu", weights_only=True) | |
| vocab_size = state_dict["backbone.model.language_model.embed_tokens.weight"].shape[0] | |
| converted = convert_state_dict(state_dict) | |
| if args.dtype != "keep": | |
| target_dtype = getattr(torch, args.dtype) | |
| converted = { | |
| key: tensor.to(target_dtype) if tensor.is_floating_point() else tensor | |
| for key, tensor in converted.items() | |
| } | |
| config = build_config(training_config, bridge_token_ids, args.qwen3_5_path, vocab_size) | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| config.save_pretrained(args.output_dir) | |
| save_file( | |
| {key: tensor.contiguous() for key, tensor in converted.items()}, | |
| args.output_dir / "model.safetensors", | |
| metadata={"format": "pt"}, | |
| ) | |
| total_parameters = sum(tensor.numel() for tensor in converted.values()) | |
| print(f"Wrote model.safetensors — {len(converted)} tensors, {total_parameters / 1e6:.2f} M parameters") | |
| # Tokenizer: Qwen3.5's, unchanged. Bridge tokens are unused vocabulary slots | |
| # with no string form, so the tokenizer needs no new entries. | |
| for filename in TOKENIZER_FILES: | |
| source = args.qwen3_5_path / filename | |
| if source.exists(): | |
| shutil.copy2(source, args.output_dir / filename) | |
| tokenizer_config_path = args.output_dir / "tokenizer_config.json" | |
| tokenizer_config = json.loads(tokenizer_config_path.read_text()) | |
| tokenizer_config["processor_class"] = "ConCor1Processor" | |
| tokenizer_config_path.write_text(json.dumps(tokenizer_config, indent=2, ensure_ascii=False) + "\n") | |
| # Image processor: Qwen3.5's, with ConCor-1's fixed pixel budget. | |
| image_processor_config = json.loads((args.qwen3_5_path / "preprocessor_config.json").read_text()) | |
| image_processor_config["processor_class"] = "ConCor1Processor" | |
| image_processor_config["min_pixels"] = config.image_min_pixels | |
| image_processor_config["max_pixels"] = config.image_max_pixels | |
| (args.output_dir / "preprocessor_config.json").write_text( | |
| json.dumps(image_processor_config, indent=2) + "\n" | |
| ) | |
| processor_config = { | |
| "processor_class": "ConCor1Processor", | |
| "auto_map": {"AutoProcessor": "processing_concor1.ConCor1Processor"}, | |
| "num_bridge_tokens": config.num_bridge_tokens, | |
| "bridge_token_id_start": config.bridge_token_id_start, | |
| "patch_size": config.patch_size, | |
| "merge_size": config.merge_size, | |
| "num_mask_upsample_blocks": config.num_mask_upsample_blocks, | |
| "min_pixels": config.image_min_pixels, | |
| "max_pixels": config.image_max_pixels, | |
| "presence_threshold": config.presence_threshold, | |
| "text_threshold": config.text_threshold, | |
| "image_threshold": config.image_threshold, | |
| "nms_iou_threshold": config.nms_iou_threshold, | |
| } | |
| (args.output_dir / "processor_config.json").write_text( | |
| json.dumps(processor_config, indent=2) + "\n" | |
| ) | |
| print(f"Wrote config.json, processor_config.json, preprocessor_config.json and tokenizer files to {args.output_dir}") | |
| if __name__ == "__main__": | |
| main() | |