diff --git a/doc/release/upcoming_changes/17394.new_function.rst b/doc/release/upcoming_changes/17394.new_function.rst
new file mode 100644
index 000000000000..50c9d1db310d
--- /dev/null
+++ b/doc/release/upcoming_changes/17394.new_function.rst
@@ -0,0 +1,5 @@
+``sliding_window_view`` provides a sliding window view for numpy arrays
+-----------------------------------------------------------------------
+`numpy.sliding_window_view` constructs views on numpy arrays that offer a
+sliding or moving window access to the array. This allows for the simple
+implementation of certain algorithms, such as running means.
diff --git a/doc/source/reference/routines.indexing.rst b/doc/source/reference/routines.indexing.rst
index aeec1a1bba55..a1c82c620340 100644
--- a/doc/source/reference/routines.indexing.rst
+++ b/doc/source/reference/routines.indexing.rst
@@ -42,6 +42,7 @@ Indexing-like operations
    diag
    diagonal
    select
+   sliding_window_view
    lib.stride_tricks.as_strided

 Inserting data into arrays
diff --git a/numpy/lib/stride_tricks.py b/numpy/lib/stride_tricks.py
index d8a8b325e3a4..a6c0da751abc 100644
--- a/numpy/lib/stride_tricks.py
+++ b/numpy/lib/stride_tricks.py
@@ -6,9 +6,11 @@

 """
 import numpy as np
+from numpy.core.numeric import normalize_axis_tuple
 from numpy.core.overrides import array_function_dispatch, set_module

-__all__ = ['broadcast_to', 'broadcast_arrays', 'broadcast_shapes']
+__all__ = ['broadcast_to', 'broadcast_arrays', 'broadcast_shapes',
+           'sliding_window_view']

 class DummyArray:
@@ -67,6 +69,8 @@ def as_strided(x, shape=None, strides=None, subok=False, writeable=True):
     --------
     broadcast_to: broadcast an array to a given shape.
     reshape : reshape an array.
+    sliding_window_view: userfriendly and safe function for the creation of
+        sliding window views.

     Notes
     -----
@@ -111,6 +115,228 @@ def as_strided(x, shape=None, strides=None, subok=False, writeable=True):
     return view

+def _sliding_window_view_dispatcher(x, window_shape, axis=None, *,
+                                    subok=None, writeable=None):
+    return (x,)
+
+
+@array_function_dispatch(_sliding_window_view_dispatcher, module='numpy')
+def sliding_window_view(x, window_shape, axis=None, *,
+                        subok=False, writeable=False):
+    """
+    Create a sliding window view into the array with the given window shape.
+
+    Also known as rolling or moving window, the window slides across all
+    dimensions of the array and extracts subsets of the array at all window
+    positions.
+
+    .. versionadded:: 1.20.0
+
+    Parameters
+    ----------
+    x : array_like
+        Array to create the sliding window view from.
+    window_shape : int or tuple of int
+        Size of window over each axis that takes part in the sliding window.
+        If `axis` is not present, must have same length as the number of input
+        array dimensions. Single integers `i` are treated as if they were the
+        tuple `(i,)`.
+    axis : int or tuple of int, optional
+        Axis or axes along which the sliding window is applied.
+        By default, the sliding window is applied to all axes and
+        `window_shape[i]` will refer to axis `i` of `x`.
+        If `axis` is given as a `tuple of int`, `window_shape[i]` will refer to
+        the axis `axis[i]` of `x`.
+        Single integers `i` are treated as if they were the tuple `(i,)`.
+    subok : bool, optional
+        If True, sub-classes will be passed-through, otherwise the returned
+        array will be forced to be a base-class array (default).
+    writeable : bool, optional
+        When true, allow writing to the returned view. The default is false,
+        as this should be used with caution: the returned view contains the
+        same memory location multiple times, so writing to one location will
+        cause others to change.
+
+    Returns
+    -------
+    view : ndarray
+        Sliding window view of the array. The sliding window dimensions are
+        inserted at the end, and the original dimensions are trimmed as
+        required by the size of the sliding window.
+        That is, ``view.shape = x_shape_trimmed + window_shape``, where
+        ``x_shape_trimmed`` is ``x.shape`` with every entry reduced by one less
+        than the corresponding window size.
+
+    See Also
+    --------
+    lib.stride_tricks.as_strided: A lower-level and less safe routine for
+        creating arbitrary views from custom shape and strides.
+    broadcast_to: broadcast an array to a given shape.
+
+    Notes
+    -----
+    For many applications using a sliding window view can be convenient, but
+    potentially very slow. Often specialized solutions exist, for example:
+
+    - `scipy.signal.fftconvolve`
+
+    - filtering functions in `scipy.ndimage`
+
+    - moving window functions provided by
+      `bottleneck <https://github.com/pydata/bottleneck>`_.
+
+    As a rough estimate, a sliding window approach with an input size of `N`
+    and a window size of `W` will scale as `O(N*W)` where frequently a special
+    algorithm can achieve `O(N)`. That means that the sliding window variant
+    for a window size of 100 can be a 100 times slower than a more specialized
+    version.
+
+    Nevertheless, for small window sizes, when no custom algorithm exists, or
+    as a prototyping and developing tool, this function can be a good solution.
+
+    Examples
+    --------
+    >>> x = np.arange(6)
+    >>> x.shape
+    (6,)
+    >>> v = np.sliding_window_view(x, 3)
+    >>> v.shape
+    (4, 3)
+    >>> v
+    array([[0, 1, 2],
+           [1, 2, 3],
+           [2, 3, 4],
+           [3, 4, 5]])
+
+    This also works in more dimensions, e.g.
+
+    >>> i, j = np.ogrid[:3, :4]
+    >>> x = 10*i + j
+    >>> x.shape
+    (3, 4)
+    >>> x
+    array([[ 0,  1,  2,  3],
+           [10, 11, 12, 13],
+           [20, 21, 22, 23]])
+    >>> shape = (2,2)
+    >>> v = np.sliding_window_view(x, shape)
+    >>> v.shape
+    (2, 3, 2, 2)
+    >>> v
+    array([[[[ 0,  1],
+             [10, 11]],
+            [[ 1,  2],
+             [11, 12]],
+            [[ 2,  3],
+             [12, 13]]],
+           [[[10, 11],
+             [20, 21]],
+            [[11, 12],
+             [21, 22]],
+            [[12, 13],
+             [22, 23]]]])
+
+    The axis can be specified explicitly:
+
+    >>> v = np.sliding_window_view(x, 3, 0)
+    >>> v.shape
+    (1, 4, 3)
+    >>> v
+    array([[[ 0, 10, 20],
+            [ 1, 11, 21],
+            [ 2, 12, 22],
+            [ 3, 13, 23]]])
+
+    The same axis can be used several times. In that case, every use reduces
+    the corresponding original dimension:
+
+    >>> v = np.sliding_window_view(x, (2, 3), (1, 1))
+    >>> v.shape
+    (3, 1, 2, 3)
+    >>> v
+    array([[[[ 0,  1,  2],
+             [ 1,  2,  3]]],
+           [[[10, 11, 12],
+             [11, 12, 13]]],
+           [[[20, 21, 22],
+             [21, 22, 23]]]])
+
+    Combining with stepped slicing (`::step`), this can be used to take sliding
+    views which skip elements:
+
+    >>> x = np.arange(7)
+    >>> np.sliding_window_view(x, 5)[:, ::2]
+    array([[0, 2, 4],
+           [1, 3, 5],
+           [2, 4, 6]])
+
+    or views which move by multiple elements
+
+    >>> x = np.arange(7)
+    >>> np.sliding_window_view(x, 3)[::2, :]
+    array([[0, 1, 2],
+           [2, 3, 4],
+           [4, 5, 6]])
+
+    A common application of `sliding_window_view` is the calculation of running
+    statistics. The simplest example is the
+    `moving average <https://en.wikipedia.org/wiki/Moving_average>`_:
+
+    >>> x = np.arange(6)
+    >>> x.shape
+    (6,)
+    >>> v = np.sliding_window_view(x, 3)
+    >>> v.shape
+    (4, 3)
+    >>> v
+    array([[0, 1, 2],
+           [1, 2, 3],
+           [2, 3, 4],
+           [3, 4, 5]])
+    >>> moving_average = v.mean(axis=-1)
+    >>> moving_average
+    array([1., 2., 3., 4.])
+
+    Note that a sliding window approach is often **not** optimal (see Notes).
+    """
+    window_shape = (tuple(window_shape)
+                    if np.iterable(window_shape)
+                    else (window_shape,))
+    # first convert input to array, possibly keeping subclass
+    x = np.array(x, copy=False, subok=subok)
+
+    window_shape_array = np.array(window_shape)
+    if np.any(window_shape_array < 0):
+        raise ValueError('`window_shape` cannot contain negative values')
+
+    if axis is None:
+        axis = tuple(range(x.ndim))
+        if len(window_shape) != len(axis):
+            raise ValueError(f'Since axis is `None`, must provide '
+                             f'window_shape for all dimensions of `x`; '
+                             f'got {len(window_shape)} window_shape elements '
+                             f'and `x.ndim` is {x.ndim}.')
+    else:
+        axis = normalize_axis_tuple(axis, x.ndim, allow_duplicate=True)
+        if len(window_shape) != len(axis):
+            raise ValueError(f'Must provide matching length window_shape and '
+                             f'axis; got {len(window_shape)} window_shape '
+                             f'elements and {len(axis)} axes elements.')
+
+    out_strides = x.strides + tuple(x.strides[ax] for ax in axis)
+
+    # note: same axis can be windowed repeatedly
+    x_shape_trimmed = list(x.shape)
+    for ax, dim in zip(axis, window_shape):
+        if x_shape_trimmed[ax] < dim:
+            raise ValueError(
+                'window shape cannot be larger than input array shape')
+        x_shape_trimmed[ax] -= dim - 1
+    out_shape = tuple(x_shape_trimmed) + window_shape
+    return as_strided(x, strides=out_strides, shape=out_shape,
+                      subok=subok, writeable=writeable)
+
+
 def _broadcast_to(array, shape, subok, readonly):
     shape = tuple(shape) if np.iterable(shape) else (shape,)
     array = np.array(array, copy=False, subok=subok)

diff --git a/numpy/lib/tests/test_stride_tricks.py b/numpy/lib/tests/test_stride_tricks.py
index 10d7a19abec0..efec5d24dad4 100644
--- a/numpy/lib/tests/test_stride_tricks.py
+++ b/numpy/lib/tests/test_stride_tricks.py
@@ -6,8 +6,10 @@
     )
 from numpy.lib.stride_tricks import (
     as_strided, broadcast_arrays, _broadcast_shape, broadcast_to,
-    broadcast_shapes,
+    broadcast_shapes, sliding_window_view,
     )
+import pytest
+

 def assert_shapes_correct(input_shapes, expected_shape):
     # Broadcast a list of arrays with the given input shapes and check the
@@ -394,6 +396,109 @@ def test_as_strided():
     assert_equal(a.dtype, a_view.dtype)
     assert_array_equal([r] * 3, a_view)

+
+class TestSlidingWindowView:
+    def test_1d(self):
+        arr = np.arange(5)
+        arr_view = sliding_window_view(arr, 2)
+        expected = np.array([[0, 1],
+                             [1, 2],
+                             [2, 3],
+                             [3, 4]])
+        assert_array_equal(arr_view, expected)
+
+    def test_2d(self):
+        i, j = np.ogrid[:3, :4]
+        arr = 10*i + j
+        shape = (2, 2)
+        arr_view = sliding_window_view(arr, shape)
+        expected = np.array([[[[0, 1], [10, 11]],
+                              [[1, 2], [11, 12]],
+                              [[2, 3], [12, 13]]],
+                             [[[10, 11], [20, 21]],
+                              [[11, 12], [21, 22]],
+                              [[12, 13], [22, 23]]]])
+        assert_array_equal(arr_view, expected)
+
+    def test_2d_with_axis(self):
+        i, j = np.ogrid[:3, :4]
+        arr = 10*i + j
+        arr_view = sliding_window_view(arr, 3, 0)
+        expected = np.array([[[0, 10, 20],
+                              [1, 11, 21],
+                              [2, 12, 22],
+                              [3, 13, 23]]])
+        assert_array_equal(arr_view, expected)
+
+    def test_2d_repeated_axis(self):
+        i, j = np.ogrid[:3, :4]
+        arr = 10*i + j
+        arr_view = sliding_window_view(arr, (2, 3), (1, 1))
+        expected = np.array([[[[0, 1, 2],
+                               [1, 2, 3]]],
+                             [[[10, 11, 12],
+                               [11, 12, 13]]],
+                             [[[20, 21, 22],
+                               [21, 22, 23]]]])
+        assert_array_equal(arr_view, expected)
+
+    def test_2d_without_axis(self):
+        i, j = np.ogrid[:4, :4]
+        arr = 10*i + j
+        shape = (2, 3)
+        arr_view = sliding_window_view(arr, shape)
+        expected = np.array([[[[0, 1, 2], [10, 11, 12]],
+                              [[1, 2, 3], [11, 12, 13]]],
+                             [[[10, 11, 12], [20, 21, 22]],
+                              [[11, 12, 13], [21, 22, 23]]],
+                             [[[20, 21, 22], [30, 31, 32]],
+                              [[21, 22, 23], [31, 32, 33]]]])
+        assert_array_equal(arr_view, expected)
+
+    def test_errors(self):
+        i, j = np.ogrid[:4, :4]
+        arr = 10*i + j
+        with pytest.raises(ValueError, match='cannot contain negative values'):
+            sliding_window_view(arr, (-1, 3))
+        with pytest.raises(
+                ValueError,
+                match='must provide window_shape for all dimensions of `x`'):
+            sliding_window_view(arr, (1,))
+        with pytest.raises(
+                ValueError,
+                match='Must provide matching length window_shape and axis'):
+            sliding_window_view(arr, (1, 3, 4), axis=(0, 1))
+        with pytest.raises(
+                ValueError,
+                match='window shape cannot be larger than input array'):
+            sliding_window_view(arr, (5, 5))
+
+    def test_writeable(self):
+        arr = np.arange(5)
+        view = sliding_window_view(arr, 2, writeable=False)
+        assert_(not view.flags.writeable)
+        with pytest.raises(
+                ValueError,
+                match='assignment destination is read-only'):
+            view[0, 0] = 3
+        view = sliding_window_view(arr, 2, writeable=True)
+        assert_(view.flags.writeable)
+        view[0, 1] = 3
+        assert_array_equal(arr, np.array([0, 3, 2, 3, 4]))
+
+    def test_subok(self):
+        class MyArray(np.ndarray):
+            pass
+
+        arr = np.arange(5).view(MyArray)
+        assert_(not isinstance(sliding_window_view(arr, 2,
+                                                   subok=False),
+                               MyArray))
+        assert_(isinstance(sliding_window_view(arr, 2, subok=True), MyArray))
+        # Default behavior
+        assert_(not isinstance(sliding_window_view(arr, 2), MyArray))
+
+
 def as_strided_writeable():
     arr = np.ones(10)
     view = as_strided(arr, writeable=False)
@@ -496,7 +601,7 @@ def test_writeable():
             # check: no warning emitted
             assert_equal(result.flags.writeable, True)
             result[:] = 0
-
+
     # keep readonly input readonly
     original.flags.writeable = False
     _, result = broadcast_arrays(0, original)

Suggestion: Sliding Window Function
Using `np.lib.stride_tricks.as_stride` one can very efficiently create a sliding window that segments an array as a preprocessing step for vectorized applications. For example a moving average of a window length `3`, stepsize `1`:

```
a = numpy.arange(10)
a_strided = numpy.lib.stride_tricks.as_strided(
    a, shape=(8, 3), strides=(8, 8)
)
print numpy.mean(a_strided, axis=1)
```

This is very performant but very hard to do, as the `shape` and `strides` parameters are very hard to understand.

I suggest the implementation of a "simple `sliding_window` function" that does the job of figuring out those two parameters for you.

[The implementation I have been using for years](https://gist.github.com/nils-werner/9d321441006b112a4b116a8387c2280c) allows the above to be replaced by

```
a = numpy.arange(10)
a_strided = sliding_window(a, size=3, stepsize=1)
print numpy.mean(a_strided, axis=1)
```

@teoliphant [also has an implementation](https://gist.github.com/teoliphant/96eb779a16bd038e374f2703da62f06d) that would change the above to

```
a = numpy.arange(10)
a_strided = array_for_sliding_window(a, 3)
print numpy.mean(a_strided, axis=1)
```

both making it much more readable.

Seeing it is a common usecase in vectorized computing I suggest we put a similar function into NumPy itself.

Regarding to which implementation to follow, they are both assume different things but allow you to do the same thing eventually:

`sliding_window`
- slides over one axis only
- allows setting windowsize and stepsize
- returns array with dimension n+1
- sliding over several axes requires two calls (which come for free as there is no memory reordered)
- has a superfluous `copy` parameter that can be removed and replaced by appending `.copy()` after the call

`array_for_sliding_window`
- slides over all axes simultaneously, window lengths are given as `tuple` parameter
- Assumes a stepsize one in all directions
- returns array with dimension n*2
- stepsize not equal to one requires slicing of output data (unsure if this implies copying data)
- Disabling sliding over `axis[n]` requires you set argument `wshape[n] = 1` or `wshape[n] = a.shape[n]`

This means for flexible `stepsize` the following are equivalent (with some minor bug in `sliding_window` there):

```
a = numpy.arange(10)
print sliding_window(a, size=3, stepsize=2)

a = numpy.arange(10)
print array_for_sliding_window(a, 3)[::2, :] # Stepsize 2 by dropping every 2nd row
```

for sliding over one `axis` the following are equivalent (with some transposing and squeezing):

```
a = numpy.arange(25).reshape(5, 5)
print sliding_window(a, size=3, axis=1)

a = numpy.arange(25).reshape(5, 5)
print array_for_sliding_window(a, (1, 3))
```

and for sliding over two `axis` the following are equivalent:

```
a = numpy.arange(25).reshape(5, 5)
print sliding_window(sliding_window(a, size=3, axis=0), size=2, axis=1)

a = numpy.arange(25).reshape(5, 5)
print array_for_sliding_window(a, (3, 2))
```

This issue is about sparking discussion about
- Do we need such a function?
- Which features are required?
- Which interface should we persue?

After discussion I am willing to draft up a pull request with an implementation we agreed on.


diff --git a/HISTORY.rst b/HISTORY.rst
index 1d6a6ebc5..0bd3055e1 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -4,6 +4,7 @@ History

 0.18.x
 ------
+* Optimization options for `xclim.sdba` : different grouping for the normalization steps of DQM and save training or fitting datasets to temporary files.
 * `xclim.sdba.detrending` objects can now act on groups.
 * Replaced `dask[complete]` with `dask[array]` in basic installation and added `distributed` to `docs` build dependencies.
 * `xclim.core.locales` now supported in Windows build environments.
diff --git a/xclim/sdba/adjustment.py b/xclim/sdba/adjustment.py
index 81c6f446f..9a4a8d8ff 100644
--- a/xclim/sdba/adjustment.py
+++ b/xclim/sdba/adjustment.py
@@ -224,9 +224,10 @@ class DetrendedQuantileMapping(EmpiricalQuantileMapping):
     The algorithm follows these steps, 1-3 being the 'train' and 4-6, the 'adjust' steps.

     1. A scaling factor that would make the mean of `hist` match the mean of `ref` is computed.
-    2. `ref` and `hist` are normalized by removing the group-wise mean.
+    2. `ref` and `hist` are normalized by removing the "dayofyear" mean.
     3. Adjustment factors are computed between the quantiles of the normalized `ref` and `hist`.
-    4. `sim` is corrected by the scaling factor, and detrended group-wise using a linear fit.
+    4. `sim` is corrected by the scaling factor, and either normalized by "dayofyear" and  detrended group-wise
+       or directly detrended per "dayofyear", using a linear fit (modifiable).
     5. Values of detrended `sim` are matched to the corresponding quantiles of normalized `hist` and corrected accordingly.
     6. The trend is put back on the result.

@@ -246,7 +247,10 @@ class DetrendedQuantileMapping(EmpiricalQuantileMapping):
     kind : {'+', '*'}
       The adjustment kind, either additive or multiplicative.
     group : Union[str, Grouper]
-      The grouping information. See :py:class:`xclim.sdba.base.Grouper` for details.
+      The grouping information used in the quantile mapping process. See :py:class:`xclim.sdba.base.Grouper` for details.
+      the normalization step is always performed on each day of the year.
+    norm_window : 1
+      The window size used in the normalization grouping. Defaults to 1.

     In adjustment:

@@ -256,26 +260,54 @@ class DetrendedQuantileMapping(EmpiricalQuantileMapping):
       The method to use when detrending. If an int is passed, it is understood as a PolyDetrend (polynomial detrending) degree. Defaults to 1 (linear detrending)
     extrapolation : {'constant', 'nan'}
       The type of extrapolation to use. See :py:func:`xclim.sdba.utils.extrapolate_qm` for details. Defaults to "constant".
+    normalize_sim : bool
+      If True, scaled sim is normalized by its "dayofyear" mean and then detrended using `group`.
+        The norm is broadcasted and added back on scen using `interp='nearest'`, ignoring the passed `interp`.
+      If False, scaled sim is detrended per "dayofyear".
+      This is useful on large datasets using dask, in which case "dayofyear" is a very small division,
+        because normalization is a more efficient operation than detrending for similarly sized groups.

     References
     ----------
     .. [Cannon2015] Cannon, A. J., Sobie, S. R., & Murdock, T. Q. (2015). Bias correction of GCM precipitation by quantile mapping: How well do methods preserve changes in quantiles and extremes? Journal of Climate, 28(17), 6938–6959. https://doi.org/10.1175/JCLI-D-14-00754.1
     """

+    @parse_group
+    def __init__(
+        self,
+        *,
+        nquantiles: int = 20,
+        kind: str = ADDITIVE,
+        group: Union[str, Grouper] = "time",
+        norm_window: int = 1,
+    ):
+        super().__init__(
+            nquantiles=nquantiles, kind=kind, group=group,
+        )
+        self["norm_group"] = Grouper("time.dayofyear", window=norm_window)
+
     def _train(self, ref, hist):
+        refn = normalize(ref, group=self.norm_group, kind=self.kind)
+        histn = normalize(hist, group=self.norm_group, kind=self.kind)
+        super()._train(refn, histn)
+
         mu_ref = self.group.apply("mean", ref)
         mu_hist = self.group.apply("mean", hist)
-        ref = normalize(ref, group=self.group, kind=self.kind)
-        hist = normalize(hist, group=self.group, kind=self.kind)
-        super()._train(ref, hist)
-
         self.ds["scaling"] = get_correction(mu_hist, mu_ref, kind=self.kind)
         self.ds.scaling.attrs.update(
             standard_name="Scaling factor",
             description="Scaling factor making the mean of hist match the one of hist.",
         )

-    def _adjust(self, sim, interp="nearest", extrapolation="constant", detrend=1):
+    def _adjust(
+        self,
+        sim,
+        interp="nearest",
+        extrapolation="constant",
+        detrend=1,
+        normalize_sim=False,
+    ):
+
         # Apply preliminary scaling from obs to hist
         sim = apply_correction(
             sim,
@@ -283,9 +315,17 @@ def _adjust(self, sim, interp="nearest", extrapolation="constant", detrend=1):
             self.kind,
         )

+        if normalize_sim:
+            sim_norm = self.norm_group.apply("mean", sim)
+            sim = normalize(sim, group=self.norm_group, kind=self.kind, norm=sim_norm)
+
         # Find trend on sim
         if isinstance(detrend, int):
-            detrend = PolyDetrend(degree=detrend, kind=self.kind, group=self.group)
+            detrend = PolyDetrend(
+                degree=detrend,
+                kind=self.kind,
+                group=self.group if normalize_sim else self.norm_group,
+            )

         sim_fit = detrend.fit(sim)
         sim_detrended = sim_fit.detrend(sim)
@@ -295,7 +335,15 @@ def _adjust(self, sim, interp="nearest", extrapolation="constant", detrend=1):
             sim_detrended, extrapolation=extrapolation, interp=interp
         )
         # Retrend
-        return sim_fit.retrend(scen_detrended)
+        scen = sim_fit.retrend(scen_detrended)
+
+        if normalize_sim:
+            return apply_correction(
+                scen,
+                broadcast(sim_norm, scen, group=self.norm_group, interp="nearest"),
+                self.kind,
+            )
+        return scen

 class QuantileDeltaMapping(EmpiricalQuantileMapping):
diff --git a/xclim/sdba/base.py b/xclim/sdba/base.py
index 7eaef29eb..a0222136d 100644
--- a/xclim/sdba/base.py
+++ b/xclim/sdba/base.py
@@ -170,9 +170,9 @@ def get_index(
         ind = da.indexes[self.dim]
         i = getattr(ind, self.prop)

-        if i.dtype != np.int:
+        if not np.issubdtype(i.dtype, np.integer):
             raise ValueError(
-                f"Index {self.name} is not of type int (rather {ind.dtype}), but {self.__class__.__name__} requires integer indexes."
+                f"Index {self.name} is not of type int (rather {i.dtype}), but {self.__class__.__name__} requires integer indexes."
             )

         interp = (
diff --git a/xclim/sdba/detrending.py b/xclim/sdba/detrending.py
index cce24308a..20cc22790 100644
--- a/xclim/sdba/detrending.py
+++ b/xclim/sdba/detrending.py
@@ -52,16 +52,23 @@ def fit(self, da: xr.DataArray):
         Returns a new object storing the fit data that can be used for detrending and retrending.
         """
         new = self.copy()
-        new._set_fitds(new.group.apply(new._fit, da, main_only=True))
+        new._set_ds(new.group.apply(new._fit, da, main_only=True))
         new.__fitted = True
         return new

     def get_trend(self, da: xr.DataArray):
-        return self.group.apply(
+        """Get the trend computed from the fit, the fitting dim as found on da.
+
+        If da is a DataArray (and has a "dtype" attribute), the trend is casted to have the same dtype.
+        """
+        out = self.group.apply(
             self._get_trend,
-            {self.group.dim: da[self.group.dim], **self.fit_ds.data_vars},
+            {self.group.dim: da[self.group.dim], **self.ds.data_vars},
             main_only=True,
         )
+        if hasattr(da, "dtype"):
+            out = out.astype(da.dtype)
+        return out

     def detrend(self, da: xr.DataArray):
         """Removes the previously fitted trend from a DataArray."""
@@ -77,8 +84,9 @@ def retrend(self, da: xr.DataArray):
         trend = self.get_trend(da)
         return self._retrend(da, trend)

-    def _set_fitds(self, ds):
-        self.fit_ds = ds
+    def _set_ds(self, ds):
+        self.ds = ds
+        self.ds.attrs["fit_params"] = str(self)

     def _detrend(self, da, trend):
         # Remove trend from series
diff --git a/xclim/sdba/processing.py b/xclim/sdba/processing.py
index e2239eff5..0b25875d7 100644
--- a/xclim/sdba/processing.py
+++ b/xclim/sdba/processing.py
@@ -1,4 +1,5 @@
 """Pre and post processing for bias adjustement"""
+from typing import Optional
 from typing import Union

 import dask.array as dsk
@@ -9,6 +10,7 @@
 from .base import parse_group
 from .utils import ADDITIVE
 from .utils import apply_correction
+from .utils import broadcast
 from .utils import ecdf
 from .utils import invert

@@ -167,15 +169,24 @@ def jitter_under_thresh(x: xr.DataArray, thresh: float):
     If thresh is high, this will change the mean value of x.
     """
     epsilon = np.finfo(x.dtype).eps
-    jitter = np.random.uniform(low=epsilon, high=thresh, size=x.shape)
+    if isinstance(x.data, dsk.Array):
+        jitter = dsk.random.uniform(
+            low=epsilon, high=thresh, size=x.shape, chunks=x.chunks
+        )
+    else:
+        jitter = np.random.uniform(low=epsilon, high=thresh, size=x.shape)
     return x.where(~((x < thresh) & (x.notnull())), jitter)

 @parse_group
 def normalize(
-    x: xr.DataArray, *, group: Union[str, Grouper] = "time", kind: str = ADDITIVE
+    x: xr.DataArray,
+    *,
+    group: Union[str, Grouper] = "time",
+    kind: str = ADDITIVE,
+    norm: Optional[xr.DataArray] = None,
 ):
-    """Normalize an array by remove its mean.
+    """Normalize an array by removing its mean.

     Normalization if performed group-wise.

@@ -187,9 +198,21 @@ def normalize(
       Grouping information. See :py:class:`xclim.sdba.base.Grouper` for details.
     kind : {'+', '*'}
       How to apply the adjustment, either additively or multiplicatively.
+    norm : xr.DataArray
+      If the norm was already computed (for example with `group.apply("mean", x)`), skip the
+        computation step. The array should have the same dimensions as `x` except for "time" that should
+        be replaced by `group.prop`.
+
+    Returns
+    -------
+    xr.DataArray or xr.Dataset
+      Group-wise anomaly of x
     """

     def _normalize_group(grp, dim=["time"]):
         return apply_correction(grp, invert(grp.mean(dim=dim), kind), kind)

-    return group.apply(_normalize_group, x)
+    if norm is None:
+        return group.apply(_normalize_group, x)
+
+    return apply_correction(x, broadcast(norm, x, group=group, interp="nearest"), kind,)
diff --git a/xclim/sdba/utils.py b/xclim/sdba/utils.py
index 24e51e1ee..4911ee8a5 100644
--- a/xclim/sdba/utils.py
+++ b/xclim/sdba/utils.py
@@ -456,7 +456,7 @@ def _interp_quantiles_2D(newx, newg, oldx, oldy, oldg):
         output_core_dims=[[dim]],
         vectorize=True,
         dask="parallelized",
-        output_dtypes=[np.float],
+        output_dtypes=[yq.dtype],
     )

diff --git a/tests/test_sdba/test_adjustment.py b/tests/test_sdba/test_adjustment.py
index c897ea3ca..532f8a67f 100644
--- a/tests/test_sdba/test_adjustment.py
+++ b/tests/test_sdba/test_adjustment.py
@@ -190,7 +190,7 @@ def test_mon_U(self, mon_series, series, mon_triangular, kind, name, spatial_dim
         if spatial_dims:
             mqm = mqm.isel({crd: 0 for crd in spatial_dims.keys()})
         np.testing.assert_array_almost_equal(mqm, int(kind == MULTIPLICATIVE), 1)
-        np.testing.assert_array_almost_equal(p, ref_t, 1)
+        np.testing.assert_allclose(p, ref_t, rtol=0.1, atol=0.5)

     def test_cannon(self, cannon_2015_rvs):
         ref, hist, sim = cannon_2015_rvs(15000)
@@ -202,17 +202,6 @@ def test_cannon(self, cannon_2015_rvs):
         np.testing.assert_almost_equal(p.mean(), 41.6, 0)
         np.testing.assert_almost_equal(p.std(), 15.0, 0)

-    def test_group_norm(self, tas_series):
-        x = np.arange(365)
-        ref = tas_series(273 - 10 * np.cos(2 * np.pi * x / 365), start="2001-01-01")
-        hist = sim = tas_series(
-            273 - 8 * np.cos(2 * np.pi * x / 365), start="2001-01-01"
-        )
-        DQM = DetrendedQuantileMapping(group="time.month")
-        DQM.train(ref, hist)
-        scen = DQM.adjust(sim, interp="linear")
-        xr.testing.assert_allclose(ref, scen, rtol=1e-3)
-

 class TestQDM:
     @pytest.mark.parametrize("kind,name", [(ADDITIVE, "tas"), (MULTIPLICATIVE, "pr")])

sdba - improve the speed of DetrendedQuantileMapping
### Description
Doing a few preliminary tests **on a single pixel and without dask**, I obtained the following times :

DetrendedQuantileMapping, groups='month', 150 years of daily data : ~15 seconds
DetrendedQuantileMapping, groups='day', window=31, 150 years of daily data : ~24 seconds

EmpiricalQuantileMapping, groups='month', 150 years of daily data : ~0.35 seconds
EmpiricalQuantileMapping, groups='day', window=31, 150 years of daily data : ~2.5 seconds

There seems to be a bottleneck in the detrending version. Discussions with @aulemahal point towards the normalization process and `sdba.utils.interp_on_quantiles`. I will continue investigating and post my findings here.

To clear out my thoughts:
- The normalization process, as all non-grouping grouped operations, has additional overhead because it needs to resort the data along the time axis. And to rechunk if dask is used.
- `interp_on_quantiles` currently calls griddata through a _vectorized_  `xr.apply_ufunc`, which is far from optimal. Vectorization is a siimple for-loop, and `apply_ufunc` forbids chunks along the main dim (here 'time'). Getting rid of both would be a great improvement.
- Also, because of #442, DQM has 2 extra operations that would disappear were that issue to be fixed.

To compare, when calling train+adjust, here is the number of "operations" that the data goes through:

- EQM: 3 (quantile / correction / interpolation)
- DQM: 11 (2x normalization, 3x correction, detrending (fit + correction), retrending + EQM) (Fixing 442, would remove 1 normalization and 1 correction)

So we expect a longer computation time, but maybe not that much.
The issue seems smaller than I previously thought. While it does take a long time to bias correct a single pixel, the script seems very scalable.

So while it can take as long as 70 seconds to bias correct a single pixel and save it to NetCDF, I managed to bias correct 14 375 pixels, 1/3rd of Canada, in 441.52 seconds (0.0307 seconds per pixel).

This is still something that could be explored for a future enhancement, but this is not a priority.

Edit: To clarify, those numbers are with the monthly grouping.


diff --git a/doc/modules/classes.rst b/doc/modules/classes.rst
index 68494051041be..99731c7fda599 100644
--- a/doc/modules/classes.rst
+++ b/doc/modules/classes.rst
@@ -955,6 +955,7 @@ See the :ref:`metrics` section of the user guide for further details.
    metrics.pairwise_distances
    metrics.pairwise_distances_argmin
    metrics.pairwise_distances_argmin_min
+   metrics.pairwise_distances_chunked

 .. _mixture_ref:
diff --git a/doc/modules/computational_performance.rst b/doc/modules/computational_performance.rst
index d66cba212a2dd..ca128c515fb90 100644
--- a/doc/modules/computational_performance.rst
+++ b/doc/modules/computational_performance.rst
@@ -308,6 +308,27 @@ Debian / Ubuntu.
     or upgrade to Python 3.4 which has a new version of ``multiprocessing``
     that should be immune to this problem.

+.. _working_memory:
+
+Limiting Working Memory
+-----------------------
+
+Some calculations when implemented using standard numpy vectorized operations
+involve using a large amount of temporary memory.  This may potentially exhaust
+system memory.  Where computations can be performed in fixed-memory chunks, we
+attempt to do so, and allow the user to hint at the maximum size of this
+working memory (defaulting to 1GB) using :func:`sklearn.set_config` or
+:func:`config_context`.  The following suggests to limit temporary working
+memory to 128 MiB::
+
+  >>> import sklearn
+  >>> with sklearn.config_context(working_memory=128):
+  ...    pass  # do chunked work here
+
+An example of a chunked operation adhering to this setting is
+:func:`metric.pairwise_distances_chunked`, which facilitates computing
+row-wise reductions of a pairwise distance matrix.
+
 Model Compression
 -----------------

diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst
index 13de05ada8369..d10400442b387 100644
--- a/doc/whats_new/v0.20.rst
+++ b/doc/whats_new/v0.20.rst
@@ -56,6 +56,9 @@ Classifiers and regressors
 - :class:`dummy.DummyRegressor` now has a ``return_std`` option in its
   ``predict`` method. The returned standard deviations will be zeros.

+- Added :class:`multioutput.RegressorChain` for multi-target
+  regression. :issue:`9257` by :user:`Kumar Ashutosh <thechargedneutron>`.
+
 - Added :class:`naive_bayes.ComplementNB`, which implements the Complement
   Naive Bayes classifier described in Rennie et al. (2003).
   :issue:`8190` by :user:`Michael A. Alcorn <airalcorn2>`.
@@ -115,6 +118,13 @@ Metrics
   :func:`metrics.roc_auc_score`. :issue:`3273` by
   :user:`Alexander Niederbühl <Alexander-N>`.

+Misc
+
+- A new configuration parameter, ``working_memory`` was added to control memory
+  consumption limits in chunked operations, such as the new
+  :func:`metrics.pairwise_distances_chunked`.  See :ref:`working_memory`.
+  :issue:`10280` by `Joel Nothman`_ and :user:`Aman Dalmia <dalmia>`.
+
 Enhancements
 ............

@@ -521,6 +531,12 @@ Metrics
   due to floating point error in the input.
   :issue:`9851` by :user:`Hanmin Qin <qinhanmin2014>`.

+- The ``batch_size`` parameter to :func:`metrics.pairwise_distances_argmin_min`
+  and :func:`metrics.pairwise_distances_argmin` is deprecated to be removed in
+  v0.22.  It no longer has any effect, as batch size is determined by global
+  ``working_memory`` config. See :ref:`working_memory`. :issue:`10280` by `Joel
+  Nothman`_ and :user:`Aman Dalmia <dalmia>`.
+
 Cluster

 - Deprecate ``pooling_func`` unused parameter in
diff --git a/sklearn/_config.py b/sklearn/_config.py
index 1ad53c9a22527..2b8a2e795bf86 100644
--- a/sklearn/_config.py
+++ b/sklearn/_config.py
@@ -5,6 +5,7 @@

 _global_config = {
     'assume_finite': bool(os.environ.get('SKLEARN_ASSUME_FINITE', False)),
+    'working_memory': int(os.environ.get('SKLEARN_WORKING_MEMORY', 1024))
 }

@@ -19,7 +20,7 @@ def get_config():
     return _global_config.copy()

-def set_config(assume_finite=None):
+def set_config(assume_finite=None, working_memory=None):
     """Set global scikit-learn configuration

     Parameters
@@ -29,9 +30,17 @@ def set_config(assume_finite=None):
         saving time, but leading to potential crashes. If
         False, validation for finiteness will be performed,
         avoiding error.  Global default: False.
+
+    working_memory : int, optional
+        If set, scikit-learn will attempt to limit the size of temporary arrays
+        to this number of MiB (per job when parallelised), often saving both
+        computation time and memory on expensive operations that can be
+        performed in chunks. Global default: 1024.
     """
     if assume_finite is not None:
         _global_config['assume_finite'] = assume_finite
+    if working_memory is not None:
+        _global_config['working_memory'] = working_memory

 @contextmanager
@@ -46,6 +55,12 @@ def config_context(**new_config):
         False, validation for finiteness will be performed,
         avoiding error.  Global default: False.

+    working_memory : int, optional
+        If set, scikit-learn will attempt to limit the size of temporary arrays
+        to this number of MiB (per job when parallelised), often saving both
+        computation time and memory on expensive operations that can be
+        performed in chunks. Global default: 1024.
+
     Notes
     -----
     All settings, not just those presently modified, will be returned to
diff --git a/sklearn/metrics/__init__.py b/sklearn/metrics/__init__.py
index c98b0e14493c6..846634e7afec4 100644
--- a/sklearn/metrics/__init__.py
+++ b/sklearn/metrics/__init__.py
@@ -51,6 +51,7 @@
 from .pairwise import pairwise_distances_argmin
 from .pairwise import pairwise_distances_argmin_min
 from .pairwise import pairwise_kernels
+from .pairwise import pairwise_distances_chunked

 from .regression import explained_variance_score
 from .regression import mean_absolute_error
@@ -106,6 +107,7 @@
     'pairwise_distances_argmin',
     'pairwise_distances_argmin_min',
     'pairwise_distances_argmin_min',
+    'pairwise_distances_chunked',
     'pairwise_kernels',
     'precision_recall_curve',
     'precision_recall_fscore_support',
diff --git a/sklearn/metrics/pairwise.py b/sklearn/metrics/pairwise.py
index 58bd3f1627f22..b4928ed7492f3 100644
--- a/sklearn/metrics/pairwise.py
+++ b/sklearn/metrics/pairwise.py
@@ -18,9 +18,10 @@
 from scipy.sparse import csr_matrix
 from scipy.sparse import issparse

+from ..utils.validation import _num_samples
 from ..utils import check_array
 from ..utils import gen_even_slices
-from ..utils import gen_batches
+from ..utils import gen_batches, get_chunk_n_rows
 from ..utils.extmath import row_norms, safe_sparse_dot
 from ..preprocessing import normalize
 from ..externals.joblib import Parallel
@@ -257,8 +258,14 @@ def euclidean_distances(X, Y=None, Y_norm_squared=None, squared=False,
     return distances if squared else np.sqrt(distances, out=distances)

+def _argmin_min_reduce(dist, start):
+    indices = dist.argmin(axis=1)
+    values = dist[np.arange(dist.shape[0]), indices]
+    return indices, values
+
+
 def pairwise_distances_argmin_min(X, Y, axis=1, metric="euclidean",
-                                  batch_size=500, metric_kwargs=None):
+                                  batch_size=None, metric_kwargs=None):
     """Compute minimum distances between one point and a set of points.

     This function computes for each row in X, the index of the row of Y which
@@ -310,11 +317,9 @@ def pairwise_distances_argmin_min(X, Y, axis=1, metric="euclidean",
         metrics.

     batch_size : integer
-        To reduce memory consumption over the naive solution, data are
-        processed in batches, comprising batch_size rows of X and
-        batch_size rows of Y. The default value is quite conservative, but
-        can be changed for fine-tuning. The larger the number, the larger the
-        memory usage.
+        .. deprecated:: 0.20
+            Deprecated for removal in 0.22.
+            Use sklearn.set_config(working_memory=...) instead.

     metric_kwargs : dict, optional
         Keyword arguments to pass to specified metric function.
@@ -333,12 +338,11 @@ def pairwise_distances_argmin_min(X, Y, axis=1, metric="euclidean",
     sklearn.metrics.pairwise_distances
     sklearn.metrics.pairwise_distances_argmin
     """
-    dist_func = None
-    if metric in PAIRWISE_DISTANCE_FUNCTIONS:
-        dist_func = PAIRWISE_DISTANCE_FUNCTIONS[metric]
-    elif not callable(metric) and not isinstance(metric, str):
-        raise ValueError("'metric' must be a string or a callable")
-
+    if batch_size is not None:
+        warnings.warn("'batch_size' is ignored. It was deprecated in version "
+                      "0.20 and will be removed in version 0.22. "
+                      "Use sklearn.set_config(working_memory=...) instead.",
+                      DeprecationWarning)
     X, Y = check_pairwise_arrays(X, Y)

     if metric_kwargs is None:
@@ -347,39 +351,11 @@ def pairwise_distances_argmin_min(X, Y, axis=1, metric="euclidean",
     if axis == 0:
         X, Y = Y, X

-    # Allocate output arrays
-    indices = np.empty(X.shape[0], dtype=np.intp)
-    values = np.empty(X.shape[0])
-    values.fill(np.infty)
-
-    for chunk_x in gen_batches(X.shape[0], batch_size):
-        X_chunk = X[chunk_x, :]
-
-        for chunk_y in gen_batches(Y.shape[0], batch_size):
-            Y_chunk = Y[chunk_y, :]
-
-            if dist_func is not None:
-                if metric == 'euclidean':  # special case, for speed
-                    d_chunk = safe_sparse_dot(X_chunk, Y_chunk.T,
-                                              dense_output=True)
-                    d_chunk *= -2
-                    d_chunk += row_norms(X_chunk, squared=True)[:, np.newaxis]
-                    d_chunk += row_norms(Y_chunk, squared=True)[np.newaxis, :]
-                    np.maximum(d_chunk, 0, d_chunk)
-                else:
-                    d_chunk = dist_func(X_chunk, Y_chunk, **metric_kwargs)
-            else:
-                d_chunk = pairwise_distances(X_chunk, Y_chunk,
-                                             metric=metric, **metric_kwargs)
-
-            # Update indices and minimum values using chunk
-            min_indices = d_chunk.argmin(axis=1)
-            min_values = d_chunk[np.arange(chunk_x.stop - chunk_x.start),
-                                 min_indices]
-
-            flags = values[chunk_x] > min_values
-            indices[chunk_x][flags] = min_indices[flags] + chunk_y.start
-            values[chunk_x][flags] = min_values[flags]
+    indices, values = zip(*pairwise_distances_chunked(
+        X, Y, reduce_func=_argmin_min_reduce, metric=metric,
+        **metric_kwargs))
+    indices = np.concatenate(indices)
+    values = np.concatenate(values)

     if metric == "euclidean" and not metric_kwargs.get("squared", False):
         np.sqrt(values, values)
@@ -387,7 +363,7 @@ def pairwise_distances_argmin_min(X, Y, axis=1, metric="euclidean",

 def pairwise_distances_argmin(X, Y, axis=1, metric="euclidean",
-                              batch_size=500, metric_kwargs=None):
+                              batch_size=None, metric_kwargs=None):
     """Compute minimum distances between one point and a set of points.

     This function computes for each row in X, the index of the row of Y which
@@ -441,11 +417,9 @@ def pairwise_distances_argmin(X, Y, axis=1, metric="euclidean",
         metrics.

     batch_size : integer
-        To reduce memory consumption over the naive solution, data are
-        processed in batches, comprising batch_size rows of X and
-        batch_size rows of Y. The default value is quite conservative, but
-        can be changed for fine-tuning. The larger the number, the larger the
-        memory usage.
+        .. deprecated:: 0.20
+            Deprecated for removal in 0.22.
+            Use sklearn.set_config(working_memory=...) instead.

     metric_kwargs : dict
         keyword arguments to pass to specified metric function.
@@ -463,8 +437,9 @@ def pairwise_distances_argmin(X, Y, axis=1, metric="euclidean",
     if metric_kwargs is None:
         metric_kwargs = {}

-    return pairwise_distances_argmin_min(X, Y, axis, metric, batch_size,
-                                         metric_kwargs)[0]
+    return pairwise_distances_argmin_min(X, Y, axis, metric,
+                                         metric_kwargs=metric_kwargs,
+                                         batch_size=batch_size)[0]

 def manhattan_distances(X, Y=None, sum_over_features=True,
@@ -928,7 +903,8 @@ def cosine_similarity(X, Y=None, dense_output=True):
     else:
         Y_normalized = normalize(Y, copy=True)

-    K = safe_sparse_dot(X_normalized, Y_normalized.T, dense_output=dense_output)
+    K = safe_sparse_dot(X_normalized, Y_normalized.T,
+                        dense_output=dense_output)

     return K

@@ -1144,6 +1120,177 @@ def _pairwise_callable(X, Y, metric, **kwds):
                   'sokalsneath', 'sqeuclidean', 'yule', "wminkowski"]

+def _check_chunk_size(reduced, chunk_size):
+    """Checks chunk is a sequence of expected size or a tuple of same
+    """
+    is_tuple = isinstance(reduced, tuple)
+    if not is_tuple:
+        reduced = (reduced,)
+    if any(isinstance(r, tuple) or not hasattr(r, '__iter__')
+           for r in reduced):
+        raise TypeError('reduce_func returned %r. '
+                        'Expected sequence(s) of length %d.' %
+                        (reduced if is_tuple else reduced[0], chunk_size))
+    if any(_num_samples(r) != chunk_size for r in reduced):
+        # XXX: we use int(_num_samples...) because sometimes _num_samples
+        #      returns a long in Python 2, even for small numbers.
+        actual_size = tuple(int(_num_samples(r)) for r in reduced)
+        raise ValueError('reduce_func returned object of length %s. '
+                         'Expected same length as input: %d.' %
+                         (actual_size if is_tuple else actual_size[0],
+                          chunk_size))
+
+
+def pairwise_distances_chunked(X, Y=None, reduce_func=None,
+                               metric='euclidean', n_jobs=1,
+                               working_memory=None, **kwds):
+    """Generate a distance matrix chunk by chunk with optional reduction
+
+    In cases where not all of a pairwise distance matrix needs to be stored at
+    once, this is used to calculate pairwise distances in
+    ``working_memory``-sized chunks.  If ``reduce_func`` is given, it is run
+    on each chunk and its return values are concatenated into lists, arrays
+    or sparse matrices.
+
+    Parameters
+    ----------
+    X : array [n_samples_a, n_samples_a] if metric == "precomputed", or,
+        [n_samples_a, n_features] otherwise
+        Array of pairwise distances between samples, or a feature array.
+
+    Y : array [n_samples_b, n_features], optional
+        An optional second feature array. Only allowed if
+        metric != "precomputed".
+
+    reduce_func : callable, optional
+        The function which is applied on each chunk of the distance matrix,
+        reducing it to needed values.  ``reduce_func(D_chunk, start)``
+        is called repeatedly, where ``D_chunk`` is a contiguous vertical
+        slice of the pairwise distance matrix, starting at row ``start``.
+        It should return an array, a list, or a sparse matrix of length
+        ``D_chunk.shape[0]``, or a tuple of such objects.
+
+        If None, pairwise_distances_chunked returns a generator of vertical
+        chunks of the distance matrix.
+
+    metric : string, or callable
+        The metric to use when calculating distance between instances in a
+        feature array. If metric is a string, it must be one of the options
+        allowed by scipy.spatial.distance.pdist for its metric parameter, or
+        a metric listed in pairwise.PAIRWISE_DISTANCE_FUNCTIONS.
+        If metric is "precomputed", X is assumed to be a distance matrix.
+        Alternatively, if metric is a callable function, it is called on each
+        pair of instances (rows) and the resulting value recorded. The callable
+        should take two arrays from X as input and return a value indicating
+        the distance between them.
+
+    n_jobs : int
+        The number of jobs to use for the computation. This works by breaking
+        down the pairwise matrix into n_jobs even slices and computing them in
+        parallel.
+
+        If -1 all CPUs are used. If 1 is given, no parallel computing code is
+        used at all, which is useful for debugging. For n_jobs below -1,
+        (n_cpus + 1 + n_jobs) are used. Thus for n_jobs = -2, all CPUs but one
+        are used.
+
+    working_memory : int, optional
+        The sought maximum memory for temporary distance matrix chunks.
+        When None (default), the value of
+        ``sklearn.get_config()['working_memory']`` is used.
+
+    `**kwds` : optional keyword parameters
+        Any further parameters are passed directly to the distance function.
+        If using a scipy.spatial.distance metric, the parameters are still
+        metric dependent. See the scipy docs for usage examples.
+
+    Yields
+    ------
+    D_chunk : array or sparse matrix
+        A contiguous slice of distance matrix, optionally processed by
+        ``reduce_func``.
+
+    Examples
+    --------
+    Without reduce_func:
+
+    >>> X = np.random.RandomState(0).rand(5, 3)
+    >>> D_chunk = next(pairwise_distances_chunked(X))
+    >>> D_chunk  # doctest: +ELLIPSIS
+    array([[0.  ..., 0.29..., 0.41..., 0.19..., 0.57...],
+           [0.29..., 0.  ..., 0.57..., 0.41..., 0.76...],
+           [0.41..., 0.57..., 0.  ..., 0.44..., 0.90...],
+           [0.19..., 0.41..., 0.44..., 0.  ..., 0.51...],
+           [0.57..., 0.76..., 0.90..., 0.51..., 0.  ...]])
+
+    Retrieve all neighbors and average distance within radius r:
+
+    >>> r = .2
+    >>> def reduce_func(D_chunk, start):
+    ...     neigh = [np.flatnonzero(d < r) for d in D_chunk]
+    ...     avg_dist = (D_chunk * (D_chunk < r)).mean(axis=1)
+    ...     return neigh, avg_dist
+    >>> gen = pairwise_distances_chunked(X, reduce_func=reduce_func)
+    >>> neigh, avg_dist = next(gen)
+    >>> neigh
+    [array([0, 3]), array([1]), array([2]), array([0, 3]), array([4])]
+    >>> avg_dist  # doctest: +ELLIPSIS
+    array([0.039..., 0.        , 0.        , 0.039..., 0.        ])
+
+    Where r is defined per sample, we need to make use of ``start``:
+
+    >>> r = [.2, .4, .4, .3, .1]
+    >>> def reduce_func(D_chunk, start):
+    ...     neigh = [np.flatnonzero(d < r[i])
+    ...              for i, d in enumerate(D_chunk, start)]
+    ...     return neigh
+    >>> neigh = next(pairwise_distances_chunked(X, reduce_func=reduce_func))
+    >>> neigh
+    [array([0, 3]), array([0, 1]), array([2]), array([0, 3]), array([4])]
+
+    Force row-by-row generation by reducing ``working_memory``:
+
+    >>> gen = pairwise_distances_chunked(X, reduce_func=reduce_func,
+    ...                                  working_memory=0)
+    >>> next(gen)
+    [array([0, 3])]
+    >>> next(gen)
+    [array([0, 1])]
+    """
+    n_samples_X = _num_samples(X)
+    if metric == 'precomputed':
+        slices = (slice(0, n_samples_X),)
+    else:
+        if Y is None:
+            Y = X
+        # We get as many rows as possible within our working_memory budget to
+        # store len(Y) distances in each row of output.
+        #
+        # Note:
+        #  - this will get at least 1 row, even if 1 row of distances will
+        #    exceed working_memory.
+        #  - this does not account for any temporary memory usage while
+        #    calculating distances (e.g. difference of vectors in manhattan
+        #    distance.
+        chunk_n_rows = get_chunk_n_rows(row_bytes=8 * _num_samples(Y),
+                                        max_n_rows=n_samples_X,
+                                        working_memory=working_memory)
+        slices = gen_batches(n_samples_X, chunk_n_rows)
+
+    for sl in slices:
+        if sl.start == 0 and sl.stop == n_samples_X:
+            X_chunk = X  # enable optimised paths for X is Y
+        else:
+            X_chunk = X[sl]
+        D_chunk = pairwise_distances(X_chunk, Y, metric=metric,
+                                     n_jobs=n_jobs, **kwds)
+        if reduce_func is not None:
+            chunk_size = D_chunk.shape[0]
+            D_chunk = reduce_func(D_chunk, sl.start)
+            _check_chunk_size(D_chunk, chunk_size)
+        yield D_chunk
+
+
 def pairwise_distances(X, Y=None, metric="euclidean", n_jobs=1, **kwds):
     """ Compute the distance matrix from a vector array X and optional Y.

@@ -1186,7 +1333,8 @@ def pairwise_distances(X, Y=None, metric="euclidean", n_jobs=1, **kwds):
         Array of pairwise distances between samples, or a feature array.

     Y : array [n_samples_b, n_features], optional
-        An optional second feature array. Only allowed if metric != "precomputed".
+        An optional second feature array. Only allowed if
+        metric != "precomputed".

     metric : string, or callable
         The metric to use when calculating distance between instances in a
@@ -1224,6 +1372,9 @@ def pairwise_distances(X, Y=None, metric="euclidean", n_jobs=1, **kwds):

     See also
     --------
+    pairwise_distances_chunked : performs the same calculation as this funtion,
+        but returns a generator of chunks of the distance matrix, in order to
+        limit memory usage.
     paired_distances : Computes the distances between corresponding
                        elements of two arrays
     """
diff --git a/sklearn/neighbors/classification.py b/sklearn/neighbors/classification.py
index ace8590b08157..83ee27cccd4b1 100644
--- a/sklearn/neighbors/classification.py
+++ b/sklearn/neighbors/classification.py
@@ -145,7 +145,6 @@ def predict(self, X):
         X = check_array(X, accept_sparse='csr')

         neigh_dist, neigh_ind = self.kneighbors(X)
-
         classes_ = self.classes_
         _y = self._y
         if not self.outputs_2d_:
diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py
index 21dcca4f0764e..e3d1e7faaabd1 100644
--- a/sklearn/utils/__init__.py
+++ b/sklearn/utils/__init__.py
@@ -17,7 +17,7 @@
 from ..externals.joblib import cpu_count
 from ..exceptions import DataConversionWarning
 from .deprecation import deprecated
-
+from .. import get_config

 __all__ = ["murmurhash3_32", "as_float_array",
            "assert_all_finite", "check_array",
@@ -514,3 +514,42 @@ def indices_to_mask(indices, mask_length):
     mask[indices] = True

     return mask
+
+
+def get_chunk_n_rows(row_bytes, max_n_rows=None,
+                     working_memory=None):
+    """Calculates how many rows can be processed within working_memory
+
+    Parameters
+    ----------
+    row_bytes : int
+        The expected number of bytes of memory that will be consumed
+        during the processing of each row.
+    max_n_rows : int, optional
+        The maximum return value.
+    working_memory : int or float, optional
+        The number of rows to fit inside this number of MiB will be returned.
+        When None (default), the value of
+        ``sklearn.get_config()['working_memory']`` is used.
+
+    Returns
+    -------
+    int or the value of n_samples
+
+    Warns
+    -----
+    Issues a UserWarning if ``row_bytes`` exceeds ``working_memory`` MiB.
+    """
+
+    if working_memory is None:
+        working_memory = get_config()['working_memory']
+
+    chunk_n_rows = int(working_memory * (2 ** 20) // row_bytes)
+    if max_n_rows is not None:
+        chunk_n_rows = min(chunk_n_rows, max_n_rows)
+    if chunk_n_rows < 1:
+        warnings.warn('Could not adhere to working_memory config. '
+                      'Currently %.0fMiB, %.0fMiB required.' %
+                      (working_memory, np.ceil(row_bytes * 2 ** -20)))
+        chunk_n_rows = 1
+    return chunk_n_rows

diff --git a/sklearn/metrics/tests/test_pairwise.py b/sklearn/metrics/tests/test_pairwise.py
index 799b3e4fe9bf7..0ef089c7a3619 100644
--- a/sklearn/metrics/tests/test_pairwise.py
+++ b/sklearn/metrics/tests/test_pairwise.py
@@ -1,11 +1,15 @@
+from types import GeneratorType
+
 import numpy as np
 from numpy import linalg
+import pytest

 from scipy.sparse import dok_matrix, csr_matrix, issparse
 from scipy.spatial.distance import cosine, cityblock, minkowski, wminkowski

 from sklearn.utils.testing import assert_greater
 from sklearn.utils.testing import assert_array_almost_equal
+from sklearn.utils.testing import assert_allclose
 from sklearn.utils.testing import assert_almost_equal
 from sklearn.utils.testing import assert_equal
 from sklearn.utils.testing import assert_array_equal
@@ -14,6 +18,7 @@
 from sklearn.utils.testing import assert_true
 from sklearn.utils.testing import assert_warns
 from sklearn.utils.testing import ignore_warnings
+from sklearn.utils.testing import assert_warns_message

 from sklearn.externals.six import iteritems

@@ -28,6 +33,7 @@
 from sklearn.metrics.pairwise import cosine_similarity
 from sklearn.metrics.pairwise import cosine_distances
 from sklearn.metrics.pairwise import pairwise_distances
+from sklearn.metrics.pairwise import pairwise_distances_chunked
 from sklearn.metrics.pairwise import pairwise_distances_argmin_min
 from sklearn.metrics.pairwise import pairwise_distances_argmin
 from sklearn.metrics.pairwise import pairwise_kernels
@@ -368,10 +374,128 @@ def test_pairwise_distances_argmin_min():
     dist_orig_val = dist[dist_orig_ind, range(len(dist_orig_ind))]

     dist_chunked_ind, dist_chunked_val = pairwise_distances_argmin_min(
-        X, Y, axis=0, metric="manhattan", batch_size=50)
+        X, Y, axis=0, metric="manhattan")
     np.testing.assert_almost_equal(dist_orig_ind, dist_chunked_ind, decimal=7)
     np.testing.assert_almost_equal(dist_orig_val, dist_chunked_val, decimal=7)

+    # Test batch_size deprecation warning
+    assert_warns_message(DeprecationWarning, "version 0.22",
+                         pairwise_distances_argmin_min, X, Y, batch_size=500,
+                         metric='euclidean')
+
+
+def _reduce_func(dist, start):
+    return dist[:, :100]
+
+
+def test_pairwise_distances_chunked_reduce():
+    rng = np.random.RandomState(0)
+    X = rng.random_sample((400, 4))
+    # Reduced Euclidean distance
+    S = pairwise_distances(X)[:, :100]
+    S_chunks = pairwise_distances_chunked(X, None, reduce_func=_reduce_func,
+                                          working_memory=2 ** -16)
+    assert isinstance(S_chunks, GeneratorType)
+    S_chunks = list(S_chunks)
+    assert len(S_chunks) > 1
+    # atol is for diagonal where S is explicitly zeroed on the diagonal
+    assert_allclose(np.vstack(S_chunks), S, atol=1e-7)
+
+
+@pytest.mark.parametrize('good_reduce', [
+    lambda D, start: list(D),
+    lambda D, start: np.array(D),
+    lambda D, start: csr_matrix(D),
+    lambda D, start: (list(D), list(D)),
+    lambda D, start: (dok_matrix(D), np.array(D), list(D)),
+    ])
+def test_pairwise_distances_chunked_reduce_valid(good_reduce):
+    X = np.arange(10).reshape(-1, 1)
+    S_chunks = pairwise_distances_chunked(X, None, reduce_func=good_reduce,
+                                          working_memory=64)
+    next(S_chunks)
+
+
+@pytest.mark.parametrize(('bad_reduce', 'err_type', 'message'), [
+    (lambda D, s: np.concatenate([D, D[-1:]]), ValueError,
+     r'length 11\..* input: 10\.'),
+    (lambda D, s: (D, np.concatenate([D, D[-1:]])), ValueError,
+     r'length \(10, 11\)\..* input: 10\.'),
+    (lambda D, s: (D[:9], D), ValueError,
+     r'length \(9, 10\)\..* input: 10\.'),
+    (lambda D, s: 7, TypeError,
+     r'returned 7\. Expected sequence\(s\) of length 10\.'),
+    (lambda D, s: (7, 8), TypeError,
+     r'returned \(7, 8\)\. Expected sequence\(s\) of length 10\.'),
+    (lambda D, s: (np.arange(10), 9), TypeError,
+     r', 9\)\. Expected sequence\(s\) of length 10\.'),
+])
+def test_pairwise_distances_chunked_reduce_invalid(bad_reduce, err_type,
+                                                   message):
+    X = np.arange(10).reshape(-1, 1)
+    S_chunks = pairwise_distances_chunked(X, None, reduce_func=bad_reduce,
+                                          working_memory=64)
+    assert_raises_regexp(err_type, message, next, S_chunks)
+
+
+def check_pairwise_distances_chunked(X, Y, working_memory, metric='euclidean'):
+    gen = pairwise_distances_chunked(X, Y, working_memory=working_memory,
+                                     metric=metric)
+    assert isinstance(gen, GeneratorType)
+    blockwise_distances = list(gen)
+    Y = np.array(X if Y is None else Y)
+    min_block_mib = len(Y) * 8 * 2 ** -20
+
+    for block in blockwise_distances:
+        memory_used = block.nbytes
+        assert memory_used <= max(working_memory, min_block_mib) * 2 ** 20
+
+    blockwise_distances = np.vstack(blockwise_distances)
+    S = pairwise_distances(X, Y, metric=metric)
+    assert_array_almost_equal(blockwise_distances, S)
+
+
+@ignore_warnings
+def test_pairwise_distances_chunked():
+    # Test the pairwise_distance helper function.
+    rng = np.random.RandomState(0)
+    # Euclidean distance should be equivalent to calling the function.
+    X = rng.random_sample((400, 4))
+    check_pairwise_distances_chunked(X, None, working_memory=1,
+                                     metric='euclidean')
+    # Test small amounts of memory
+    for power in range(-16, 0):
+        check_pairwise_distances_chunked(X, None, working_memory=2 ** power,
+                                         metric='euclidean')
+    # X as list
+    check_pairwise_distances_chunked(X.tolist(), None, working_memory=1,
+                                     metric='euclidean')
+    # Euclidean distance, with Y != X.
+    Y = rng.random_sample((200, 4))
+    check_pairwise_distances_chunked(X, Y, working_memory=1,
+                                     metric='euclidean')
+    check_pairwise_distances_chunked(X.tolist(), Y.tolist(), working_memory=1,
+                                     metric='euclidean')
+    # absurdly large working_memory
+    check_pairwise_distances_chunked(X, Y, working_memory=10000,
+                                     metric='euclidean')
+    # "cityblock" uses scikit-learn metric, cityblock (function) is
+    # scipy.spatial.
+    check_pairwise_distances_chunked(X, Y, working_memory=1,
+                                     metric='cityblock')
+    # Test that a value error is raised if the metric is unknown
+    assert_raises(ValueError, next,
+                  pairwise_distances_chunked(X, Y, metric="blah"))
+
+    # Test precomputed returns all at once
+    D = pairwise_distances(X)
+    gen = pairwise_distances_chunked(D,
+                                     working_memory=2 ** -16,
+                                     metric='precomputed')
+    assert isinstance(gen, GeneratorType)
+    assert next(gen) is D
+    assert_raises(StopIteration, next, gen)
+

 def test_euclidean_distances():
     # Check the pairwise Euclidean distances computation
diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py
index 08731b3c06e94..efaa57f850367 100644
--- a/sklearn/tests/test_config.py
+++ b/sklearn/tests/test_config.py
@@ -3,14 +3,14 @@

 def test_config_context():
-    assert get_config() == {'assume_finite': False}
+    assert get_config() == {'assume_finite': False, 'working_memory': 1024}

     # Not using as a context manager affects nothing
     config_context(assume_finite=True)
     assert get_config()['assume_finite'] is False

     with config_context(assume_finite=True):
-        assert get_config() == {'assume_finite': True}
+        assert get_config() == {'assume_finite': True, 'working_memory': 1024}
     assert get_config()['assume_finite'] is False

     with config_context(assume_finite=True):
@@ -34,7 +34,7 @@ def test_config_context():

         assert get_config()['assume_finite'] is True

-    assert get_config() == {'assume_finite': False}
+    assert get_config() == {'assume_finite': False, 'working_memory': 1024}

     # No positional arguments
     assert_raises(TypeError, config_context, True)
diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py
index fa93bf34fe6bc..1f1efed825c80 100644
--- a/sklearn/utils/tests/test_utils.py
+++ b/sklearn/utils/tests/test_utils.py
@@ -1,6 +1,7 @@
 from itertools import chain, product
 import warnings

+import pytest
 import numpy as np
 import scipy.sparse as sp
 from scipy.linalg import pinv2
@@ -9,7 +10,8 @@
 from sklearn.utils.testing import (assert_equal, assert_raises, assert_true,
                                    assert_almost_equal, assert_array_equal,
                                    SkipTest, assert_raises_regex,
-                                   assert_greater_equal, ignore_warnings)
+                                   assert_greater_equal, ignore_warnings,
+                                   assert_warns_message, assert_no_warnings)
 from sklearn.utils import check_random_state
 from sklearn.utils import deprecated
 from sklearn.utils import resample
@@ -18,9 +20,11 @@
 from sklearn.utils import safe_indexing
 from sklearn.utils import shuffle
 from sklearn.utils import gen_even_slices
+from sklearn.utils import get_chunk_n_rows
 from sklearn.utils.extmath import pinvh
 from sklearn.utils.arpack import eigsh
 from sklearn.utils.mocking import MockDataFrame
+from sklearn import config_context

 def test_make_rng():
@@ -274,3 +278,39 @@ def test_gen_even_slices():
     slices = gen_even_slices(10, -1)
     assert_raises_regex(ValueError, "gen_even_slices got n_packs=-1, must be"
                         " >=1", next, slices)
+
+
+@pytest.mark.parametrize(
+    ('row_bytes', 'max_n_rows', 'working_memory', 'expected', 'warning'),
+    [(1024, None, 1, 1024, None),
+     (1024, None, 0.99999999, 1023, None),
+     (1023, None, 1, 1025, None),
+     (1025, None, 1, 1023, None),
+     (1024, None, 2, 2048, None),
+     (1024, 7, 1, 7, None),
+     (1024 * 1024, None, 1, 1, None),
+     (1024 * 1024 + 1, None, 1, 1,
+      'Could not adhere to working_memory config. '
+      'Currently 1MiB, 2MiB required.'),
+     ])
+def test_get_chunk_n_rows(row_bytes, max_n_rows, working_memory,
+                          expected, warning):
+    if warning is not None:
+        def check_warning(*args, **kw):
+            return assert_warns_message(UserWarning, warning, *args, **kw)
+    else:
+        check_warning = assert_no_warnings
+
+    actual = check_warning(get_chunk_n_rows,
+                           row_bytes=row_bytes,
+                           max_n_rows=max_n_rows,
+                           working_memory=working_memory)
+
+    assert actual == expected
+    assert type(actual) is type(expected)
+    with config_context(working_memory=working_memory):
+        actual = check_warning(get_chunk_n_rows,
+                               row_bytes=row_bytes,
+                               max_n_rows=max_n_rows)
+        assert actual == expected
+        assert type(actual) is type(expected)

[MRG] ENH: Added block_size parameter for lesser memory consumption
#### Reference Issue
Fixes #7287

#### What does this implement/fix? Explain your changes.
This intends to add a function `pairwise_distances_blockwise` with an additional block_size parameter to avoid storing all the O(n^2) pairs' distances. This would then be used by `neighbors`, `silhouette_score` and `pairwise_distances_argmin_min`.

#### Any other comments?

- [x] Stacking within `pairwise_distances_reduce`
- [x] Revert changes to `sklearn/neighbors`
- [x] Docstring for `pairwise_distances_reduce`
- [x] Tests for `pairwise_distances_reduce`


diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst
index a7ae9f7415243..b4a8046d02956 100644
--- a/doc/whats_new/v0.20.rst
+++ b/doc/whats_new/v0.20.rst
@@ -125,6 +125,13 @@ Classifiers and regressors
   only require X to be an object with finite length or shape.
   :issue:`9832` by :user:`Vrishank Bhardwaj <vrishank97>`.

+Cluster
+
+- :class:`cluster.KMeans`, :class:`cluster.MiniBatchKMeans` and
+  :func:`cluster.k_means` passed with ``algorithm='full'`` now enforces
+  row-major ordering, improving runtime.
+  :issue:`10471` by :user:`Gaurav Dhingra <gxyd>`.
+
 Preprocessing

 - :class:`preprocessing.PolynomialFeatures` now supports sparse input.
diff --git a/sklearn/cluster/k_means_.py b/sklearn/cluster/k_means_.py
index 07e390701eaac..5b6866b61d748 100644
--- a/sklearn/cluster/k_means_.py
+++ b/sklearn/cluster/k_means_.py
@@ -22,6 +22,7 @@
 from ..utils.extmath import row_norms, squared_norm, stable_cumsum
 from ..utils.sparsefuncs_fast import assign_rows_csr
 from ..utils.sparsefuncs import mean_variance_axis
+from ..utils.validation import _num_samples
 from ..utils import check_array
 from ..utils import check_random_state
 from ..utils import as_float_array
@@ -175,7 +176,9 @@ def k_means(X, n_clusters, init='k-means++', precompute_distances='auto',
     Parameters
     ----------
     X : array-like or sparse matrix, shape (n_samples, n_features)
-        The observations to cluster.
+        The observations to cluster. It must be noted that the data
+        will be converted to C ordering, which will cause a memory copy
+        if the given data is not C-contiguous.

     n_clusters : int
         The number of clusters to form as well as the number of
@@ -230,10 +233,12 @@ def k_means(X, n_clusters, init='k-means++', precompute_distances='auto',

     copy_x : boolean, optional
         When pre-computing distances it is more numerically accurate to center
-        the data first.  If copy_x is True, then the original data is not
-        modified.  If False, the original data is modified, and put back before
-        the function returns, but small numerical differences may be introduced
-        by subtracting and then adding the data mean.
+        the data first.  If copy_x is True (default), then the original data is
+        not modified, ensuring X is C-contiguous.  If False, the original data
+        is modified, and put back before the function returns, but small
+        numerical differences may be introduced by subtracting and then adding
+        the data mean, in this case it will also not ensure that data is
+        C-contiguous which may cause a significant slowdown.

     n_jobs : int
         The number of jobs to use for the computation. This works by computing
@@ -280,7 +285,14 @@ def k_means(X, n_clusters, init='k-means++', precompute_distances='auto',
         raise ValueError('Number of iterations should be a positive number,'
                          ' got %d instead' % max_iter)

-    X = as_float_array(X, copy=copy_x)
+    # avoid forcing order when copy_x=False
+    order = "C" if copy_x else None
+    X = check_array(X, accept_sparse='csr', dtype=[np.float64, np.float32],
+                    order=order, copy=copy_x)
+    # verify that the number of samples given is larger than k
+    if _num_samples(X) < n_clusters:
+        raise ValueError("n_samples=%d should be >= n_clusters=%d" % (
+            _num_samples(X), n_clusters))
     tol = _tolerance(X, tol)

     # If the distances are precomputed every job will create a matrix of shape
@@ -392,8 +404,7 @@ def _kmeans_single_elkan(X, n_clusters, max_iter=300, init='k-means++',
                          random_state=None, tol=1e-4,
                          precompute_distances=True):
     if sp.issparse(X):
-        raise ValueError("algorithm='elkan' not supported for sparse input X")
-    X = check_array(X, order="C")
+        raise TypeError("algorithm='elkan' not supported for sparse input X")
     random_state = check_random_state(random_state)
     if x_squared_norms is None:
         x_squared_norms = row_norms(X, squared=True)
@@ -768,12 +779,14 @@ class KMeans(BaseEstimator, ClusterMixin, TransformerMixin):
         If None, the random number generator is the RandomState instance used
         by `np.random`.

-    copy_x : boolean, default True
+    copy_x : boolean, optional
         When pre-computing distances it is more numerically accurate to center
-        the data first.  If copy_x is True, then the original data is not
-        modified.  If False, the original data is modified, and put back before
-        the function returns, but small numerical differences may be introduced
-        by subtracting and then adding the data mean.
+        the data first.  If copy_x is True (default), then the original data is
+        not modified, ensuring X is C-contiguous.  If False, the original data
+        is modified, and put back before the function returns, but small
+        numerical differences may be introduced by subtracting and then adding
+        the data mean, in this case it will also not ensure that data is
+        C-contiguous which may cause a significant slowdown.

     n_jobs : int
         The number of jobs to use for the computation. This works by computing
@@ -860,14 +873,6 @@ def __init__(self, n_clusters=8, init='k-means++', n_init=10,
         self.n_jobs = n_jobs
         self.algorithm = algorithm

-    def _check_fit_data(self, X):
-        """Verify that the number of samples given is larger than k"""
-        X = check_array(X, accept_sparse='csr', dtype=[np.float64, np.float32])
-        if X.shape[0] < self.n_clusters:
-            raise ValueError("n_samples=%d should be >= n_clusters=%d" % (
-                X.shape[0], self.n_clusters))
-        return X
-
     def _check_test_data(self, X):
         X = check_array(X, accept_sparse='csr', dtype=FLOAT_DTYPES)
         n_samples, n_features = X.shape
@@ -885,13 +890,14 @@ def fit(self, X, y=None):
         Parameters
         ----------
         X : array-like or sparse matrix, shape=(n_samples, n_features)
-            Training instances to cluster.
+            Training instances to cluster. It must be noted that the data
+            will be converted to C ordering, which will cause a memory
+            copy if the given data is not C-contiguous.

         y : Ignored

         """
         random_state = check_random_state(self.random_state)
-        X = self._check_fit_data(X)

         self.cluster_centers_, self.labels_, self.inertia_, self.n_iter_ = \
             k_means(
@@ -944,7 +950,6 @@ def fit_transform(self, X, y=None):
         # np.array or CSR format already.
         # XXX This skips _check_test_data, which may change the dtype;
         # we should refactor the input validation.
-        X = self._check_fit_data(X)
         return self.fit(X)._transform(X)

     def transform(self, X):
@@ -1351,7 +1356,9 @@ def fit(self, X, y=None):
         Parameters
         ----------
         X : array-like or sparse matrix, shape=(n_samples, n_features)
-            Training instances to cluster.
+            Training instances to cluster. It must be noted that the data
+            will be converted to C ordering, which will cause a memory copy
+            if the given data is not C-contiguous.

         y : Ignored

@@ -1361,8 +1368,8 @@ def fit(self, X, y=None):
                         dtype=[np.float64, np.float32])
         n_samples, n_features = X.shape
         if n_samples < self.n_clusters:
-            raise ValueError("Number of samples smaller than number "
-                             "of clusters.")
+            raise ValueError("n_samples=%d should be >= n_clusters=%d"
+                             % (n_samples, self.n_clusters))

         n_init = self.n_init
         if hasattr(self.init, '__array__'):
@@ -1516,13 +1523,14 @@ def partial_fit(self, X, y=None):
         Parameters
         ----------
         X : array-like, shape = [n_samples, n_features]
-            Coordinates of the data points to cluster.
+            Coordinates of the data points to cluster. It must be noted that
+            X will be copied if it is not C-contiguous.

         y : Ignored

         """

-        X = check_array(X, accept_sparse="csr")
+        X = check_array(X, accept_sparse="csr", order="C")
         n_samples, n_features = X.shape
         if hasattr(self.init, '__array__'):
             self.init = np.ascontiguousarray(self.init, dtype=X.dtype)

diff --git a/sklearn/cluster/tests/test_k_means.py b/sklearn/cluster/tests/test_k_means.py
index f0f2b56bd591c..de8772d761e22 100644
--- a/sklearn/cluster/tests/test_k_means.py
+++ b/sklearn/cluster/tests/test_k_means.py
@@ -169,7 +169,8 @@ def _check_fitted_model(km):
     assert_greater(km.inertia_, 0.0)

     # check error on dataset being too small
-    assert_raises(ValueError, km.fit, [[0., 1.]])
+    assert_raise_message(ValueError, "n_samples=1 should be >= n_clusters=%d"
+                         % km.n_clusters, km.fit, [[0., 1.]])

 def test_k_means_plus_plus_init():
@@ -750,6 +751,11 @@ def test_k_means_function():
     # to many clusters desired
     assert_raises(ValueError, k_means, X, n_clusters=X.shape[0] + 1)

+    # kmeans for algorithm='elkan' raises TypeError on sparse matrix
+    assert_raise_message(TypeError, "algorithm='elkan' not supported for "
+                         "sparse input X", k_means, X=X_csr, n_clusters=2,
+                         algorithm="elkan")
+

 def test_x_squared_norms_init_centroids():
     """Test that x_squared_norms can be None in _init_centroids"""

KMeans optimisation for array C/F contiguity (was: Make sure that the output of PCA.fit_transform is C contiguous)
otherwise, I would rather use:

``` Python
pca.fit(X)
X_new = pca.transform(X)
```

Because of FORTRAN data for inner product is very slow. (for example, in the KMeans)

KMeans optimisation for array C/F contiguity (was: Make sure that the output of PCA.fit_transform is C contiguous)
otherwise, I would rather use:

``` Python
pca.fit(X)
X_new = pca.transform(X)
```

Because of FORTRAN data for inner product is very slow. (for example, in the KMeans)

Can you give an example?
Maybe we should then rather change the behavior in KMeans.
I wouldn't change the output format, since we don't know what the user wants to do next.

They should be the same that the output and input format of PCA.fit_transform, isn't it?
Because PCA.transform is such.

In _k_means._assign_labels_array, the ddot will be very slow, when X is Fortran data.

``` Python
    for sample_idx in range(n_samples):
        min_dist = -1
        for center_idx in range(n_clusters):
            dist = 0.0
            # hardcoded: minimize euclidean distance to cluster center:
            # ||a - b||^2 = ||a||^2 + ||b||^2 -2 <a, b>
            dist += ddot(n_features, &X[sample_idx, 0], x_stride,
                         &centers[center_idx, 0], center_stride)
```

I have a large sample set, before the dimension reduction, each iteration only need a little more than a minute, but need to be 7 minute after dimension reduction.

2 important questions:
- How general is the requirement to be C contiguous? It speeds up for
  k-means, but will it not slow down for other algorithms?
- Can the change be made in k-means rather than in PCA? If you are not
  using copy_X=False, I don't see any argument for not making the change
  in k-means, and I would much prefer it.

- I just think it is a natural idea that the features of a sample are stored in a row.
- I don't know how to speeds up for k-means, in the case of using Fortran data. :(
- copy_X=False. This reduces the memory usage, but without speeds up for k-means. Anyway I like it too.

>   • I just think it is a natural idea that the features of a sample are stored in a row.

I think that you are assuming too much.

>   • I don't know how to speeds up for k-means, in the case of using Fortran
>     data. :(

It's just a question of calling "np.ascontiguousarray" on the array when
making the copy.

>   • copy_X=False. This reduces the memory usage,

OK. We would have to do a copy here.

I am opposed to changing the behavior of PCA without a more exhausive
review of what it implies speedwise in the codebase: what algorithms
perform best with C or Fortran ordered data.

Ok, I agree with you, and thank you for your explanation.

@zhaipro I think it would be appreciated if you could benchmark k-means with different memory layouts and see how it performs. We could change the memory layout there, which would solve your problem. Btw, you might be interested in this PR #2008 which will make k-means quite a bit faster.

benchmark:
C order time: 1.370000 inertia: 422652.759578
F order time: 6.536000 inertia: 422652.759578

I have a large sample set, so that the `precompute_distances = False`,
by the following code in k_means:

``` Python
if precompute_distances == 'auto':
    n_samples = X.shape[0]
    precompute_distances = (n_clusters * n_samples) < 12e6
```

Here, To show my problem, I set directly `precompute_distances = False`.

``` Python
import numpy as np
from sklearn.cluster import KMeans
from time import time

def bench_kmeans(name, data):
    start = time()
    km = KMeans(n_clusters=200, init='random', n_init=1, max_iter=1,
                copy_x=False, random_state=42, precompute_distances=False).fit(data)
    print("%s time: %f inertia: %f" % (name, time() - start, km.inertia_))

np.random.seed(0)
data = np.random.random(3000*1000).reshape((3000, 1000))
# for C order
bench_kmeans(name='C order', data=data)
# for F order
data = np.asfortranarray(data)
bench_kmeans(name='F order', data=data)
```

Is that true for slim and fat data and different number of clusters? Also, have you tried the elkan branch?

Cool, for elkan alg.
@amueller However, it requires more memory?

```
('shape:', (3000, 1000))
n_clusters  alg name    time    inertia:
50  lloyd   C order 0.426000    453724.729456
50  lloyd   F order 1.782000    453724.729456
50  elkan   C order 0.588000    244227.752545
50  elkan   F order 0.652000    244227.752545
100 lloyd   C order 0.744000    442351.514140
100 lloyd   F order 3.488000    442351.514140
100 elkan   C order 0.892000    239220.149899
100 elkan   F order 0.942000    239220.149899
200 lloyd   C order 1.401000    422652.759578
200 lloyd   F order 6.694000    422652.759578
200 elkan   C order 1.608000    229660.875075
200 elkan   F order 1.632000    229660.875075
('shape:', (1000, 3000))
n_clusters  alg name    time    inertia:
50  lloyd   C order 0.510000    453021.642152
50  lloyd   F order 1.416000    453021.642152
50  elkan   C order 0.630000    236247.135296
50  elkan   F order 0.692000    236247.135296
100 lloyd   C order 1.042000    427321.624518
100 lloyd   F order 2.580000    427321.624518
100 elkan   C order 1.152000    222766.273428
100 elkan   F order 1.212000    222766.273428
200 lloyd   C order 1.908000    378199.959299
200 lloyd   F order 5.046000    378199.959299
200 elkan   C order 1.964000    196655.300444
200 elkan   F order 2.069000    196655.300444
```

``` Python
import numpy as np
from sklearn.cluster import KMeans
from time import time
from itertools import product

def bench_kmeans(name, data, alg, n_clusters):
    start = time()
    km = KMeans(algorithm=alg, n_clusters=n_clusters, init='random', n_init=1, max_iter=1,
                copy_x=False, random_state=42, precompute_distances=False).fit(data)
    print("%d\t%s\t%s\t%f\t%f" % (n_clusters, alg, name, time() - start, km.inertia_))

def test_kmeans(data):
    c_data = data
    f_data = np.asfortranarray(data)
    print('n_clusters\talg\tname\ttime\tinertia:')
    for n_clusters, alg, (name, data) in product((50, 100, 200),
                                    ('lloyd', 'elkan'),
                                     zip(('C order', 'F order'), (c_data, f_data))):
        bench_kmeans(name=name, data=data, alg=alg, n_clusters=n_clusters)

np.random.seed(0)
data = np.random.random(3000*1000).reshape((3000, 1000))
print('shape:', data.shape)
test_kmeans(data)
data = data.reshape((1000, 3000))
print('shape:', data.shape)
test_kmeans(data)
```

Thanks a lot for the benchmark.
I'm quite surprised at the difference in inertia between the two implementations... maybe I messed up the stopping criterion again?
The memory requirement for elkan shouldn't be substantial.

It looks to me like for lloyd (the current implementation) we should `copy("C")` by default (or rather do that in `check_array`
I'm not sure if we should make this optional or add a parameter. I think I'd actually not add a parameter but just add to the docs "the data will be converted to C ordering, which might cause a memory copy if the data is given in fortran order" something like that.

> I'm quite surprised at the difference in inertia between the two implementations...

@amueller I think maybe the reason is that I have only one iteration.

makes sense.

Can someone give me a little explanation of what needs to be done for this issue? Since it seems like the issue description (of title was later updated), perhaps the issue description was not updated.

In particular, there is term ['C contiguous'](https://www.ibm.com/support/knowledgecenter/SSAT4T_14.1.0/com.ibm.xlf141.linux.doc/language_ref/contiguous.html) (probably right link) what does that refer, is that fortran term? machine learning term? or [numpy.ascountiguous](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ascontiguousarray.html).

Also if a summary of issue discussion can be given it had be great (since following the issue discussion is a little confusing for me). I'm assuming that this issue needs work, even though the PR https://github.com/scikit-learn/scikit-learn/pull/5414 was merged, which seems to be for accelerating K means algorithm.
Summary: KMeans, when using check_array, should declare order='C'.

See https://docs.scipy.org/doc/numpy-1.12.0/glossary.html#term-row-major
Can you give an example?
Maybe we should then rather change the behavior in KMeans.
I wouldn't change the output format, since we don't know what the user wants to do next.

They should be the same that the output and input format of PCA.fit_transform, isn't it?
Because PCA.transform is such.

In _k_means._assign_labels_array, the ddot will be very slow, when X is Fortran data.

``` Python
    for sample_idx in range(n_samples):
        min_dist = -1
        for center_idx in range(n_clusters):
            dist = 0.0
            # hardcoded: minimize euclidean distance to cluster center:
            # ||a - b||^2 = ||a||^2 + ||b||^2 -2 <a, b>
            dist += ddot(n_features, &X[sample_idx, 0], x_stride,
                         &centers[center_idx, 0], center_stride)
```

I have a large sample set, before the dimension reduction, each iteration only need a little more than a minute, but need to be 7 minute after dimension reduction.

2 important questions:
- How general is the requirement to be C contiguous? It speeds up for
  k-means, but will it not slow down for other algorithms?
- Can the change be made in k-means rather than in PCA? If you are not
  using copy_X=False, I don't see any argument for not making the change
  in k-means, and I would much prefer it.

- I just think it is a natural idea that the features of a sample are stored in a row.
- I don't know how to speeds up for k-means, in the case of using Fortran data. :(
- copy_X=False. This reduces the memory usage, but without speeds up for k-means. Anyway I like it too.

>   • I just think it is a natural idea that the features of a sample are stored in a row.

I think that you are assuming too much.

>   • I don't know how to speeds up for k-means, in the case of using Fortran
>     data. :(

It's just a question of calling "np.ascontiguousarray" on the array when
making the copy.

>   • copy_X=False. This reduces the memory usage,

OK. We would have to do a copy here.

I am opposed to changing the behavior of PCA without a more exhausive
review of what it implies speedwise in the codebase: what algorithms
perform best with C or Fortran ordered data.

Ok, I agree with you, and thank you for your explanation.

@zhaipro I think it would be appreciated if you could benchmark k-means with different memory layouts and see how it performs. We could change the memory layout there, which would solve your problem. Btw, you might be interested in this PR #2008 which will make k-means quite a bit faster.

benchmark:
C order time: 1.370000 inertia: 422652.759578
F order time: 6.536000 inertia: 422652.759578

I have a large sample set, so that the `precompute_distances = False`,
by the following code in k_means:

``` Python
if precompute_distances == 'auto':
    n_samples = X.shape[0]
    precompute_distances = (n_clusters * n_samples) < 12e6
```

Here, To show my problem, I set directly `precompute_distances = False`.

``` Python
import numpy as np
from sklearn.cluster import KMeans
from time import time

def bench_kmeans(name, data):
    start = time()
    km = KMeans(n_clusters=200, init='random', n_init=1, max_iter=1,
                copy_x=False, random_state=42, precompute_distances=False).fit(data)
    print("%s time: %f inertia: %f" % (name, time() - start, km.inertia_))

np.random.seed(0)
data = np.random.random(3000*1000).reshape((3000, 1000))
# for C order
bench_kmeans(name='C order', data=data)
# for F order
data = np.asfortranarray(data)
bench_kmeans(name='F order', data=data)
```

Is that true for slim and fat data and different number of clusters? Also, have you tried the elkan branch?

Cool, for elkan alg.
@amueller However, it requires more memory?

```
('shape:', (3000, 1000))
n_clusters  alg name    time    inertia:
50  lloyd   C order 0.426000    453724.729456
50  lloyd   F order 1.782000    453724.729456
50  elkan   C order 0.588000    244227.752545
50  elkan   F order 0.652000    244227.752545
100 lloyd   C order 0.744000    442351.514140
100 lloyd   F order 3.488000    442351.514140
100 elkan   C order 0.892000    239220.149899
100 elkan   F order 0.942000    239220.149899
200 lloyd   C order 1.401000    422652.759578
200 lloyd   F order 6.694000    422652.759578
200 elkan   C order 1.608000    229660.875075
200 elkan   F order 1.632000    229660.875075
('shape:', (1000, 3000))
n_clusters  alg name    time    inertia:
50  lloyd   C order 0.510000    453021.642152
50  lloyd   F order 1.416000    453021.642152
50  elkan   C order 0.630000    236247.135296
50  elkan   F order 0.692000    236247.135296
100 lloyd   C order 1.042000    427321.624518
100 lloyd   F order 2.580000    427321.624518
100 elkan   C order 1.152000    222766.273428
100 elkan   F order 1.212000    222766.273428
200 lloyd   C order 1.908000    378199.959299
200 lloyd   F order 5.046000    378199.959299
200 elkan   C order 1.964000    196655.300444
200 elkan   F order 2.069000    196655.300444
```

``` Python
import numpy as np
from sklearn.cluster import KMeans
from time import time
from itertools import product

def bench_kmeans(name, data, alg, n_clusters):
    start = time()
    km = KMeans(algorithm=alg, n_clusters=n_clusters, init='random', n_init=1, max_iter=1,
                copy_x=False, random_state=42, precompute_distances=False).fit(data)
    print("%d\t%s\t%s\t%f\t%f" % (n_clusters, alg, name, time() - start, km.inertia_))

def test_kmeans(data):
    c_data = data
    f_data = np.asfortranarray(data)
    print('n_clusters\talg\tname\ttime\tinertia:')
    for n_clusters, alg, (name, data) in product((50, 100, 200),
                                    ('lloyd', 'elkan'),
                                     zip(('C order', 'F order'), (c_data, f_data))):
        bench_kmeans(name=name, data=data, alg=alg, n_clusters=n_clusters)

np.random.seed(0)
data = np.random.random(3000*1000).reshape((3000, 1000))
print('shape:', data.shape)
test_kmeans(data)
data = data.reshape((1000, 3000))
print('shape:', data.shape)
test_kmeans(data)
```

Thanks a lot for the benchmark.
I'm quite surprised at the difference in inertia between the two implementations... maybe I messed up the stopping criterion again?
The memory requirement for elkan shouldn't be substantial.

It looks to me like for lloyd (the current implementation) we should `copy("C")` by default (or rather do that in `check_array`
I'm not sure if we should make this optional or add a parameter. I think I'd actually not add a parameter but just add to the docs "the data will be converted to C ordering, which might cause a memory copy if the data is given in fortran order" something like that.

> I'm quite surprised at the difference in inertia between the two implementations...

@amueller I think maybe the reason is that I have only one iteration.

makes sense.

Can someone give me a little explanation of what needs to be done for this issue? Since it seems like the issue description (of title was later updated), perhaps the issue description was not updated.

In particular, there is term ['C contiguous'](https://www.ibm.com/support/knowledgecenter/SSAT4T_14.1.0/com.ibm.xlf141.linux.doc/language_ref/contiguous.html) (probably right link) what does that refer, is that fortran term? machine learning term? or [numpy.ascountiguous](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ascontiguousarray.html).

Also if a summary of issue discussion can be given it had be great (since following the issue discussion is a little confusing for me). I'm assuming that this issue needs work, even though the PR https://github.com/scikit-learn/scikit-learn/pull/5414 was merged, which seems to be for accelerating K means algorithm.
Summary: KMeans, when using check_array, should declare order='C'.

See https://docs.scipy.org/doc/numpy-1.12.0/glossary.html#term-row-major