Skip to content
Back to skills

Modomodо Multimodal Rl

ASecurity

Optimize data mixtures across diverse vision-language domains when applying RL with verifiable rewards to multimodal LLMs, balancing task-specific performance with generalization.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 9, 2026
testingpythongoperformance

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add ADu2021/skillXiv --skill 'modomodо-multimodal-rl' --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Modomodо Multimodal Rl?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Modomodо Multimodal Rl
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adu2021-modomod-multimodal-rl/badge)](https://www.skillsdirectory.com/skills/adu2021-modomod-multimodal-rl)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: modomodо-multimodal-rl
title: "MoDoMoDo: Multi-Domain Data Mixtures for Multimodal LLM Reinforcement Learning"
version: 0.0.2
engine: skillxiv-v0.0.2-claude-opus-4.6
license: MIT
url: "https://arxiv.org/abs/2505.24871"
keywords: [Multimodal RL, Data Mixture, Vision-Language, RLVR, Multi-Task]
description: "Optimize data mixtures across diverse vision-language domains when applying RL with verifiable rewards to multimodal LLMs, balancing task-specific performance with generalization."
---

# Optimize Multi-Domain Data Mixtures for Multimodal RL Training

Multimodal large language models (MLLMs) must handle heterogeneous tasks: visual QA, image captioning, spatial reasoning, scene understanding. When applying reinforcement learning with verifiable rewards (RLVR), a critical challenge emerges: how should you balance training data across these diverse domains?

MoDoMoDo addresses this through systematic data mixture optimization: finding the right proportion of training from each domain so the model learns task-specific skills without forgetting others. Unlike single-domain RL, multimodal RL requires careful orchestration of data flows to prevent task interference and maximize transfer.

## Core Concept

MoDoMoDo optimizes task mixture ratios for multimodal RL:

- **Domain-specific capability**: Each domain requires unique visual, logical, and spatial skills
- **Interference avoidance**: Training hard on one domain shouldn't degrade others
- **Curriculum design**: Sequence domains in ways that enable positive transfer
- **Metric tracking**: Monitor per-domain performance during training
- **Adaptive mixture**: Adjust ratios based on observed performance gaps
- **Generalization**: Find mixtures that improve broader capability

The key insight is that heterogeneous task distributions demand explicit mixture management—uniform random sampling isn't optimal.

## Architecture Overview

- **Task taxonomy**: Categorize multimodal tasks by required capabilities
- **Capacity tracking**: Monitor model capacity utilization per task
- **Performance monitoring**: Track metrics separately for each domain
- **Interference detection**: Identify when training on one domain hurts others
- **Mixture scheduler**: Dynamically adjust domain sampling probabilities
- **Transfer measurement**: Quantify positive/negative transfer between domains
- **Consolidation strategy**: Ensure all domains reach target performance

## Implementation

Build a data mixture optimizer for multimodal RL:

```python
# MoDoMoDo: Multi-Domain Data Mixture Optimization
import torch
from torch.utils.data import DataLoader, WeightedRandomSampler
from typing import Dict, List, Tuple
import numpy as np

class MultiDomainDataMixer:
    """
    Optimize data mixture ratios for multimodal RL training across domains.
    """
    def __init__(self, domain_names: List[str], initial_weights: Dict[str, float] = None):
        self.domains = domain_names
        self.num_domains = len(domain_names)

        # Initialize mixture weights (uniform by default)
        if initial_weights is None:
            initial_weights = {d: 1.0 / self.num_domains for d in domain_names}

        self.mixture_weights = initial_weights
        self.performance_history = {d: [] for d in domain_names}
        self.interference_matrix = np.zeros((self.num_domains, self.num_domains))

    def create_mixed_dataloader(self, domain_datasets: Dict[str, torch.utils.data.Dataset],
                                batch_size: int = 32, training: bool = True):
        """
        Create a dataloader that samples from domains according to mixture weights.
        """
        if training:
            # Weighted sampling favors domains with lower performance
            adjusted_weights = self._adjust_weights_by_performance()

            # Create sampler that respects mixture ratios
            all_indices = []
            all_domain_labels = []

            # Concatenate datasets
            total_samples = 0
            dataset_offsets = {}
            concatenated_data = []

            offset = 0
            for domain in self.domains:
                dataset_offsets[domain] = offset
                concatenated_data.extend(domain_datasets[domain])
                offset += len(domain_datasets[domain])
                total_samples += len(domain_datasets[domain])

            # Create weighted sampler
            sample_weights = [adjusted_weights.get(d, 1.0 / self.num_domains)
                             for d in self.domains]

            # Oversample domains with high mixture weight
            samples_per_domain = {
                d: int(adjusted_weights[d] * total_samples)
                for d in self.domains
            }

            # Stratified sampling
            indices = []
            for domain in self.domains:
                domain_size = len(domain_datasets[domain])
                domain_indices = np.random.choice(
                    domain_size,
                    size=min(samples_per_domain[domain], domain_size),
                    replace=True
                )
                indices.extend(domain_indices + dataset_offsets[domain])

            # Shuffle
            np.random.shuffle(indices)

            # Create dataloader
            from torch.utils.data import Subset
            subset = Subset(concatenated_data, indices)
            dataloader = DataLoader(subset, batch_size=batch_size, shuffle=True)

        else:
            # Test: no mixture, equal distribution
            concatenated_data = []
            for domain in self.domains:
                concatenated_data.extend(domain_datasets[domain])

            dataloader = DataLoader(concatenated_data, batch_size=batch_size, shuffle=False)

        return dataloader

    def update_mixture_from_performance(self, per_domain_metrics: Dict[str, float]):
        """
        Adjust mixture weights based on observed per-domain performance.
        """
        # Store history
        for domain, metric in per_domain_metrics.items():
            self.performance_history[domain].append(metric)

        # Compute performance improvement rate per domain
        improvement_rates = {}
        for domain in self.domains:
            hist = self.performance_history[domain]
            if len(hist) >= 2:
                recent = np.mean(hist[-5:]) if len(hist) >= 5 else hist[-1]
                older = np.mean(hist[:5]) if len(hist) >= 5 else hist[0]
                improvement = recent - older
                improvement_rates[domain] = improvement
            else:
                improvement_rates[domain] = 0.0

        # Find slowest-improving domains and increase their mixture weight
        mean_improvement = np.mean(list(improvement_rates.values()))

        new_weights = {}
        for domain in self.domains:
            if improvement_rates[domain] < mean_improvement:
                # Increase weight for slow-improving domains
                new_weights[domain] = self.mixture_weights[domain] * 1.1
            else:
                # Decrease weight for fast-improving domains
                new_weights[domain] = self.mixture_weights[domain] * 0.9

        # Normalize to sum to 1
        total_weight = sum(new_weights.values())
        self.mixture_weights = {d: w / total_weight for d, w in new_weights.items()}

        print(f"Updated mixture weights: {self.mixture_weights}")

    def _adjust_weights_by_performance(self) -> Dict[str, float]:
        """
        Adjust mixture weights based on current performance gaps.
        Higher weight for lower-performance domains to prevent catastrophic forgetting.
        """
        # Get latest metrics for each domain
        latest_metrics = {}
        for domain in self.domains:
            if self.performance_history[domain]:
                latest_metrics[domain] = self.performance_history[domain][-1]
            else:
                latest_metrics[domain] = 0.5  # Neutral

        # Compute inverse performance (lower performance = higher weight)
        adjusted_weights = {}
        for domain in self.domains:
            # Use inverse of performance gap from target
            target_performance = 0.9
            gap = max(0, target_performance - latest_metrics[domain])
            adjusted_weights[domain] = (1.0 + gap) * self.mixture_weights[domain]

        # Normalize
        total = sum(adjusted_weights.values())
        adjusted_weights = {d: w / total for d, w in adjusted_weights.items()}

        return adjusted_weights

    def detect_interference(self, before_metrics: Dict[str, float],
                          after_metrics: Dict[str, float]):
        """
        Detect negative transfer: when training on one domain hurts another.
        """
        for d1 in self.domains:
            for d2 in self.domains:
                if d1 != d2:
                    # Did training emphasize d1 hurt d2?
                    d2_degradation = before_metrics[d2] - after_metrics[d2]
                    if d2_degradation > 0.02:  # >2% drop is interference
                        self.interference_matrix[self.domains.index(d1),
                                              self.domains.index(d2)] += 1

    def get_curriculum_order(self) -> List[str]:
        """
        Determine order of domains to minimize negative transfer.
        Uses interference matrix to schedule domains intelligently.
        """
        # Start with lowest-interference domains
        # This enables positive transfer before tackling harder domains
        interference_sums = self.interference_matrix.sum(axis=1)
        order_indices = np.argsort(interference_sums)
        return [self.domains[i] for i in order_indices]
```

Implement RLVR training with mixture management:

```python
def train_mllm_with_mixed_domains(model, domain_datasets: Dict[str, torch.utils.data.Dataset],
                                  num_epochs=10, target_domains: Dict[str, float] = None):
    """
    Train multimodal LLM using RL with verifiable rewards across multiple domains.
    """
    mixer = MultiDomainDataMixer(list(domain_datasets.keys()))

    # Initialize target performance per domain
    if target_domains is None:
        target_domains = {d: 0.8 for d in domain_datasets.keys()}

    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)

    for epoch in range(num_epochs):
        # Create mixed dataloader
        train_loader = mixer.create_mixed_dataloader(domain_datasets, batch_size=32)

        epoch_metrics = {d: [] for d in domain_datasets.keys()}
        before_epoch_metrics = {d: 0.5 for d in domain_datasets.keys()}  # Placeholder

        for batch_idx, batch in enumerate(train_loader):
            images = batch['images']
            questions = batch['questions']
            ground_truths = batch['ground_truths']
            domain_labels = batch['domain']  # Which domain this batch is from

            # Forward pass: get model predictions
            outputs = model(images, questions)

            # Compute per-domain rewards (verifiable)
            per_domain_rewards = {}
            for domain in domain_datasets.keys():
                domain_mask = domain_labels == domain
                if domain_mask.sum() > 0:
                    domain_outputs = outputs[domain_mask]
                    domain_gts = ground_truths[domain_mask]

                    # Task-specific verification
                    if domain == 'vqa':
                        reward = verify_vqa_answer(domain_outputs, domain_gts)
                    elif domain == 'caption':
                        reward = verify_caption_quality(domain_outputs, domain_gts)
                    elif domain == 'spatial':
                        reward = verify_spatial_reasoning(domain_outputs, domain_gts)
                    else:
                        reward = verify_generic_task(domain_outputs, domain_gts)

                    per_domain_rewards[domain] = reward.mean().item()
                    epoch_metrics[domain].extend(reward.cpu().numpy())

            # RL update with domain-aware weighting
            mixture_weights = mixer.mixture_weights
            total_loss = 0

            for domain, reward in per_domain_rewards.items():
                domain_loss = -reward  # Negative because we maximize reward
                weighted_loss = domain_loss * mixture_weights[domain]
                total_loss += weighted_loss

            optimizer.zero_grad()
            total_loss.backward()
            optimizer.step()

        # Update mixture based on domain performance
        avg_domain_metrics = {d: np.mean(epoch_metrics[d]) if epoch_metrics[d] else 0.5
                             for d in domain_datasets.keys()}
        mixer.update_mixture_from_performance(avg_domain_metrics)

        # Detect interference
        mixer.detect_interference(before_epoch_metrics, avg_domain_metrics)

        # Log performance
        print(f"Epoch {epoch}:")
        for domain, metric in avg_domain_metrics.items():
            target = target_domains[domain]
            print(f"  {domain}: {metric:.3f} (target: {target:.3f})")

        before_epoch_metrics = avg_domain_metrics

    return mixer
```

## Practical Guidance

| Parameter | Typical Range | Notes |
|-----------|---------------|-------|
| Initial mixture ratio | Uniform or inverse performance | Start balanced, then adapt |
| Weight adjustment speed | 1.05 - 1.2 per epoch | Too fast = instability; too slow = slow adaptation |
| Target performance gap threshold | 0.02 - 0.05 | When to increase domain weight |
| Interference detection threshold | 0.02 drop | What counts as negative transfer |
| Num domains | 2 - 8 | More domains = more complex optimization |

**When to use MoDoMoDo:**
- Training MLLMs on multiple heterogeneous vision-language tasks
- RL with verifiable rewards across diverse domains
- Need to prevent catastrophic forgetting of some tasks
- Want to measure and understand task interference
- Building robust multimodal foundation models

**When NOT to use:**
- Single task training (mixture optimization not relevant)
- Data is naturally uniform in distribution
- Tasks are already well-balanced in importance
- Computational budget for tracking per-domain metrics is tight
- Using pure supervised learning without RL

**Common pitfalls:**
- Not tracking per-domain metrics separately (can't detect problems)
- Mixture weights diverge too far (one domain gets almost nothing)
- Ignoring negative transfer signals (interference goes unaddressed)
- Adjusting weights too aggressively (training becomes unstable)
- Using same reward for all domains (domain-specific verification critical)
- Not considering curriculum order (some domains should be learned first)
- Interference detection threshold too loose (false positives)

## Reference

**MoDoMoDo: Multi-Domain Data Mixtures for Multimodal LLM Reinforcement Learning**
https://arxiv.org/abs/2505.24871

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.