Running a DataFlow in >=1 processes using Python multiprocessing utilities. It will fork the process that calls :meth:`__init__`, collect datapoints from `ds` in each process by a Python :class:`multiprocessing.Queue`. Note: 1. (Data integrity) An iterator cannot run faster
| 140 | |
| 141 | |
| 142 | class MultiProcessRunner(ProxyDataFlow): |
| 143 | """ |
| 144 | Running a DataFlow in >=1 processes using Python multiprocessing utilities. |
| 145 | It will fork the process that calls :meth:`__init__`, collect datapoints from `ds` in each |
| 146 | process by a Python :class:`multiprocessing.Queue`. |
| 147 | |
| 148 | Note: |
| 149 | 1. (Data integrity) An iterator cannot run faster automatically -- what's happening is |
| 150 | that the process will be forked ``num_proc`` times. |
| 151 | There will be ``num_proc`` dataflow running in parallel and **independently**. |
| 152 | As a result, we have the following guarantee on the dataflow correctness: |
| 153 | |
| 154 | a. When ``num_proc=1``, this dataflow produces the same data as the |
| 155 | given dataflow in the same order. |
| 156 | b. When ``num_proc>1``, if each sample from the given dataflow is i.i.d., |
| 157 | then this dataflow produces the **same distribution** of data as the given dataflow. |
| 158 | This implies that there will be duplication, reordering, etc. |
| 159 | You probably only want to use it for training. |
| 160 | |
| 161 | For example, if your original dataflow contains no randomness and produces the same first datapoint, |
| 162 | then after parallel prefetching, the datapoint will be produced ``num_proc`` times |
| 163 | at the beginning. |
| 164 | Even when your original dataflow is fully shuffled, you still need to be aware of the |
| 165 | `Birthday Paradox <https://en.wikipedia.org/wiki/Birthday_problem>`_ |
| 166 | and know that you'll likely see duplicates. |
| 167 | |
| 168 | To utilize parallelism with more strict data integrity, you can use |
| 169 | the parallel versions of :class:`MapData`: :class:`MultiThreadMapData`, :class:`MultiProcessMapData`. |
| 170 | 2. This has more serialization overhead than :class:`MultiProcessRunnerZMQ` when data is large. |
| 171 | 3. You can nest like this: ``MultiProcessRunnerZMQ(MultiProcessRunner(df, num_proc=a), num_proc=b)``. |
| 172 | A total of ``a`` instances of ``df`` worker processes will be created. |
| 173 | 4. Fork happens in `__init__`. `reset_state()` is a no-op. |
| 174 | DataFlow in the worker processes will be reset at the time of fork. |
| 175 | 5. This DataFlow does support windows. However, Windows requires more strict picklability on processes, |
| 176 | which means that some code that's forkable on Linux may not be forkable on Windows. If that happens you'll |
| 177 | need to re-organize some part of code that's not forkable. |
| 178 | """ |
| 179 | |
| 180 | class _Worker(mp.Process): |
| 181 | def __init__(self, ds, queue, idx): |
| 182 | super(MultiProcessRunner._Worker, self).__init__() |
| 183 | self.ds = ds |
| 184 | self.queue = queue |
| 185 | self.idx = idx |
| 186 | |
| 187 | def run(self): |
| 188 | enable_death_signal(_warn=self.idx == 0) |
| 189 | # reset all ds so each process will produce different data |
| 190 | self.ds.reset_state() |
| 191 | while True: |
| 192 | for dp in self.ds: |
| 193 | self.queue.put(dp) |
| 194 | |
| 195 | def __init__(self, ds, num_prefetch, num_proc): |
| 196 | """ |
| 197 | Args: |
| 198 | ds (DataFlow): input DataFlow. |
| 199 | num_prefetch (int): size of the queue to hold prefetched datapoints. |