We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Require `split_batch_fn` only for `grad_accum > 1`
For easy out-of-the-box use with custom datatypes, we should only require `split_batch_fn` if `grad_accum > 1`

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `composer/core/data_spec.py`
Content:
```
1 # Copyright 2021 MosaicML. All Rights Reserved.
2 
3 """Specifications for operating and training on data."""
4 from __future__ import annotations
5 
6 import collections.abc
7 import textwrap
8 from typing import TYPE_CHECKING, Callable, List, Optional, Sequence
9 
10 import torch
11 
12 from composer.utils.iter_helpers import ensure_tuple
13 
14 if TYPE_CHECKING:
15     from composer.core.types import Batch, DataLoader
16 
17 __all__ = ["DataSpec"]
18 
19 
20 class DataSpec:
21     """Specifications for operating and training on data.
22 
23     An example of constructing a :class:`DataSpec` object with a ``device_transforms`` callable
24     (:class:`~composer.datasets.utils.NormalizationFn`) and then using it with :class:`~.Trainer`:
25 
26        >>> # In this case, we apply NormalizationFn 
27        >>> # Construct DataSpec as shown below to apply this transformation
28        >>> from composer.datasets.utils import NormalizationFn
29        >>> CHANNEL_MEAN = (0.485 * 255, 0.456 * 255, 0.406 * 255)
30        >>> CHANNEL_STD = (0.229 * 255, 0.224 * 255, 0.225 * 255)
31        >>> device_transform_fn = NormalizationFn(mean=CHANNEL_MEAN, std=CHANNEL_STD)
32        >>> train_dspec = DataSpec(train_dataloader, device_transforms=device_transform_fn)
33        >>> # The same function can be used for eval dataloader as well
34        >>> eval_dspec = DataSpec(eval_dataloader, device_transforms=device_transform_fn)
35        >>> # Use this DataSpec object to construct trainer
36        >>> trainer = Trainer(
37        ...     model=model,
38        ...     train_dataloader=train_dspec,
39        ...     eval_dataloader=eval_dspec,
40        ...     optimizers=optimizer,
41        ...     max_duration="1ep",
42        ... )
43 
44     Args:
45         dataloader (DataLoader): The dataloader.
46 
47         num_samples (int, optional): The total number of samples in an epoch, across all ranks. This field is used by
48             the :class:`~.time.Timer` (training progress tracker). If not specified, then ``len(dataloader.dataset)`` is
49             used (if this property is available). Otherwise, the dataset is assumed to be unsized.
50 
51         num_tokens (int, optional): The total number of tokens in an epoch. This field is used by the
52             :class:`~.time.Timer` (training progress tracker).
53 
54         device_transforms ((Batch) -> Batch, optional): Function called by the :class:`~.trainer.Trainer` to modify the
55             batch once it has been moved onto the device. For example, this function can be used for GPU-based
56             normalization.  It can modify the batch in-place, and it should return the modified batch. If not specified, the
57             batch is not modified.
58 
59         split_batch ((Batch, int) -> Sequence[Batch], optional): Function called by the :class:`~.trainer.Trainer` to
60             split a batch (the first parameter) into the number of microbatches specified (the second parameter).  By
61             default, batches of type :attr:`~.types.BatchPair` can be split automatically. If the ``dataloader`` yields
62             batches of a different type, then this function must be specified.
63 
64         get_num_samples_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer`
65             to get the number of samples in the provided batch.
66 
67             By default, if the batch contains tensors that all have the same 0th dim, then the value of the 0th dim will
68             be returned. If the batch contains tensors where the 0th dim differ, then this function must be specified.
69 
70         get_num_tokens_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer` to
71             get the number of tokens in the provided batch.
72 
73             By default, it returns 0, meaning that number of tokens processed will not be tracked as a part of the
74             training progress tracking. This function must be specified to track the number of tokens processed during
75             training.
76     """
77 
78     def __init__(
79         self,
80         dataloader: DataLoader,
81         num_samples: Optional[int] = None,
82         num_tokens: Optional[int] = None,
83         device_transforms: Optional[Callable[[Batch], Batch]] = None,
84         split_batch: Optional[Callable[[Batch, int], Sequence[Batch]]] = None,
85         get_num_samples_in_batch: Optional[Callable[[Batch], int]] = None,
86         get_num_tokens_in_batch: Optional[Callable[[Batch], int]] = None,
87     ) -> None:
88         self.dataloader = dataloader
89         self.num_tokens = num_tokens
90         self.device_transforms = self._default_device_transforms if device_transforms is None else device_transforms
91         self.split_batch = self._default_split_batch if split_batch is None else split_batch
92         self.get_num_samples_in_batch = self._default_get_num_samples_in_batch if get_num_samples_in_batch is None else get_num_samples_in_batch
93         self.get_num_tokens_in_batch = self._default_get_num_tokens_in_batch if get_num_tokens_in_batch is None else get_num_tokens_in_batch
94         if num_samples is not None:
95             self.num_samples = num_samples
96 
97         else:
98             if isinstance(dataloader.dataset, collections.abc.Sized):
99                 try:
100                     self.num_samples = len(dataloader.dataset)
101                 except (TypeError, NotImplementedError):
102                     self.num_samples = None
103             else:
104                 self.num_samples = None
105 
106     def _default_device_transforms(self, batch: Batch):
107         return batch
108 
109     def _default_split_batch(self, batch: Batch, num_microbatches: int) -> Sequence[Batch]:
110         if not isinstance(batch, Sequence):
111             raise ValueError(f'split_fn requires batch be a tuple pair of tensors, got {type(batch)}')
112         x, y = batch
113         if isinstance(x, torch.Tensor) and isinstance(y, torch.Tensor):
114             return list(zip(x.chunk(num_microbatches), y.chunk(num_microbatches)))
115         if isinstance(x, List) and isinstance(y, List):
116             return list(
117                 zip(
118                     [x[i::num_microbatches] for i in range(num_microbatches)],
119                     [y[i::num_microbatches] for i in range(num_microbatches)],
120                 ))
121         raise NotImplementedError(
122             textwrap.dedent("""\
123                 The default split_fn is unable to split the output of this
124                 dataloader. Please use a DataSpec and specify `split_batch`."""))
125 
126     def _default_get_num_samples_in_batch(self, batch: Batch) -> int:
127         if isinstance(batch, torch.Tensor):
128             return batch.shape[0]
129 
130         dim0_sizes = []
131         if isinstance(batch, (list, tuple)):
132             for tensors in batch:
133                 for t in ensure_tuple(tensors):
134                     dim0_sizes.append(t.shape[0])
135         elif isinstance(batch, dict):
136             dim0_sizes = [t.shape[0] for t in batch.values()]
137 
138         if len(set(dim0_sizes)) == 1:
139             return dim0_sizes[0]
140         else:
141             raise NotImplementedError(
142                 textwrap.dedent(f"""\
143                     Cannot determine the batch size, as multiple Tensors of
144                     different lengths were found in the batch: sizes in batch: {dim0_sizes}.
145                     Please use a DataSpec and specify `get_num_samples_in_batch`."""))
146 
147     def _default_get_num_tokens_in_batch(self, batch: Batch) -> int:
148         del batch  # unused
149         return 0
150
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/composer/core/data_spec.py b/composer/core/data_spec.py
--- a/composer/core/data_spec.py
+++ b/composer/core/data_spec.py
@@ -107,6 +107,10 @@
         return batch
 
     def _default_split_batch(self, batch: Batch, num_microbatches: int) -> Sequence[Batch]:
+        if num_microbatches < 1:
+            raise ValueError("num_microbatches must be at least 1")
+        if num_microbatches == 1:
+            return [batch]
         if not isinstance(batch, Sequence):
             raise ValueError(f'split_fn requires batch be a tuple pair of tensors, got {type(batch)}')
         x, y = batch

{"golden_diff": "diff --git a/composer/core/data_spec.py b/composer/core/data_spec.py\n--- a/composer/core/data_spec.py\n+++ b/composer/core/data_spec.py\n@@ -107,6 +107,10 @@\n         return batch\n \n     def _default_split_batch(self, batch: Batch, num_microbatches: int) -> Sequence[Batch]:\n+        if num_microbatches < 1:\n+            raise ValueError(\"num_microbatches must be at least 1\")\n+        if num_microbatches == 1:\n+            return [batch]\n         if not isinstance(batch, Sequence):\n             raise ValueError(f'split_fn requires batch be a tuple pair of tensors, got {type(batch)}')\n         x, y = batch\n", "issue": "Require `split_batch_fn` only for `grad_accum > 1`\nFor easy out-of-the-box use with custom datatypes, we should only require `split_batch_fn` if `grad_accum > 1`\n", "before_files": [{"content": "# Copyright 2021 MosaicML. All Rights Reserved.\n\n\"\"\"Specifications for operating and training on data.\"\"\"\nfrom __future__ import annotations\n\nimport collections.abc\nimport textwrap\nfrom typing import TYPE_CHECKING, Callable, List, Optional, Sequence\n\nimport torch\n\nfrom composer.utils.iter_helpers import ensure_tuple\n\nif TYPE_CHECKING:\n    from composer.core.types import Batch, DataLoader\n\n__all__ = [\"DataSpec\"]\n\n\nclass DataSpec:\n    \"\"\"Specifications for operating and training on data.\n\n    An example of constructing a :class:`DataSpec` object with a ``device_transforms`` callable\n    (:class:`~composer.datasets.utils.NormalizationFn`) and then using it with :class:`~.Trainer`:\n\n       >>> # In this case, we apply NormalizationFn \n       >>> # Construct DataSpec as shown below to apply this transformation\n       >>> from composer.datasets.utils import NormalizationFn\n       >>> CHANNEL_MEAN = (0.485 * 255, 0.456 * 255, 0.406 * 255)\n       >>> CHANNEL_STD = (0.229 * 255, 0.224 * 255, 0.225 * 255)\n       >>> device_transform_fn = NormalizationFn(mean=CHANNEL_MEAN, std=CHANNEL_STD)\n       >>> train_dspec = DataSpec(train_dataloader, device_transforms=device_transform_fn)\n       >>> # The same function can be used for eval dataloader as well\n       >>> eval_dspec = DataSpec(eval_dataloader, device_transforms=device_transform_fn)\n       >>> # Use this DataSpec object to construct trainer\n       >>> trainer = Trainer(\n       ...     model=model,\n       ...     train_dataloader=train_dspec,\n       ...     eval_dataloader=eval_dspec,\n       ...     optimizers=optimizer,\n       ...     max_duration=\"1ep\",\n       ... )\n\n    Args:\n        dataloader (DataLoader): The dataloader.\n\n        num_samples (int, optional): The total number of samples in an epoch, across all ranks. This field is used by\n            the :class:`~.time.Timer` (training progress tracker). If not specified, then ``len(dataloader.dataset)`` is\n            used (if this property is available). Otherwise, the dataset is assumed to be unsized.\n\n        num_tokens (int, optional): The total number of tokens in an epoch. This field is used by the\n            :class:`~.time.Timer` (training progress tracker).\n\n        device_transforms ((Batch) -> Batch, optional): Function called by the :class:`~.trainer.Trainer` to modify the\n            batch once it has been moved onto the device. For example, this function can be used for GPU-based\n            normalization.  It can modify the batch in-place, and it should return the modified batch. If not specified, the\n            batch is not modified.\n\n        split_batch ((Batch, int) -> Sequence[Batch], optional): Function called by the :class:`~.trainer.Trainer` to\n            split a batch (the first parameter) into the number of microbatches specified (the second parameter).  By\n            default, batches of type :attr:`~.types.BatchPair` can be split automatically. If the ``dataloader`` yields\n            batches of a different type, then this function must be specified.\n\n        get_num_samples_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer`\n            to get the number of samples in the provided batch.\n\n            By default, if the batch contains tensors that all have the same 0th dim, then the value of the 0th dim will\n            be returned. If the batch contains tensors where the 0th dim differ, then this function must be specified.\n\n        get_num_tokens_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer` to\n            get the number of tokens in the provided batch.\n\n            By default, it returns 0, meaning that number of tokens processed will not be tracked as a part of the\n            training progress tracking. This function must be specified to track the number of tokens processed during\n            training.\n    \"\"\"\n\n    def __init__(\n        self,\n        dataloader: DataLoader,\n        num_samples: Optional[int] = None,\n        num_tokens: Optional[int] = None,\n        device_transforms: Optional[Callable[[Batch], Batch]] = None,\n        split_batch: Optional[Callable[[Batch, int], Sequence[Batch]]] = None,\n        get_num_samples_in_batch: Optional[Callable[[Batch], int]] = None,\n        get_num_tokens_in_batch: Optional[Callable[[Batch], int]] = None,\n    ) -> None:\n        self.dataloader = dataloader\n        self.num_tokens = num_tokens\n        self.device_transforms = self._default_device_transforms if device_transforms is None else device_transforms\n        self.split_batch = self._default_split_batch if split_batch is None else split_batch\n        self.get_num_samples_in_batch = self._default_get_num_samples_in_batch if get_num_samples_in_batch is None else get_num_samples_in_batch\n        self.get_num_tokens_in_batch = self._default_get_num_tokens_in_batch if get_num_tokens_in_batch is None else get_num_tokens_in_batch\n        if num_samples is not None:\n            self.num_samples = num_samples\n\n        else:\n            if isinstance(dataloader.dataset, collections.abc.Sized):\n                try:\n                    self.num_samples = len(dataloader.dataset)\n                except (TypeError, NotImplementedError):\n                    self.num_samples = None\n            else:\n                self.num_samples = None\n\n    def _default_device_transforms(self, batch: Batch):\n        return batch\n\n    def _default_split_batch(self, batch: Batch, num_microbatches: int) -> Sequence[Batch]:\n        if not isinstance(batch, Sequence):\n            raise ValueError(f'split_fn requires batch be a tuple pair of tensors, got {type(batch)}')\n        x, y = batch\n        if isinstance(x, torch.Tensor) and isinstance(y, torch.Tensor):\n            return list(zip(x.chunk(num_microbatches), y.chunk(num_microbatches)))\n        if isinstance(x, List) and isinstance(y, List):\n            return list(\n                zip(\n                    [x[i::num_microbatches] for i in range(num_microbatches)],\n                    [y[i::num_microbatches] for i in range(num_microbatches)],\n                ))\n        raise NotImplementedError(\n            textwrap.dedent(\"\"\"\\\n                The default split_fn is unable to split the output of this\n                dataloader. Please use a DataSpec and specify `split_batch`.\"\"\"))\n\n    def _default_get_num_samples_in_batch(self, batch: Batch) -> int:\n        if isinstance(batch, torch.Tensor):\n            return batch.shape[0]\n\n        dim0_sizes = []\n        if isinstance(batch, (list, tuple)):\n            for tensors in batch:\n                for t in ensure_tuple(tensors):\n                    dim0_sizes.append(t.shape[0])\n        elif isinstance(batch, dict):\n            dim0_sizes = [t.shape[0] for t in batch.values()]\n\n        if len(set(dim0_sizes)) == 1:\n            return dim0_sizes[0]\n        else:\n            raise NotImplementedError(\n                textwrap.dedent(f\"\"\"\\\n                    Cannot determine the batch size, as multiple Tensors of\n                    different lengths were found in the batch: sizes in batch: {dim0_sizes}.\n                    Please use a DataSpec and specify `get_num_samples_in_batch`.\"\"\"))\n\n    def _default_get_num_tokens_in_batch(self, batch: Batch) -> int:\n        del batch  # unused\n        return 0\n", "path": "composer/core/data_spec.py"}], "after_files": [{"content": "# Copyright 2021 MosaicML. All Rights Reserved.\n\n\"\"\"Specifications for operating and training on data.\"\"\"\nfrom __future__ import annotations\n\nimport collections.abc\nimport textwrap\nfrom typing import TYPE_CHECKING, Callable, List, Optional, Sequence\n\nimport torch\n\nfrom composer.utils.iter_helpers import ensure_tuple\n\nif TYPE_CHECKING:\n    from composer.core.types import Batch, DataLoader\n\n__all__ = [\"DataSpec\"]\n\n\nclass DataSpec:\n    \"\"\"Specifications for operating and training on data.\n\n    An example of constructing a :class:`DataSpec` object with a ``device_transforms`` callable\n    (:class:`~composer.datasets.utils.NormalizationFn`) and then using it with :class:`~.Trainer`:\n\n       >>> # In this case, we apply NormalizationFn \n       >>> # Construct DataSpec as shown below to apply this transformation\n       >>> from composer.datasets.utils import NormalizationFn\n       >>> CHANNEL_MEAN = (0.485 * 255, 0.456 * 255, 0.406 * 255)\n       >>> CHANNEL_STD = (0.229 * 255, 0.224 * 255, 0.225 * 255)\n       >>> device_transform_fn = NormalizationFn(mean=CHANNEL_MEAN, std=CHANNEL_STD)\n       >>> train_dspec = DataSpec(train_dataloader, device_transforms=device_transform_fn)\n       >>> # The same function can be used for eval dataloader as well\n       >>> eval_dspec = DataSpec(eval_dataloader, device_transforms=device_transform_fn)\n       >>> # Use this DataSpec object to construct trainer\n       >>> trainer = Trainer(\n       ...     model=model,\n       ...     train_dataloader=train_dspec,\n       ...     eval_dataloader=eval_dspec,\n       ...     optimizers=optimizer,\n       ...     max_duration=\"1ep\",\n       ... )\n\n    Args:\n        dataloader (DataLoader): The dataloader.\n\n        num_samples (int, optional): The total number of samples in an epoch, across all ranks. This field is used by\n            the :class:`~.time.Timer` (training progress tracker). If not specified, then ``len(dataloader.dataset)`` is\n            used (if this property is available). Otherwise, the dataset is assumed to be unsized.\n\n        num_tokens (int, optional): The total number of tokens in an epoch. This field is used by the\n            :class:`~.time.Timer` (training progress tracker).\n\n        device_transforms ((Batch) -> Batch, optional): Function called by the :class:`~.trainer.Trainer` to modify the\n            batch once it has been moved onto the device. For example, this function can be used for GPU-based\n            normalization.  It can modify the batch in-place, and it should return the modified batch. If not specified, the\n            batch is not modified.\n\n        split_batch ((Batch, int) -> Sequence[Batch], optional): Function called by the :class:`~.trainer.Trainer` to\n            split a batch (the first parameter) into the number of microbatches specified (the second parameter).  By\n            default, batches of type :attr:`~.types.BatchPair` can be split automatically. If the ``dataloader`` yields\n            batches of a different type, then this function must be specified.\n\n        get_num_samples_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer`\n            to get the number of samples in the provided batch.\n\n            By default, if the batch contains tensors that all have the same 0th dim, then the value of the 0th dim will\n            be returned. If the batch contains tensors where the 0th dim differ, then this function must be specified.\n\n        get_num_tokens_in_batch ((Batch) -> int, optional): Function that is called by the :class:`~.trainer.Trainer` to\n            get the number of tokens in the provided batch.\n\n            By default, it returns 0, meaning that number of tokens processed will not be tracked as a part of the\n            training progress tracking. This function must be specified to track the number of tokens processed during\n            training.\n    \"\"\"\n\n    def __init__(\n        self,\n        dataloader: DataLoader,\n        num_samples: Optional[int] = None,\n        num_tokens: Optional[int] = None,\n        device_transforms: Optional[Callable[[Batch], Batch]] = None,\n        split_batch: Optional[Callable[[Batch, int], Sequence[Batch]]] = None,\n        get_num_samples_in_batch: Optional[Callable[[Batch], int]] = None,\n        get_num_tokens_in_batch: Optional[Callable[[Batch], int]] = None,\n    ) -> None:\n        self.dataloader = dataloader\n        self.num_tokens = num_tokens\n        self.device_transforms = self._default_device_transforms if device_transforms is None else device_transforms\n        self.split_batch = self._default_split_batch if split_batch is None else split_batch\n        self.get_num_samples_in_batch = self._default_get_num_samples_in_batch if get_num_samples_in_batch is None else get_num_samples_in_batch\n        self.get_num_tokens_in_batch = self._default_get_num_tokens_in_batch if get_num_tokens_in_batch is None else get_num_tokens_in_batch\n        if num_samples is not None:\n            self.num_samples = num_samples\n\n        else:\n            if isinstance(dataloader.dataset, collections.abc.Sized):\n                try:\n                    self.num_samples = len(dataloader.dataset)\n                except (TypeError, NotImplementedError):\n                    self.num_samples = None\n            else:\n                self.num_samples = None\n\n    def _default_device_transforms(self, batch: Batch):\n        return batch\n\n    def _default_split_batch(self, batch: Batch, num_microbatches: int) -> Sequence[Batch]:\n        if num_microbatches < 1:\n            raise ValueError(\"num_microbatches must be at least 1\")\n        if num_microbatches == 1:\n            return [batch]\n        if not isinstance(batch, Sequence):\n            raise ValueError(f'split_fn requires batch be a tuple pair of tensors, got {type(batch)}')\n        x, y = batch\n        if isinstance(x, torch.Tensor) and isinstance(y, torch.Tensor):\n            return list(zip(x.chunk(num_microbatches), y.chunk(num_microbatches)))\n        if isinstance(x, List) and isinstance(y, List):\n            return list(\n                zip(\n                    [x[i::num_microbatches] for i in range(num_microbatches)],\n                    [y[i::num_microbatches] for i in range(num_microbatches)],\n                ))\n        raise NotImplementedError(\n            textwrap.dedent(\"\"\"\\\n                The default split_fn is unable to split the output of this\n                dataloader. Please use a DataSpec and specify `split_batch`.\"\"\"))\n\n    def _default_get_num_samples_in_batch(self, batch: Batch) -> int:\n        if isinstance(batch, torch.Tensor):\n            return batch.shape[0]\n\n        dim0_sizes = []\n        if isinstance(batch, (list, tuple)):\n            for tensors in batch:\n                for t in ensure_tuple(tensors):\n                    dim0_sizes.append(t.shape[0])\n        elif isinstance(batch, dict):\n            dim0_sizes = [t.shape[0] for t in batch.values()]\n\n        if len(set(dim0_sizes)) == 1:\n            return dim0_sizes[0]\n        else:\n            raise NotImplementedError(\n                textwrap.dedent(f\"\"\"\\\n                    Cannot determine the batch size, as multiple Tensors of\n                    different lengths were found in the batch: sizes in batch: {dim0_sizes}.\n                    Please use a DataSpec and specify `get_num_samples_in_batch`.\"\"\"))\n\n    def _default_get_num_tokens_in_batch(self, batch: Batch) -> int:\n        del batch  # unused\n        return 0\n", "path": "composer/core/data_spec.py"}]}