Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
582 changes: 582 additions & 0 deletions docs/source/BestPractices/Decision.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/source/Instruction/Supported-models-and-datasets.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,14 @@
|[zenosai/MonkeyOCRv2-B](https://modelscope.cn/models/zenosai/MonkeyOCRv2-B)|monkeyocrv2|monkeyocrv2||transformers>=4.45, qwen_vl_utils|✘|vision, embedding, document-understanding|[zenosai/MonkeyOCRv2-B](https://huggingface.co/zenosai/MonkeyOCRv2-B)|


### Decision模型
| Model ID | Model Type | Default Template | Default Agent Template | Requires | Support Megatron | Tags | HF Model ID |
| -------- | -----------| ---------------- | ---------------------- | -------- | ---------------- | ---- | ----------- |
|[autotrust/JEV-27B-VL](https://modelscope.cn/models/autotrust/JEV-27B-VL)|jev|jev||transformers>=5.2.0, qwen_vl_utils>=0.0.14, decord|✘|vision, decision|[autotrust/JEV-27B-VL](https://huggingface.co/autotrust/JEV-27B-VL)|
|[Cloudflare/clef](https://modelscope.cn/models/Cloudflare/clef)|clef|clef||transformers>=5.2.0, qwen_vl_utils>=0.0.14, decord|✘|decision|[Cloudflare/clef](https://huggingface.co/Cloudflare/clef)|
|[AI-ModelScope/OmniJev](https://modelscope.cn/models/AI-ModelScope/OmniJev)|omnijev|omnijev||transformers>=5.2.0, qwen_vl_utils>=0.0.14, decord|✘|vision, decision|[AI-ModelScope/OmniJev](https://huggingface.co/AI-ModelScope/OmniJev)|


## 数据集
下表介绍了ms-swift接入的数据集的相关信息:
- Dataset ID: ModelScope数据集id
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Swift DOCUMENTATION
BestPractices/MLLM-Registration.md
BestPractices/Embedding.md
BestPractices/Reranker.md
BestPractices/Decision.md
BestPractices/Rapidly-Training-VL-model.md
BestPractices/NPU-support.md
BestPractices/Metax-support.md
Expand Down
533 changes: 533 additions & 0 deletions docs/source_en/BestPractices/Decision.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/source_en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Swift DOCUMENTATION
BestPractices/MLLM-Registration.md
BestPractices/Embedding.md
BestPractices/Reranker.md
BestPractices/Decision.md
BestPractices/Rapidly-Training-VL-model.md
BestPractices/NPU-support.md
BestPractices/Metax-support.md
Expand Down
43 changes: 43 additions & 0 deletions examples/train/decision/clef.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Typed-decision (System-1) fine-tuning of Cloudflare/clef.
#
# Clef decodes ALL of a record's questions jointly in ONE forward (the only multi-question-per-forward
# model of the three), scoring each option of every question and softmaxing within each question's own
# option set. `--task_type decision` routes to ScoringTrainer + the decision data collator;
# `--loss_type smooth_brier` is label-smoothing CE + Brier over each question's options.
#
# The scoring head (`scoring_head`) is the decision layer; ScoringTrainer re-activates it for joint
# training AUTOMATICALLY (the `--trainable_parameters scoring_head` flag is a no-op in the LoRA branch,
# so it is omitted). Clef ships a MERGED backbone that already carries the decision ability (no adapter
# to continue), so a fresh `--tuner_type lora --target_modules all-linear` is correct here -- unlike
# JEV/OmniJev, which continue-train their shipped adapter via `--adapters`. `--attn_impl eager` matches
# tests/test_align/test_decision_clef_align.py (removes sdpa/flash kernel divergence).
#
# sh examples/train/decision/clef.sh
USE_HF=0 \
CUDA_VISIBLE_DEVICES=0 \
swift sft \
--external_plugins examples/train/decision/dataset.py \
--model Cloudflare/clef \
--model_type clef \
--task_type decision \
--loss_type smooth_brier \
--dataset clef_decision \
--tuner_type lora \
--target_modules all-linear \
--torch_dtype bfloat16 \
--attn_impl eager \
--num_train_epochs 1 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--lora_rank 8 \
--lora_alpha 32 \
--gradient_accumulation_steps 4 \
--gradient_checkpointing true \
--logging_steps 1 \
--save_steps 50 \
--save_total_limit 2 \
--max_length 4096 \
--split_dataset_ratio 0 \
--output_dir output \
--dataloader_num_workers 1
4 changes: 4 additions & 0 deletions examples/train/decision/data/clef.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"id": "clef_noul", "state": {"text": "The sky is blue.", "lang": "en"}, "questions": {"is_blue": {"type": "noul", "instructions": "Is the sky blue?"}}, "answers": {"is_blue": true}}
{"id": "clef_choice", "state": "A review: the product arrived late and was damaged.", "questions": {"sentiment": {"type": "choice", "instructions": "Classify the sentiment.", "criteria": {"positive": "Good", "negative": "Bad", "neutral": "Mixed"}}}, "answers": {"sentiment": "negative"}}
{"id": "clef_score", "state": {"doc": "some text to rate"}, "questions": {"quality": {"type": "score", "instructions": "Rate quality.", "criteria": ["terrible", "poor", "ok", "good", "great"]}}, "answers": {"quality": 3}}
{"id": "clef_joint", "state": {"user": "alice", "msg": "hello there", "flag": true}, "questions": {"spam": {"type": "noul", "instructions": "Is this spam?"}, "topic": {"type": "choice", "instructions": "Pick a topic.", "criteria": {"b_tech": "Technology", "a_sport": "Sports", "c_food": "Food"}}, "polite": {"type": "score", "instructions": "Politeness level.", "criteria": ["rude", "neutral", "polite"]}}, "answers": {"spam": false, "topic": "a_sport", "polite": 2}}
4 changes: 4 additions & 0 deletions examples/train/decision/data/jev.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"state": "The sky is blue.", "questions": [{"kind": "noul", "question": "Is the sky blue?", "gold": 1}]}
{"state": "A review: the product arrived late and was damaged.", "questions": [{"kind": "choice", "question": "Classify the sentiment.", "options": ["positive", "negative", "neutral"], "gold": 1}]}
{"state": "some text to rate", "questions": [{"kind": "score", "question": "Rate quality.", "gold": [0.05, 0.1, 0.15, 0.4, 0.2, 0.1]}]}
{"state": "A joint record rendered one question per forward.", "questions": [{"kind": "noul", "question": "Is this spam?", "gold": 0}, {"kind": "choice", "question": "Pick a topic.", "options": ["Technology", "Sports", "Food"], "gold": 1}, {"kind": "score", "question": "Politeness level.", "gold": 4}]}
4 changes: 4 additions & 0 deletions examples/train/decision/data/omnijev.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"state": "Look at the image.", "images": ["http://modelscope-open.oss-cn-hangzhou.aliyuncs.com/images/animal.png"], "questions": [{"type": "noul", "instructions": "Is there an animal in the image?", "gold": true}]}
{"images": ["http://modelscope-open.oss-cn-hangzhou.aliyuncs.com/images/animal.png"], "questions": [{"type": "choice", "instructions": "What is in the image?", "options": [{"key": "cat", "text": "a cat"}, {"key": "dog", "text": "a dog"}, {"key": "bird", "text": "a bird"}], "gold": "cat"}]}
{"images": ["http://modelscope-open.oss-cn-hangzhou.aliyuncs.com/images/animal.png"], "questions": [{"type": "score", "instructions": "How clear is the image?", "levels": ["blurry", "ok", "sharp"], "gold": "sharp"}]}
{"images": ["http://modelscope-open.oss-cn-hangzhou.aliyuncs.com/images/animal.png"], "questions": [{"type": "choice", "instructions": "Which region contains the animal?", "options": [{"key": "left", "region": {"box": [0, 0, 100, 100]}}, {"key": "right", "region": {"box": [100, 0, 200, 100]}}, {"key": "none", "abstain": true}], "gold": "left"}]}
56 changes: 56 additions & 0 deletions examples/train/decision/dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
"""External plugin: register the three typed-decision mini datasets used by these examples.

Each model has its own `ScoringPreprocessor` subclass (see `swift/dataset/preprocessor/decision.py`)
because the three corpora differ in raw shape:
* Clef -- ONE record -> ONE row carrying ALL its questions (joint decode); native
`{state, questions: {qid: {type, instructions, criteria}}, answers}` schema.
* JEV -- ONE record -> ONE row PER question (s1_pass scores one question per forward); unified
`{state, questions: [{kind, question, options, gold}]}` schema.
* OmniJev -- ONE record -> ONE row PER question, multimodal state (image / pre-composed video
mosaic); unified option blocks with region / abstain support.

Pass this file to `swift sft --external_plugins examples/train/decision/dataset.py` and reference a
dataset by its registered name (`clef_decision` / `jev_decision` / `omnijev_decision`).
"""
import os

from swift.dataset import DatasetMeta, register_dataset
from swift.dataset.preprocessor.decision import ClefPreprocessor, JevPreprocessor, OmniJevPreprocessor

_DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')

register_dataset(
DatasetMeta(
dataset_name='clef_decision',
dataset_path=os.path.join(_DATA_DIR, 'clef.jsonl'),
preprocess_func=ClefPreprocessor(),
),
exist_ok=True)

register_dataset(
DatasetMeta(
dataset_name='jev_decision',
dataset_path=os.path.join(_DATA_DIR, 'jev.jsonl'),
preprocess_func=JevPreprocessor(),
),
exist_ok=True)

register_dataset(
DatasetMeta(
dataset_name='omnijev_decision',
dataset_path=os.path.join(_DATA_DIR, 'omnijev.jsonl'),
preprocess_func=OmniJevPreprocessor(),
),
exist_ok=True)

if __name__ == '__main__':
# Quick self-check: load each dataset and print the normalized decision row contract.
from swift.dataset import load_dataset
for name in ('clef_decision', 'jev_decision', 'omnijev_decision'):
ds = load_dataset([name], num_proc=1)[0]
print(f'==== {name}: {len(ds)} rows ====')
row = ds[0]
for k in ('messages', 'questions', 'kinds', 'options', 'target_probs', 'images'):
if k in row:
print(f' {k}: {row[k]}')
54 changes: 54 additions & 0 deletions examples/train/decision/jev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Typed-decision (System-1) DISTILLATION fine-tuning of autotrust/JEV-27B-VL.
#
# JEV scores ONE question per forward (serve_decide.py::s1_pass), so JevPreprocessor fans a record out
# into one row per question. It is a DISTILLATION model: `--loss_type distill_rps` is
# KL(target || model) on the activated option distribution (+ 0.5*RPS on `score`), and `target_probs`
# may be a full soft distribution (the teacher's) rather than a one-hot. `--task_type decision` routes
# to ScoringTrainer; `--attn_impl eager` matches tests/test_align/test_decision_jev_align.py.
#
# CONTINUE-TRAINING, not a fresh LoRA: JEV's decision ability lives in the shipped adapter
# `adapter_vllm/` (a backbone LoRA + the 24-slot verbalizer head packaged as an lm_head LoRA). So
# `--model_type jev` loads the base Qwen3.5-VL and builds the fp32 calibration head from
# `adapter_vllm/decision_head.json` (+ calibration.json), and `--adapters .../adapter_vllm` continues
# training THAT LoRA (is_trainable=True). A fresh `--tuner_type lora --target_modules all-linear`
# would DISCARD the trained lm_head-LoRA the head reads, so it is wrong for JEV. adapter_vllm ships no
# args.json, which is fine: `swift sft` defaults `--load_args false`, so nothing tries to read it.
#
# The head's fp32 `bias` is re-activated for joint training AUTOMATICALLY by ScoringTrainer (the
# `--trainable_parameters scoring_head` flag is a no-op in the adapter branch, so it is omitted). On
# save, ScoringTrainer._save writes the trained head beside the adapter as scoring_head.safetensors +
# scoring_head.json; the next `--adapters <checkpoint>` (or `swift infer`) reloads it via the guarded
# maybe_load_trained_scoring_head hook.
#
# sh examples/train/decision/jev.sh
# The decision LoRA lives in the model repo's `adapter_vllm/` subfolder. Resolve its local snapshot path
# with the SAME interpreter that runs `swift` (read from the swift entry-point shebang), so this works
# no matter which `python` happens to be first on PATH. USE_HF=0 -> modelscope.
SWIFT_PY="$(sed -n '1s/^#!//p' "$(command -v swift)")"
JEV_ADAPTER="$("$SWIFT_PY" -c "import os; from modelscope import snapshot_download; print(os.path.join(snapshot_download('autotrust/JEV-27B-VL'), 'adapter_vllm'))")"

USE_HF=0 \
CUDA_VISIBLE_DEVICES=0 \
swift sft \
--external_plugins examples/train/decision/dataset.py \
--model autotrust/JEV-27B-VL \
--model_type jev \
--task_type decision \
--adapters "$JEV_ADAPTER" \
--loss_type distill_rps \
--dataset jev_decision \
--torch_dtype bfloat16 \
--attn_impl eager \
--num_train_epochs 1 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--gradient_accumulation_steps 4 \
--gradient_checkpointing true \
--logging_steps 1 \
--save_steps 50 \
--save_total_limit 2 \
--max_length 4096 \
--split_dataset_ratio 0 \
--output_dir output \
--dataloader_num_workers 1
69 changes: 69 additions & 0 deletions examples/train/decision/omnijev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Typed-decision (System-1) fine-tuning of AI-ModelScope/OmniJev.
#
# OmniJev scores ONE question per forward over a MULTIMODAL state (image, or a video pre-composed
# offline into a mosaic). OmniJevPreprocessor fans a record out into one row per question and carries
# the official option-BLOCK structure plus width-matched `omnijev_column_keys` (noul -> [yes,no];
# choice -> keys + an extra learned `abstain` column; score -> the level labels). Because the hybrid
# Qwen3.5-4B backbone carries recurrent state, options are scored by the two-stage cached BRANCH
# (swift/model/omnijev_branch.py), not a block-diagonal mask.
#
# `--loss_type ce_rps` uses PROPER scoring rules only (faithful to mso/head.py: no label smoothing).
#
# CONTINUE-TRAINING a SPLIT layout, not a fresh LoRA: OmniJev ships adapter-only (AI-ModelScope/OmniJev = a
# Qwen/Qwen3.5-4B LoRA + the head artifacts head.pt / ord.pt / new_tok_emb.pt / head_meta.json, with NO
# base weights and NO config.json). So `--model Qwen/Qwen3.5-4B` supplies the hybrid base and
# `--adapters <OmniJev>` continues training THAT LoRA (is_trainable=True); a fresh `--tuner_type lora
# --target_modules all-linear` would discard it. Both the base Qwen3.5-4B and the AI-ModelScope/OmniJev
# adapter are distributed on ModelScope (so `USE_HF=0` -> swift resolves the base there), and the adapter
# is resolved by the explicit `modelscope.snapshot_download` below (which is independent of `USE_HF`).
# Because the loader's model_dir is the BASE dir, the four head artifacts (which live in the adapter dir)
# are pointed at via OMNIJEV_* envs -- including new_tok_emb.pt, the ONLY source of the two option-token
# embeddings (modules_to_save=None, so they are not in the LoRA; a missing file must error, never silently
# mean-init). The head's fp32 params are re-activated for joint training AUTOMATICALLY by ScoringTrainer
# (--trainable_parameters is a no-op in the adapter branch, so it is omitted), and saved beside the
# adapter on checkpoint.
#
# MEMORY: the cached BRANCH is fundamentally incompatible with HF `--gradient_checkpointing` (that flag
# forces use_cache=False inside the backbone, but the branch's prefix pass MUST return a KV/recurrent
# cache to reuse per option row). `_omnijev_branch_forward` therefore auto-disables gradient_checkpointing
# around the branch, so `--gradient_checkpointing` is a NO-OP for OmniJev. The branch's own chunk-level
# activation recomputation (MSO_BRANCH_CKPT=1, swift/model/omnijev_branch.py) provides the memory savings
# instead -- set it for real runs; leave --gradient_checkpointing false to avoid the false impression.
#
# sh examples/train/decision/omnijev.sh
# Resolve the OmniJev adapter dir with the SAME interpreter that runs `swift` (its entry-point shebang),
# then point the OMNIJEV_* head-artifact envs at it (they live in the adapter dir, not the base dir).
SWIFT_PY="$(sed -n '1s/^#!//p' "$(command -v swift)")"
OMNIJEV_CKPT="$("$SWIFT_PY" -c "from modelscope import snapshot_download; print(snapshot_download('AI-ModelScope/OmniJev'))")"
export OMNIJEV_HEAD="$OMNIJEV_CKPT/head.pt"
export OMNIJEV_ORD="$OMNIJEV_CKPT/ord.pt"
export OMNIJEV_HEAD_META="$OMNIJEV_CKPT/head_meta.json"
export OMNIJEV_NEW_TOK_EMB="$OMNIJEV_CKPT/new_tok_emb.pt"
export MSO_BRANCH_CKPT=1

USE_HF=0 \
CUDA_VISIBLE_DEVICES=0 \
swift sft \
--external_plugins examples/train/decision/dataset.py \
--model Qwen/Qwen3.5-4B \
--model_type omnijev \
--task_type decision \
--adapters "$OMNIJEV_CKPT" \
--loss_type ce_rps \
--dataset omnijev_decision \
--torch_dtype bfloat16 \
--attn_impl eager \
--freeze_vit true \
--num_train_epochs 1 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--gradient_accumulation_steps 4 \
--gradient_checkpointing false \
--logging_steps 1 \
--save_steps 50 \
--save_total_limit 2 \
--max_length 4096 \
--split_dataset_ratio 0 \
--output_dir output \
--dataloader_num_workers 1
39 changes: 38 additions & 1 deletion swift/arguments/base_args/data_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ class DataArguments:
`swift/self-cognition` dataset. Pass author's Chinese and English names.
Example: `--model_author '魔搭' 'ModelScope'`. Defaults to None.
custom_dataset_info (List[str]): Path to a custom dataset registration JSON file. Defaults to [].
decision_drop_instructions (float): Probability of dropping the question instruction text for
``decision`` task_type datasets. The question text is set to empty (the layout is still
rendered), forcing the model to infer intent from options only. 0.0 disables. Defaults to 0.0.
decision_subsample_options (int): Subsample choice question options to at most this many (keeping
the gold option + N-1 distractors). Only applies to ``choice`` questions; ``noul`` and ``score``
have fixed option sets. 0 disables. Defaults to 0.
decision_state_as_text (float): Probability of rendering a dict ``state`` as human-readable text
(``"Key: value\\n..."``) instead of JSON, so the model sees different renderings of the same
record. 0.0 disables. Defaults to 0.0.
decision_shuffle_fields (bool): Shuffle the field order in dict ``state`` AND the question order in
multi-question records (e.g. Clef). Makes the model robust to permutation of schema fields.
Defaults to False.
"""
# dataset_id or dataset_dir or dataset_path
dataset: List[str] = field(default_factory=list)
Expand Down Expand Up @@ -99,6 +111,12 @@ class DataArguments:

custom_dataset_info: List[str] = field(default_factory=list) # .json

# Decision task_type schema augmentation
decision_drop_instructions: float = 0.0
decision_subsample_options: int = 0
decision_state_as_text: float = 0.0
decision_shuffle_fields: bool = False

def _init_custom_dataset_info(self):
"""register custom dataset_info.json to datasets"""
if isinstance(self.custom_dataset_info, str):
Expand All @@ -124,8 +142,24 @@ def _init_val_dataset_exists(self):
self._val_dataset_exists = bool(self.dataset and self.split_dataset_ratio > 0 or self.val_dataset
or self.cached_val_dataset)

@property
def _decision_augment_enabled(self) -> bool:
return (self.decision_drop_instructions > 0 or self.decision_subsample_options > 0
or self.decision_state_as_text > 0 or self.decision_shuffle_fields)

def get_decision_augment_config(self):
if not self._decision_augment_enabled:
return None
from swift.dataset.preprocessor.decision import DecisionAugmentConfig
return DecisionAugmentConfig(
drop_instructions=self.decision_drop_instructions,
subsample_options=self.decision_subsample_options,
state_as_text=self.decision_state_as_text,
shuffle_fields=self.decision_shuffle_fields,
)

def get_dataset_kwargs(self):
return {
kwargs = {
'seed': self.data_seed,
'num_proc': self.dataset_num_proc,
'load_from_cache_file': self.load_from_cache_file,
Expand All @@ -143,3 +177,6 @@ def get_dataset_kwargs(self):
'remove_unused_columns': self.remove_unused_columns,
'disable_auto_column_mapping': self.disable_auto_column_mapping,
}
if self._decision_augment_enabled:
kwargs['decision_augment_config'] = self.get_decision_augment_config()
return kwargs
Loading
Loading