Text Generation
Transformers
Safetensors
English
cxrmate-2
chest X-ray report generation
radiology report generation
image captioning
chest X-ray
X-ray
radiology
cxrmate
cxrmate-ed
cxrmate-rrg24
report
radiology report
multimodal
patient data
mimic-cxr
custom_code
Instructions to use aehrc/cxrmate-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use aehrc/cxrmate-2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="aehrc/cxrmate-2", trust_remote_code=True)# Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("aehrc/cxrmate-2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use aehrc/cxrmate-2 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "aehrc/cxrmate-2" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "aehrc/cxrmate-2", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/aehrc/cxrmate-2
- SGLang
How to use aehrc/cxrmate-2 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "aehrc/cxrmate-2" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "aehrc/cxrmate-2", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "aehrc/cxrmate-2" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "aehrc/cxrmate-2", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use aehrc/cxrmate-2 with Docker Model Runner:
docker model run hf.co/aehrc/cxrmate-2
| import warnings | |
| from typing import List, Optional, Tuple, Union | |
| import torch | |
| from torch import nn | |
| from transformers import AutoBackbone, AutoModelForCausalLM, RoFormerConfig | |
| from transformers.generation import GenerationMixin | |
| from transformers.modeling_utils import PreTrainedModel | |
| from transformers.models.llava.modeling_llava import LlavaCausalLMOutputWithPast | |
| from transformers.models.roformer.modeling_roformer import RoFormerLayer | |
| from transformers.utils import check_min_version, logging | |
| try: | |
| from .configuration_cxrmate2 import CXRMate2Config | |
| except ImportError: | |
| from configuration_cxrmate2 import CXRMate2Config | |
| logger = logging.get_logger(__name__) | |
| class CXRMate2FNNEncoder(torch.nn.Module): | |
| def __init__(self, num_features, intermediate_size, hidden_size): | |
| super().__init__() | |
| self.up_proj = torch.nn.Linear(num_features, intermediate_size, bias=False) | |
| self.down_proj = torch.nn.Linear(intermediate_size, hidden_size, bias=False) | |
| self.act_fn = torch.nn.GELU() | |
| def forward(self, x): | |
| return self.down_proj(self.act_fn(self.up_proj(x))) | |
| class CXRMate2QAdapter(torch.nn.Module): # Inspired by the Perceiver Resampler (e.g., used by Flamingo). | |
| def __init__(self, config): | |
| super().__init__() | |
| self.num_q_adapter_queries = config.num_q_adapter_queries | |
| # https://huggingface.co/docs/transformers/en/model_doc/roformer#transformers.RoFormerConfig | |
| roformer_config = RoFormerConfig( | |
| num_hidden_layers=config.num_q_adapter_layers, | |
| max_position_embeddings=config.num_q_adapter_positions, | |
| is_decoder=False | |
| ) | |
| self.queries = torch.nn.Parameter(torch.empty(config.num_q_adapter_queries, roformer_config.hidden_size)) | |
| torch.nn.init.xavier_uniform_(self.queries) | |
| self.layers = torch.nn.ModuleList( | |
| [RoFormerLayer(roformer_config) for _ in range(roformer_config.num_hidden_layers)] | |
| ) | |
| self.projection = torch.nn.Linear(roformer_config.hidden_size, config.text_config.hidden_size) | |
| def forward(self, x): | |
| queries = self.queries.unsqueeze(0).expand(x.shape[0], -1, -1) | |
| x = torch.cat((queries, x), dim=1) | |
| for layer in self.layers: | |
| x, *_ = layer(hidden_states=x) | |
| x = x[:, :self.num_q_adapter_queries, :] | |
| x = self.projection(x) | |
| return x | |
| class CXRMate2PreTrainedModel(PreTrainedModel): | |
| config: CXRMate2Config | |
| base_model_prefix = '' | |
| supports_gradient_checkpointing = True | |
| _skip_keys_device_placement = 'past_key_values' | |
| _supports_flash_attn = False | |
| _supports_sdpa = False | |
| _can_compile_fullgraph = False | |
| _supports_flex_attn = False | |
| _supports_attention_backend = False | |
| class CXRMate2ForConditionalGeneration(CXRMate2PreTrainedModel, GenerationMixin): | |
| config_class = CXRMate2Config | |
| def __init__(self, config: CXRMate2Config) -> None: | |
| super(CXRMate2PreTrainedModel, self).__init__(config) | |
| assert self.config.sep_token_id is not None | |
| assert self.config.bos_token_id is not None | |
| self.permute_encoder_last_hidden_state = config.permute_encoder_last_hidden_state | |
| self.vision_tower = AutoBackbone.from_config( | |
| config.vision_config, | |
| torch_dtype=config.vision_config.torch_dtype, | |
| ) | |
| self.multi_modal_projector = CXRMate2QAdapter(config) | |
| self.vocab_size = config.text_config.vocab_size | |
| self.language_model = AutoModelForCausalLM.from_config( | |
| config.text_config, | |
| attn_implementation=config._attn_implementation, | |
| trust_remote_code=True, | |
| torch_dtype=config.text_config.torch_dtype, | |
| ) | |
| self.time_delta_encoder = CXRMate2FNNEncoder( | |
| num_features=1, | |
| intermediate_size=config.time_delta_encoder_intermediate_size, | |
| hidden_size=config.text_config.hidden_size, | |
| ) | |
| self.post_init() | |
| def get_input_embeddings(self): | |
| return self.language_model.get_input_embeddings() | |
| def get_image_features( | |
| self, pixel_values: torch.FloatTensor, vision_feature_layer: int, vision_feature_select_strategy: str | |
| ) -> torch.Tensor: | |
| # Flatten the batch and study_id dimensions: | |
| assert len(pixel_values.shape) == 5, 'pixel_values must be B, S, C, H, W, where S is the max number of images for a study in the batch.' | |
| image_outputs = self.vision_tower(pixel_values.view(-1, *pixel_values.shape[2:]), output_hidden_states=True) | |
| image_features = image_outputs.feature_maps[vision_feature_layer] | |
| # Flatten h x w: | |
| image_features = torch.flatten(image_features, 2) if image_features.dim() > 3 else image_features | |
| image_features = torch.permute(image_features, [0, 2, 1]) if self.permute_encoder_last_hidden_state else image_features | |
| if vision_feature_select_strategy == 'default': | |
| image_features = image_features[:, 1:] | |
| elif vision_feature_select_strategy == 'full': | |
| image_features = image_features | |
| else: | |
| raise ValueError(f'Unexpected select feature strategy: {self.config.vision_feature_select_strategy}') | |
| image_features = self.multi_modal_projector(image_features) | |
| # Concatenate the features for each chest X-ray: | |
| image_features = image_features.view(pixel_values.shape[0], -1, image_features.shape[-1]) | |
| return image_features | |
| def forward( | |
| self, | |
| token_type_ids: torch.LongTensor, | |
| time_deltas: torch.FloatTensor, | |
| time_deltas_mask: torch.FloatTensor, | |
| input_ids: torch.LongTensor, | |
| attention_mask: Optional[torch.Tensor], | |
| position_ids: Optional[torch.LongTensor], | |
| pixel_values: Optional[torch.FloatTensor] = None, | |
| initial_attention_mask: Optional[torch.Tensor] = None, | |
| past_key_values: Optional[List[torch.FloatTensor]] = None, | |
| inputs_embeds: Optional[torch.FloatTensor] = None, | |
| vision_feature_layer: Optional[int] = None, | |
| vision_feature_select_strategy: Optional[str] = None, | |
| labels: Optional[torch.LongTensor] = None, | |
| use_cache: Optional[bool] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| return_dict: Optional[bool] = None, | |
| cache_position: Optional[torch.LongTensor] = None, | |
| num_logits_to_keep: int = 0, | |
| ) -> Union[Tuple, LlavaCausalLMOutputWithPast]: | |
| output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions | |
| output_hidden_states = ( | |
| output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states | |
| ) | |
| return_dict = return_dict if return_dict is not None else self.config.use_return_dict | |
| vision_feature_layer = ( | |
| vision_feature_layer if vision_feature_layer is not None else self.config.vision_feature_layer | |
| ) | |
| vision_feature_select_strategy = ( | |
| vision_feature_select_strategy | |
| if vision_feature_select_strategy is not None | |
| else self.config.vision_feature_select_strategy | |
| ) | |
| if (input_ids is None) ^ (inputs_embeds is not None): | |
| raise ValueError('You must specify exactly one of input_ids or inputs_embeds') | |
| if pixel_values is not None and inputs_embeds is not None: | |
| raise ValueError( | |
| 'You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one' | |
| ) | |
| if inputs_embeds is None: | |
| inputs_embeds = self.get_input_embeddings()(input_ids) | |
| if pixel_values is not None: | |
| image_features = self.get_image_features( | |
| pixel_values=pixel_values, | |
| vision_feature_layer=vision_feature_layer, | |
| vision_feature_select_strategy=vision_feature_select_strategy, | |
| ) | |
| n_image_tokens = (input_ids == self.config.image_token_index).sum().item() | |
| n_image_features = image_features.shape[0] * image_features.shape[1] | |
| if n_image_tokens != n_image_features: | |
| raise ValueError( | |
| f'Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}' | |
| ) | |
| special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) | |
| special_image_mask = special_image_mask.expand_as(inputs_embeds).to(inputs_embeds.device) | |
| image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) | |
| inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) | |
| # Add token type embeddings: | |
| token_type_embeddings = self.get_input_embeddings()(token_type_ids) | |
| inputs_embeds += token_type_embeddings | |
| # Add time delta embeddings: | |
| missing_time_delta_mask = time_deltas.isnan() | |
| time_deltas = time_deltas.nan_to_num(0) # Replace NaN with dummy value before projection. | |
| time_delta_embeddings = self.time_delta_encoder(time_deltas.unsqueeze(-1)) | |
| time_delta_embeddings[missing_time_delta_mask] = self.get_input_embeddings()(torch.tensor(self.config.missing_time_delta_token_id, device=inputs_embeds.device)) | |
| time_delta_embeddings *= time_deltas_mask.unsqueeze(-1) | |
| inputs_embeds += time_delta_embeddings | |
| outputs = self.language_model( | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| past_key_values=past_key_values, | |
| inputs_embeds=inputs_embeds, | |
| use_cache=use_cache, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| cache_position=cache_position, | |
| num_logits_to_keep=num_logits_to_keep, | |
| ) | |
| logits = outputs[0] | |
| loss = None | |
| if labels is not None: | |
| # Shift so that tokens < n predict n | |
| if attention_mask is not None: | |
| # we use the input attention mask to shift the logits and labels, because it is 2D. | |
| # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft | |
| shift_attention_mask = attention_mask[:, -(logits.shape[1] - 1) :].to(logits.device) | |
| shift_logits = logits[..., :-1, :][shift_attention_mask.to(logits.device) != 0].contiguous() | |
| shift_labels = labels[..., 1:][shift_attention_mask.to(labels.device) != 0].contiguous() | |
| else: | |
| shift_logits = logits[..., :-1, :].contiguous() | |
| shift_labels = labels[..., 1:].contiguous() | |
| # Flatten the tokens | |
| loss_fct = nn.CrossEntropyLoss() | |
| loss = loss_fct( | |
| shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1).to(shift_logits.device) | |
| ) | |
| if not return_dict: | |
| output = (logits,) + outputs[1:] | |
| return (loss,) + output if loss is not None else output | |
| return LlavaCausalLMOutputWithPast( | |
| loss=loss, | |
| logits=logits, | |
| past_key_values=outputs.past_key_values, | |
| hidden_states=outputs.hidden_states, | |
| attentions=outputs.attentions, | |
| image_hidden_states=image_features if pixel_values is not None else None, | |
| ) | |
| def prepare_inputs_for_generation( | |
| self, | |
| input_ids, | |
| past_key_values=None, | |
| inputs_embeds=None, | |
| pixel_values=None, | |
| attention_mask=None, | |
| cache_position=None, | |
| num_logits_to_keep=None, | |
| **kwargs, | |
| ): | |
| model_inputs = {} | |
| model_inputs.update( | |
| self.language_model.prepare_inputs_for_generation( | |
| input_ids, | |
| past_key_values=past_key_values, | |
| inputs_embeds=inputs_embeds, | |
| attention_mask=attention_mask, | |
| cache_position=cache_position, | |
| num_logits_to_keep=num_logits_to_keep, | |
| **kwargs, | |
| ), | |
| ) | |
| if cache_position[0] == 0: | |
| model_inputs['pixel_values'] = pixel_values | |
| model_inputs['attention_mask'] = kwargs['initial_attention_mask'] # Use the 4D attention mask for the initial iteration. | |
| else: | |
| # Always ensure that the token_type_ids are computed from all of the input_ids, not just the last to be generated: | |
| model_inputs['token_type_ids'] = self.token_ids_to_token_type_ids(input_ids) | |
| # Time deltas: | |
| model_inputs['time_deltas'] = torch.zeros_like( | |
| model_inputs['token_type_ids'], dtype=torch.float, device=model_inputs['token_type_ids'].device | |
| ) # These will be masked; no need to set this to inf_time_delta_value. | |
| model_inputs['time_deltas_mask'] = torch.zeros_like(model_inputs['token_type_ids'], dtype=torch.float, device=model_inputs['token_type_ids'].device) | |
| # Position identifiers: | |
| model_inputs['position_ids'] = kwargs['position_ids'].max(dim=1).values.unsqueeze(-1) + (input_ids.shape[1] - kwargs['position_ids'].shape[1]) | |
| # Validate that the findings token type identifier is used for sep_token_id: | |
| mask = input_ids[:, -1] == self.config.sep_token_id | |
| if mask.any(): | |
| invalid = ~(model_inputs['token_type_ids'][mask] == self.config.findings_token_type_id) | |
| if invalid.any(): | |
| warnings.warn( | |
| 'Token type for sep_token_id must be findings_token_type_id', | |
| UserWarning, | |
| stacklevel=2 | |
| ) | |
| print(f'Warning: token type for sep_token_id must be findings_token_type_id: {self.config.findings_token_type_id}, got {model_inputs["token_type_ids"][mask]} instead for input_ids: {input_ids[mask]}.') | |
| print('This is likely due to more than one sep_token_id being generated. This is only an issue if not handled during reinforcement learning (i.e., a loss of zero needs to be set for candidates with more than one sep_token_id).') | |
| # Validate that the impression token type identifier is used after sep_token_id: | |
| mask = (input_ids[:, :-1] == self.config.sep_token_id).any(dim=1) | |
| if mask.any(): | |
| invalid = ~(model_inputs['token_type_ids'][mask] == self.config.impression_token_type_id) | |
| if invalid.any(): | |
| warnings.warn( | |
| 'Token type after sep_token_id must be impression_token_type_id', | |
| UserWarning, | |
| stacklevel=2 | |
| ) | |
| print(f'Warning: token type after sep_token_id must be impression_token_type_id: {self.config.impression_token_type_id}, got {model_inputs["token_type_ids"][mask]} instead for input_ids: {input_ids[mask]}.') | |
| model_inputs.pop('initial_attention_mask', None) | |
| return model_inputs | |
| def token_ids_to_token_type_ids(self, token_ids): | |
| assert token_ids.ndim == 2 | |
| token_type_ids = [] | |
| for i in token_ids: | |
| if self.config.sep_token_id in i[:-1]: | |
| token_type_ids.append(self.config.impression_token_type_id) | |
| else: | |
| token_type_ids.append(self.config.findings_token_type_id) | |
| token_type_ids = torch.tensor(token_type_ids, dtype=torch.long, device=token_ids.device)[:, None] | |
| return token_type_ids | |