diff --git a/.github/scripts/check_doc_links.py b/.github/scripts/check_doc_links.py new file mode 100644 index 000000000..d5f24c0d4 --- /dev/null +++ b/.github/scripts/check_doc_links.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Fail when a Sphinx warning reports a broken internal documentation link.""" + +import argparse +from pathlib import Path + + +BLOCKING_WARNING_MARKERS = ( + "[myst.xref_missing]", + "[myst.iref_missing]", + "[myst.xref_ambiguous]", + "[myst.iref_ambiguous]", + "[ref.", + "[toc.", +) + +BLOCKING_WARNING_MESSAGES = ( + "cross-reference target not found", + "reference target not found", + "undefined label:", + "failed to create a cross reference", + "toctree contains reference to", + "document isn't included in any toctree", + "duplicated entry found in toctree", +) + + +def find_blocking_warnings(warning_log: str) -> list[str]: + """Return warnings related to internal references and navigation.""" + blocking_warnings = [] + for line in warning_log.splitlines(): + normalized_line = line.lower() + if any(marker in normalized_line for marker in BLOCKING_WARNING_MARKERS) or any( + message in normalized_line for message in BLOCKING_WARNING_MESSAGES + ): + blocking_warnings.append(line) + return blocking_warnings + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("warning_log", type=Path, help="Sphinx warning log generated with -w") + args = parser.parse_args() + + blocking_warnings = find_blocking_warnings(args.warning_log.read_text(encoding="utf-8")) + if not blocking_warnings: + print("Documentation internal-link check passed.") + return 0 + + print("Documentation internal-link check failed:") + for warning in blocking_warnings: + print(warning) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a648d50ca..45922d131 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,6 +54,11 @@ jobs: cache-dependency-path: | requirements.txt setup.py + - name: Install CPU-only PyTorch + run: >- + python -m pip install + --index-url https://download.pytorch.org/whl/cpu + torch==2.8.0+cpu - name: Install LMFlow and test dependencies run: python -m pip install -e ".[develop]" - name: Run offline CPU tests diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 1ea066b15..831120e33 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -17,6 +17,7 @@ concurrency: jobs: build: name: Build documentation + if: github.repository == 'OptimalScale/LMFlow' runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -31,29 +32,44 @@ jobs: - name: Install dependencies run: python -m pip install -r docs/requirements.txt - name: Build documentation - run: sphinx-build -b html docs/source _build/html - - name: Configure GitHub Pages - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - - name: Upload Pages artifact - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + run: sphinx-build -b html -w _build/sphinx-warnings.log docs/source _build/html + - name: Check documentation links + run: python .github/scripts/check_doc_links.py _build/sphinx-warnings.log + - name: Upload documentation artifact + if: >- + github.repository == 'OptimalScale/LMFlow' && + github.event_name != 'pull_request' && + github.ref == 'refs/heads/main' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: + name: documentation-html path: _build/html + if-no-files-found: error + retention-days: 1 deploy: name: Deploy documentation - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + if: >- + github.repository == 'OptimalScale/LMFlow' && + github.event_name != 'pull_request' && + github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest timeout-minutes: 10 permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + contents: write steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + - name: Check out repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.0.2 + - name: Download documentation artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: documentation-html + path: _build/html + - name: Deploy to gh-pages branch + uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: _build/html + force_orphan: true diff --git a/docs/requirements.txt b/docs/requirements.txt index b17141911..d9f3a3077 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,3 @@ sphinx_design myst-parser sphinx-autoapi matplotlib -numpydoc \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 6259349d2..4eeebdb95 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,7 +37,7 @@ "matplotlib.sphinxext.plot_directive", # "myst_nb", # "nbsphinx", # Uncomment and comment-out MyST-NB for local testing purposes. - "numpydoc", + "sphinx.ext.napoleon", # "sphinx_togglebutton", # "sphinx_favicon", ] @@ -46,6 +46,35 @@ autoapi_type = "python" autoapi_dirs = ["../../src"] +autoapi_options = [ + "members", + "undoc-members", + "show-inheritance", + "show-module-summary", + "special-members", +] + +myst_heading_anchors = 4 +show_warning_types = True + +_autoapi_internal_modules = ( + "lmflow.pipeline.utils", + "lmflow.utils.deprecated", + "lmflow.utils.protocol", +) + + +def _skip_internal_autoapi_modules(app, what, name, obj, skip, options): + if what in {"module", "package"} and any( + name == module_name or name.startswith(f"{module_name}.") + for module_name in _autoapi_internal_modules + ): + return True + return skip + + +def setup(app): + app.connect("autoapi-skip-member", _skip_internal_autoapi_modules) source_suffix = { ".rst": "restructuredtext", diff --git a/docs/source/examples/DATASETS.md b/docs/source/examples/DATASETS.md index 220dcc7fc..daee2c420 100644 --- a/docs/source/examples/DATASETS.md +++ b/docs/source/examples/DATASETS.md @@ -149,12 +149,12 @@ Conversations should be formatted before feeding into the model. As of now, we'v | Template Name | Filled Example | Detailed Template | | ------------- | -------------- | ----------------- | -| `chatglm3` | `[gMASK]sop<\|system\|>`
` You are a chatbot developed by LMFlow team.<\|user\|>`
` Who are you?<\|assistant\|>`
` I am a chatbot developed by LMFlow team.<\|user\|>`
` How old are you?<\|assistant\|>`
` I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.` | [Link](./supported_conversation_template.md#chatglm3) | +| `chatglm3` | `[gMASK]sop<\|system\|>`
` You are a chatbot developed by LMFlow team.<\|user\|>`
` Who are you?<\|assistant\|>`
` I am a chatbot developed by LMFlow team.<\|user\|>`
` How old are you?<\|assistant\|>`
` I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.` | [Link](./supported_conversation_template.md#chatglm-3) | | `chatml` | `<\|im_start\|>system`
`You are a chatbot developed by LMFlow team.<\|im_end\|>`
`<\|im_start\|>user`
`Who are you?<\|im_end\|>`
`<\|im_start\|>assistant`
`I am a chatbot developed by LMFlow team.<\|im_end\|>`
`<\|im_start\|>user`
`How old are you?<\|im_end\|>`
`<\|im_start\|>assistant`
`I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.<\|im_end\|>`
| [Link](./supported_conversation_template.md#chatml) | -| `deepseek_v2` | `<|begin▁of▁sentence|>You are a chatbot developed by LMFlow team.`

`User: Who are you?`

`Assistant: I am a chatbot developed by LMFlow team.<|end▁of▁sentence|>User: How old are you?`

`Assistant: I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.<|end▁of▁sentence|>` | [Link](./supported_conversation_template.md#deepseek) | +| `deepseek_v2` | `<|begin▁of▁sentence|>You are a chatbot developed by LMFlow team.`

`User: Who are you?`

`Assistant: I am a chatbot developed by LMFlow team.<|end▁of▁sentence|>User: How old are you?`

`Assistant: I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.<|end▁of▁sentence|>` | [Link](./supported_conversation_template.md#deepseek-v2) | | `deepseek_v3` | -- | [Link](./supported_conversation_template.md#deepseek-v3) | | `deepseek_r1` | -- | [Link](./supported_conversation_template.md#deepseek-r1-zero) | -| `deepseek_r1_distill` | -- | [Link](./supported_conversation_template.md#deepseek-r1-distill-llamaqwenl) | +| `deepseek_r1_distill` | -- | [Link](./supported_conversation_template.md#deepseek-r1-distill-llamaqwen) | | `gemma` | `You are a chatbot developed by LMFlow team.user`
`Who are you?`
`model`
`I am a chatbot developed by LMFlow team.`
`user`
`How old are you?`
`model`
`I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.`
| [Link](./supported_conversation_template.md#gemma) | | `hymba` | `System`
`You are a chatbot developed by LMFlow team.`
` {"name": "generate_qrcode", "description": "Generate a QR code for a given text", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "The text to encode in the QR code"}}, "required": ["text"]}} `

`User`
`Who are you?`
`Assistant`
`I am a chatbot developed by LMFlow team.`
`User`
`How old are you?`
`Assistant`
`I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.` | [Link](./supported_conversation_template.md#hymba) | | `internlm2` | `<\|im_start\|>system`
`You are a chatbot developed by LMFlow team.<\|im_end\|>`
`<\|im_start\|>user`
`Who are you?<\|im_end\|>`
`<\|im_start\|>assistant`
`I am a chatbot developed by LMFlow team.<\|im_end\|>`
`<\|im_start\|>user`
`How old are you?<\|im_end\|>`
`<\|im_start\|>assistant`
`I don't age like humans do. I exist as a piece of software, so I don't have a concept of age in the traditional sense.<\|im_end\|>`
| [Link](./supported_conversation_template.md#internlm2) | @@ -389,4 +389,4 @@ please refer to [conversation data](#conversation). ] } ``` -```` \ No newline at end of file +```` diff --git a/docs/source/examples/customize_conversation_template.md b/docs/source/examples/customize_conversation_template.md index 0b58b3347..1738a9afe 100644 --- a/docs/source/examples/customize_conversation_template.md +++ b/docs/source/examples/customize_conversation_template.md @@ -5,7 +5,7 @@ We provide the flexibility to customize the conversation template. You can customize your own conversation template by following the steps below: -### 1. Decompose your conversations +## 1. Decompose your conversations Say you want to make the conversations between user and assistant look like: ``` @@ -32,7 +32,7 @@ It is easy to abstract the format for each message: Also, we have a bos token at the beginning of the conversation session. -### 2. Choose proper `Formatter` +## 2. Choose proper `Formatter` Recall the requirements for a conversation dataset: > - `system`: `Optional[string]`. > - `tools`: `Optional[list[string]]`. @@ -42,7 +42,7 @@ Recall the requirements for a conversation dataset: System message, user message, and assistant message are strings thus we can use `StringFormatter` for them. -### 3. Build the template +## 3. Build the template All preset templates are located at `src/lmflow/utils/conversation_template`. Within the template file, define your own template like: @@ -89,7 +89,7 @@ YOUR_TEMPLATE = ConversationTemplate( Feel free to create your own template by inheriting the `ConversationTemplate` class. Llama-2 v.s. llama-3 would be a good examples to refer to. -### 4. Register your template +## 4. Register your template After defining your own template, you need to register it in the `src/lmflow/utils/conversation_template/__init__.py` file. ```python @@ -103,7 +103,7 @@ PRESET_TEMPLATES = { } ``` -### 5. Use your template +## 5. Use your template You are all set! Specify the template name in, for example, your finetune script: ```bash @@ -112,4 +112,4 @@ You are all set! Specify the template name in, for example, your finetune script --dataset_path your_conversation_dataset \ --conversation_template your_template_name \ --output_model_path output_models/your_model -``` \ No newline at end of file +``` diff --git a/docs/source/examples/index.md b/docs/source/examples/index.md index 30dc1d335..05a8619aa 100644 --- a/docs/source/examples/index.md +++ b/docs/source/examples/index.md @@ -8,6 +8,8 @@ We provide several examples to show how to use our package in your problem. :maxdepth: 3 DATASETS +supported_conversation_template +customize_conversation_template ``` ```{toctree} @@ -24,6 +26,7 @@ For SFT, :maxdepth: 3 finetuning +medical_finetune ``` @@ -54,4 +57,3 @@ Refer to [examples](https://github.com/OptimalScale/LMFlow/blob/main/examples). TASK_GUIDE ``` - diff --git a/src/lmflow/args.py b/src/lmflow/args.py index 249c29486..9841d883b 100644 --- a/src/lmflow/args.py +++ b/src/lmflow/args.py @@ -855,6 +855,8 @@ class InferencerArguments: Define a class InferencerArguments using the dataclass decorator. The class contains several optional parameters that can be used to configure a inferencer. + Parameters + ---------- local_rank : str For distributed training: local_rank random_seed : int, default = 1 diff --git a/src/lmflow/datasets/dataset.py b/src/lmflow/datasets/dataset.py index 014e52f4a..ab8d58b81 100644 --- a/src/lmflow/datasets/dataset.py +++ b/src/lmflow/datasets/dataset.py @@ -148,21 +148,21 @@ def _check_hf_json_format(self, data_files: list[str]): ) def from_dict(self, dict_obj: dict, *args, **kwargs): - r""" - Create a Dataset object from a dictionary. + """Populate this dataset from an LMFlow dataset dictionary. + + The expected dictionary shape is:: - Return a Dataset given a dict with format: { "type": TYPE, "instances": [ { - "key_1": VALUE_1.1, - "key_2": VALUE_1.2, + "key_1": VALUE_1_1, + "key_2": VALUE_1_2, ... }, { - "key_1": VALUE_2.1, - "key_2": VALUE_2.2, + "key_1": VALUE_2_1, + "key_2": VALUE_2_2, ... }, ... @@ -170,21 +170,18 @@ def from_dict(self, dict_obj: dict, *args, **kwargs): } Parameters - ----------- - - dict_obj : dict. - A dictionary containing the dataset information. - - args : Optional. - Positional arguments. - - kwargs : Optional. - Keyword arguments. + ---------- + dict_obj : dict + Dataset data containing ``type`` and ``instances`` keys. + *args + Positional arguments passed to the selected dataset backend. + **kwargs + Keyword arguments passed to the selected dataset backend. Returns - --------- - - self : Dataset object. + ------- + Dataset + This dataset instance. """ if self.backend == "huggingface": if KEY_TYPE not in dict_obj: @@ -247,29 +244,31 @@ def create_from_dict(cls, dict_obj, *args, **kwargs): return dataset.from_dict(dict_obj) def to_dict(self): - r""" - Returns - --------- + """Convert this dataset to the LMFlow dictionary format. + + The returned dictionary has the following shape:: - Return a dict represents the dataset: { "type": TYPE, "instances": [ { - "key_1": VALUE_1.1, - "key_2": VALUE_1.2, + "key_1": VALUE_1_1, + "key_2": VALUE_1_2, ... }, { - "key_1": VALUE_2.1, - "key_2": VALUE_2.2, + "key_1": VALUE_2_1, + "key_2": VALUE_2_2, ... }, ... ] } - A python dict object represents the content of this dataset. + Returns + ------- + dict + Dataset data containing ``type`` and ``instances`` keys. """ if self.backend == "huggingface": dict_obj = {} diff --git a/src/lmflow/datasets/multi_modal_dataset.py b/src/lmflow/datasets/multi_modal_dataset.py index 053ec957c..4c51c71f7 100644 --- a/src/lmflow/datasets/multi_modal_dataset.py +++ b/src/lmflow/datasets/multi_modal_dataset.py @@ -136,15 +136,21 @@ def insert_separator(X, sep): def preprocess_llama_from_llava_plain(sources, tokenizer: transformers.PreTrainedTokenizer, has_image: bool = False): - """ - This function just add the image in the front of text. - And don't add any prompt. - Args: - sources: The input data with text and image. - tokenizer: The tokenizer to process text. - has_image: Whether the input data has image. - Returns: - The input_ids and labels for the model. + """Preprocess plain LLaVA samples without adding a prompt. + + Parameters + ---------- + sources + Input samples containing text and image placeholders. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer used to encode text. + has_image : bool + Whether the samples contain images. + + Returns + ------- + dict + Model ``input_ids`` and ``labels``. """ conversations = [] for source in sources: @@ -164,15 +170,21 @@ def preprocess_llama_from_llava_plain(sources, tokenizer: transformers.PreTraine def preprocess_llama_from_llava_v1(sources, tokenizer: transformers.PreTrainedTokenizer, has_image: bool = False): - """ - This function add the prompt and then put the image after the prompt. - So it needs additional code to generate the target label. - Args: - sources: The input data with text and image. - tokenizer: The tokenizer to process text. - has_image: Whether the input data has image. - Returns: - The input_ids and labels for the model. + """Preprocess LLaVA v1 samples and generate target labels. + + Parameters + ---------- + sources + Input samples containing text and image placeholders. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer used to encode text. + has_image : bool + Whether the samples contain images. + + Returns + ------- + dict + Model ``input_ids`` and ``labels``. """ conv = conversation_lib.default_conversation.copy() diff --git a/src/lmflow/models/hf_decoder_model.py b/src/lmflow/models/hf_decoder_model.py index 66b57cccc..dc00ef903 100644 --- a/src/lmflow/models/hf_decoder_model.py +++ b/src/lmflow/models/hf_decoder_model.py @@ -232,30 +232,33 @@ def tokenize(self, dataset: Dataset, add_special_tokens=True, *args, **kwargs) - return tokenized_datasets def encode(self, input: Union[str, list[str]], *args, **kwargs) -> Union[list[int], list[list[int]]]: - """ - Perform encoding process of the tokenizer. + """Encode one string or a batch of strings with the model tokenizer. Parameters - ------------ - inputs : str or list. - The text sequence. + ---------- + input : str or list[str] + Text input to encode. + *args + Positional tokenizer arguments. + **kwargs + Keyword tokenizer arguments. - args : Optional. - Positional arguments. + Returns + ------- + transformers.BatchEncoding or list[int] + A batch encoding for list input, or token ids for string input. - kwargs : Optional. - Keyword arguments. + Examples + -------- + A single string returns token IDs:: + + "Hello, world!" -> [101, 7592, 1010, 2088, 102] + + A list of strings returns a batch encoding with fields such as:: - Returns - ------------ - outputs : - if string input,return the tokenized inputs. - "Hello,world!"-> [101, 7592, 1010, 2088, 102] - if batch input,return {input_ids,attention_mask,token_type_ids} - ["Hello,world!","Hello!"] -> { - 'input_ids': tensor([[ 101, 7592, 1010, 2088, 102],...), - 'attention_mask': tensor([[1, 1, 1, 1, 1],[0,0,1,1,1]]) + "input_ids": tensor([[101, 7592, 1010, 2088, 102], ...]), + "attention_mask": tensor([[1, 1, 1, 1, 1], [0, 0, 1, 1, 1]]) } """ if isinstance(input, list): diff --git a/src/lmflow/models/vision2seq_model.py b/src/lmflow/models/vision2seq_model.py index b80982df7..cfd92e9a8 100644 --- a/src/lmflow/models/vision2seq_model.py +++ b/src/lmflow/models/vision2seq_model.py @@ -174,6 +174,7 @@ def save_prompt_cache(self, path): def load_prompt_cache(self, path): """ Load prompt embedding and id. + Args: path: The path to load the prompt embedding and id. @@ -202,6 +203,7 @@ def forward( image_token_indexes: Optional[list] = None, one_sample_multiple_images: bool = False, ) -> Union[tuple, CausalLMOutputWithPast]: + """Run a multimodal forward pass through the vision and language models.""" if not image_token_indexes: image_token_indexes = [0] diff --git a/src/lmflow/optim/adabelief.py b/src/lmflow/optim/adabelief.py index 717fdd428..a39abdc48 100644 --- a/src/lmflow/optim/adabelief.py +++ b/src/lmflow/optim/adabelief.py @@ -87,9 +87,11 @@ def reset(self): def step(self, closure=None): """Performs a single optimization step. - Arguments: - closure (callable, optional): A closure that reevaluates the model - and returns the loss. + + Parameters + ---------- + closure : callable, optional + A closure that reevaluates the model and returns the loss. """ loss = None if closure is not None: diff --git a/src/lmflow/optim/adabound.py b/src/lmflow/optim/adabound.py index e30511967..3de6909c1 100644 --- a/src/lmflow/optim/adabound.py +++ b/src/lmflow/optim/adabound.py @@ -7,13 +7,12 @@ class AdaBound(Optimizer): - r"""Implements AdaBound algorithm. + """Implement the AdaBound optimization algorithm. - It has been proposed in `Adaptive Gradient Methods with Dynamic Bound of - Learning Rate - https://arxiv.org/abs/1902.09843 - Note: - Reference code: https://github.com/Luolc/AdaBound + See "Adaptive Gradient Methods with Dynamic Bound of Learning Rate": + https://arxiv.org/abs/1902.09843. + + Reference implementation: https://github.com/Luolc/AdaBound. """ def __init__( diff --git a/src/lmflow/optim/lars.py b/src/lmflow/optim/lars.py index e507f96fc..3256366c0 100644 --- a/src/lmflow/optim/lars.py +++ b/src/lmflow/optim/lars.py @@ -7,6 +7,7 @@ class LARS(Optimizer): r"""Extends SGD in PyTorch with LARS scaling from the paper `Large batch training of Convolutional Networks`__. + .. note:: The application of momentum in the SGD part is modified according to the PyTorch standards. LARS scaling fits into the equation in the diff --git a/src/lmflow/optim/radam.py b/src/lmflow/optim/radam.py index 39d2f590e..a0d590b78 100644 --- a/src/lmflow/optim/radam.py +++ b/src/lmflow/optim/radam.py @@ -11,7 +11,7 @@ class RAdam(Optimizer): r"""Implements RAdam optimization algorithm. Note: - Deprecated, please use version provided by PyTorch_. + Deprecated; use the version provided by ``torch.optim``. It has been proposed in `On the Variance of the Adaptive Learning Rate and Beyond`. diff --git a/src/lmflow/optim/utils.py b/src/lmflow/optim/utils.py index 57430bdd0..b0a46269e 100644 --- a/src/lmflow/optim/utils.py +++ b/src/lmflow/optim/utils.py @@ -1,10 +1,10 @@ from typing import Any, Optional -from transformers import PreTrainedModel +from transformers import PreTrainedModel, TrainingArguments from transformers.utils import is_sagemaker_mp_enabled import lmflow.optim.optimizers as optim -from lmflow.args import OptimizerNames, TrainingArguments +from lmflow.args import OptimizerNames def create_customized_optimizer(base_trainer_class, model_args): diff --git a/src/lmflow/pipeline/dpo_aligner.py b/src/lmflow/pipeline/dpo_aligner.py index 254762e46..b206d1f15 100644 --- a/src/lmflow/pipeline/dpo_aligner.py +++ b/src/lmflow/pipeline/dpo_aligner.py @@ -29,15 +29,17 @@ def get_paired_dataset( ) -> Dataset: """Load dataset and convert it to the necessary format. - The dataset is converted to a dictionary with the following structure: - { - 'prompt': list[str], - 'chosen': list[str], - 'rejected': list[str], - } - - Prompts are structured as follows: - "Question: " + + "\n\nAnswer: " + The returned dataset uses the following structure:: + + { + "prompt": list[str], + "chosen": list[str], + "rejected": list[str] + } + + Each prompt is formatted as:: + + "Question: " + prompt + "\n\nAnswer: " """ data_path = Path(data_root) / data_dir data_files = [x.absolute().as_posix() for x in data_path.glob("*.json")] diff --git a/src/lmflow/utils/conversation_template/base.py b/src/lmflow/utils/conversation_template/base.py index ceafac708..9d1488e79 100644 --- a/src/lmflow/utils/conversation_template/base.py +++ b/src/lmflow/utils/conversation_template/base.py @@ -21,28 +21,26 @@ class TemplateComponent: Parameters ---------- type : Literal['token', 'token_id', 'string', 'tools'] - - Type of the component. + Type of the component. - - When the component is a token or a string, the content should be `string`. + When the component is a token or a string, the content should be `string`. The difference between the two is that token will be converted to token ids by the tokenizer.convert_tokens_to_ids() method, while string will be directly encoded by the tokenizer.encode() method. Specially, since the bos token and eos token are frequently used across different templates, we provide the convenience to use `'bos_token'` and `'eos_token'` to represent the actual bos and eos tokens when - `type` of the `TemplateComponent` is `token`. For example: + `type` of the `TemplateComponent` is `token`. For example:: - ```python - TemplateComponent(type='token', content='bos_token') - ``` + TemplateComponent(type='token', content='bos_token') After encoding, the content will be replaced by the actual token id of the bos token. Please do remember that if you set the `type` to `string`, the tokenizer will try to encode the string 'bos_token' instead of providing the actual bos token. - - When the component is token_id, the content should be `int` or `list[int]`, and + When the component is token_id, the content should be `int` or `list[int]`, and will be directly appended to the encoded token ids. - - Tools are not supported yet. + Tools are not supported yet. content : Union[str, int, list[str], list[int]] Content of the component. @@ -189,24 +187,23 @@ def encode_conversation( ) -> Sequence[tuple[list[int], list[int]]]: r""" Messages here should be guaranteed to be in pairs, with the first message being the user message and the second message being the system message. - Data example: - ```json - { - "conversation_id": 2, - "system": "sysinfo1", - "tools": ["tool_1_desc"], - "messages": [ - { - "role": "user", - "content": "hi" - }, - { - "role": "assistant", - "content": "Hello!" - } - ] - } - ``` + Data example:: + + { + "conversation_id": 2, + "system": "sysinfo1", + "tools": ["tool_1_desc"], + "messages": [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello!" + } + ] + } """ assert isinstance(messages, list), "Messages must be a list." @@ -414,24 +411,23 @@ def encode_conversation( ) -> Sequence[tuple[list[int], list[int]]]: r""" Messages here should be guaranteed to be in pairs, with the first message being the user message and the second message being the system message. - Data example: - ```json - { - "conversation_id": 2, - "system": "sysinfo1", - "tools": ["tool_1_desc"], - "messages": [ - { - "role": "user", - "content": "hi" - }, - { - "role": "assistant", - "content": "Hello!" - } - ] - } - ``` + Data example:: + + { + "conversation_id": 2, + "system": "sysinfo1", + "tools": ["tool_1_desc"], + "messages": [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello!" + } + ] + } """ assert isinstance(messages, list), "Messages must be a list." diff --git a/src/lmflow/utils/conversation_template/gemma.py b/src/lmflow/utils/conversation_template/gemma.py index 504923c8e..d73f8f884 100644 --- a/src/lmflow/utils/conversation_template/gemma.py +++ b/src/lmflow/utils/conversation_template/gemma.py @@ -11,6 +11,7 @@ @dataclass class GemmaConversationTemplate(ConversationTemplate): def encode_conversation(self, *args, **kwargs): + """Encode a Gemma conversation and handle optional system prompts.""" if kwargs.get("system"): logger.warning( "As of now, Gemma does not support system messages officially. " diff --git a/src/lmflow/utils/data_utils.py b/src/lmflow/utils/data_utils.py index 7ce34772a..50b469e9e 100644 --- a/src/lmflow/utils/data_utils.py +++ b/src/lmflow/utils/data_utils.py @@ -17,7 +17,7 @@ def set_random_seed(seed: int): Set the random seed for `random`, `numpy`, `torch`, `torch.cuda`. Parameters - ------------ + ---------- seed : int The default seed. @@ -34,17 +34,17 @@ def load_data(file_name: str): Load data with file name. Parameters - ------------ - file_name : str. + ---------- + file_name : str The dataset file name. Returns - ------------ - inputs : list. + ------- + inputs : list The input texts of the dataset. - outputs : list. + outputs : list The output texts file datasets. - len : int. + length : int The length of the dataset. """ inputs = [] @@ -68,17 +68,17 @@ def batchlize(examples: list, batch_size: int, random_shuffle: bool): Convert examples to a dataloader. Parameters - ------------ - examples : list. + ---------- + examples : list Data list. - batch_size : int. - + batch_size : int + Number of examples in each batch. random_shuffle : bool If true, the dataloader shuffle the training data. Returns - ------------ - dataloader: + ------- + list Dataloader with batch generator. """ size = 0 @@ -152,22 +152,21 @@ def check_dataset_instances_key_fast(file_path: str, instances_key: str, max_lin return False -def answer_extraction(response, answer_type=None): # use this funtion to extract answers from generated text +def answer_extraction(response, answer_type=None): # use this function to extract answers from generated text """ - Use this funtion to extract answers from generated text + Extract answers from generated text. Parameters - ------------ - args : - Arguments. + ---------- response : str plain string response. - + answer_type : str, optional + Type of answer to extract. Returns - ------------ - answer: - Decoded answer (such as A, B, C, D, E for mutiple-choice QA). + ------- + str + Decoded answer (such as A, B, C, D, E for multiple-choice QA). """ # temp = response["generated_text"] @@ -286,10 +285,14 @@ def process_image_flag(text, image_flag=""): class VLLMInferenceResultWithInput(TypedDict): + """Structured vLLM inference result with its original input.""" + input: str output: Union[list[str], list[list[int]]] class RewardModelInferenceResultWithInput(TypedDict): + """Structured reward-model inference result with its original input.""" + input: str output: list[dict[str, Union[str, float]]] # [{"score": 0.5, "text": "output text"}]