diff --git a/dask/array/percentile.py b/dask/array/percentile.py
index baa1e5bf0..0fd239e74 100644
--- a/dask/array/percentile.py
+++ b/dask/array/percentile.py
@@ -14,21 +14,22 @@ from .. import sharedict
 
 @wraps(np.percentile)
 def _percentile(a, q, interpolation='linear'):
+    n = len(a)
     if not len(a):
-        return None
+        return None, n
     if isinstance(q, Iterator):
         q = list(q)
     if a.dtype.name == 'category':
         result = np.percentile(a.codes, q, interpolation=interpolation)
         import pandas as pd
-        return pd.Categorical.from_codes(result, a.categories, a.ordered)
+        return pd.Categorical.from_codes(result, a.categories, a.ordered), n
     if np.issubdtype(a.dtype, np.datetime64):
         a2 = a.astype('i8')
         result = np.percentile(a2, q, interpolation=interpolation)
-        return result.astype(a.dtype)
+        return result.astype(a.dtype), n
     if not np.issubdtype(a.dtype, np.number):
         interpolation = 'nearest'
-    return np.percentile(a, q, interpolation=interpolation)
+    return np.percentile(a, q, interpolation=interpolation), n
 
 
 def percentile(a, q, interpolation='linear'):
@@ -49,7 +50,7 @@ def percentile(a, q, interpolation='linear'):
 
     name2 = 'percentile-' + token
     dsk2 = {(name2, 0): (merge_percentiles, q, [q] * len(a.chunks[0]),
-                         sorted(dsk), a.chunks[0], interpolation)}
+                         sorted(dsk), interpolation)}
 
     dtype = a.dtype
     if np.issubdtype(dtype, np.integer):
@@ -60,7 +61,7 @@ def percentile(a, q, interpolation='linear'):
     return Array(dsk, name2, chunks=((len(q),),), dtype=dtype)
 
 
-def merge_percentiles(finalq, qs, vals, Ns, interpolation='lower'):
+def merge_percentiles(finalq, qs, vals, interpolation='lower', Ns=None):
     """ Combine several percentile calculations of different data.
 
     Parameters
@@ -86,7 +87,7 @@ def merge_percentiles(finalq, qs, vals, Ns, interpolation='lower'):
     >>> vals = [np.array([1, 2, 3, 4]), np.array([10, 11, 12, 13])]
     >>> Ns = [100, 100]  # Both original arrays had 100 elements
 
-    >>> merge_percentiles(finalq, qs, vals, Ns)
+    >>> merge_percentiles(finalq, qs, vals, Ns=Ns)
     array([ 1,  2,  3,  4, 10, 11, 12, 13])
     """
     if isinstance(finalq, Iterator):
@@ -94,6 +95,8 @@ def merge_percentiles(finalq, qs, vals, Ns, interpolation='lower'):
     finalq = np.array(finalq)
     qs = list(map(list, qs))
     vals = list(vals)
+    if Ns is None:
+        vals, Ns = zip(*vals)
     Ns = list(Ns)
 
     L = list(zip(*[(q, val, N) for q, val, N in zip(qs, vals, Ns) if N]))
@@ -104,7 +107,7 @@ def merge_percentiles(finalq, qs, vals, Ns, interpolation='lower'):
     # TODO: Perform this check above in percentile once dtype checking is easy
     #       Here we silently change meaning
     if vals[0].dtype.name == 'category':
-        result = merge_percentiles(finalq, qs, [v.codes for v in vals], Ns, interpolation)
+        result = merge_percentiles(finalq, qs, [v.codes for v in vals], interpolation, Ns)
         import pandas as pd
         return pd.Categorical.from_codes(result, vals[0].categories, vals[0].ordered)
     if not np.issubdtype(vals[0].dtype, np.number):
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
index 1e047d977..e2034ae7c 100644
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -3446,14 +3446,12 @@ def quantile(df, q):
     name = 'quantiles-1-' + token
     val_dsk = {(name, i): (_percentile, (getattr, key, 'values'), qs)
                for i, key in enumerate(df.__dask_keys__())}
-    name2 = 'quantiles-2-' + token
-    len_dsk = {(name2, i): (len, key) for i, key in enumerate(df.__dask_keys__())}
 
     name3 = 'quantiles-3-' + token
     merge_dsk = {(name3, 0): finalize_tsk((merge_percentiles, qs,
                                            [qs] * df.npartitions,
-                                           sorted(val_dsk), sorted(len_dsk)))}
-    dsk = merge(df.dask, val_dsk, len_dsk, merge_dsk)
+                                           sorted(val_dsk)))}
+    dsk = merge(df.dask, val_dsk, merge_dsk)
     return return_type(dsk, name3, meta, new_divisions)
 
 
diff --git a/dask/dataframe/partitionquantiles.py b/dask/dataframe/partitionquantiles.py
index b83dbabc6..aeaf4c2d8 100644
--- a/dask/dataframe/partitionquantiles.py
+++ b/dask/dataframe/partitionquantiles.py
@@ -410,7 +410,7 @@ def percentiles_summary(df, num_old, num_new, upsample, state):
     if is_categorical_dtype(data):
         data = data.codes
         interpolation = 'nearest'
-    vals = _percentile(data, qs, interpolation=interpolation)
+    vals, n = _percentile(data, qs, interpolation=interpolation)
     if interpolation == 'linear' and np.issubdtype(data.dtype, np.integer):
         vals = np.round(vals).astype(data.dtype)
     vals_and_weights = percentiles_to_weights(qs, vals, length)
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 4ac97da0f..e76dabba2 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -11,6 +11,8 @@ Array
 - Update error handling when len is called with empty chunks (:issue:`3058`) `Xander Johnson`_
 - Fixes a metadata bug with ``store``'s ``return_stored`` option (:pr:`3064`) `John A Kirkham`_
 - Fix a bug in ``optimization.fuse_slice`` to properly handle when first input is ``None`` (:pr:`3076`) `James Bourbeau`_
+- Support arrays with unknown chunk sizes in percentile (:pr:`3107`) `Matthew Rocklin`_
+
 
 DataFrame
 +++++++++

BUG: da.percentile silently returns nan for array of unknown length
So the percentile on a normal dask array is fine:

```
In [2]: a = da.from_array(np.random.randn(10), chunks=(10,))

In [3]: a
Out[3]: dask.array<array, shape=(10,), dtype=float64, chunksize=(10,)>

In [4]: da.percentile(a, 50)
Out[4]: dask.array<percentile, shape=(1,), dtype=float64, chunksize=(1,)>

In [5]: da.percentile(a, 50).compute()
Out[5]: array([ 0.27603973])
```

but if it has unknown chunk sizes (eg from getting the values from a dataframe), it doesn't complain about this, but silently returns a dask array consisting of NaNs:
```
In [6]: ddf = dd.from_pandas(pd.DataFrame(np.random.randn(10, 3)), npartitions=2)

In [7]: ddf.values
Out[7]: dask.array<values, shape=(nan, 3), dtype=float64, chunksize=(nan, 3)>

In [8]: a2 = ddf.values[:, 0]

In [9]: a2
Out[9]: dask.array<getitem, shape=(nan,), dtype=float64, chunksize=(nan,)>

In [10]: da.percentile(a2, 50)
Out[10]: dask.array<percentile, shape=(1,), dtype=float64, chunksize=(1,)>

In [11]: da.percentile(a2, 50).compute()
Out[11]: array([ nan])
```

Should this rather raise an error?
diff --git a/dask/array/tests/test_percentiles.py b/dask/array/tests/test_percentiles.py
index 1ff01aa6d..f6a9b3408 100644
--- a/dask/array/tests/test_percentiles.py
+++ b/dask/array/tests/test_percentiles.py
@@ -59,3 +59,16 @@ def test_percentiles_with_scaler_percentile(q):
     # See #3020
     d = da.ones((16,), chunks=(4,))
     assert_eq(da.percentile(d, q), np.array([1], dtype=d.dtype))
+
+
+def test_unknown_chunk_sizes():
+    x = da.random.random(1000, chunks=(100,))
+    x._chunks = ((np.nan,) * 10,)
+
+    result = da.percentile(x, 50).compute()
+    assert 0.1 < result < 0.9
+
+    a, b = da.percentile(x, [40, 60]).compute()
+    assert 0.1 < a < 0.9
+    assert 0.1 < b < 0.9
+    assert a < b

pip install -e .[complete]
parse_log_pytest
pytest
pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
click==8.0.4
cloudpickle==2.2.1
-e git+https://github.com/dask/dask.git@35ee4dfea06392b3b359869b945d00202358d296#egg=dask
distributed==1.20.2
HeapDict==1.0.1
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
locket==1.0.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
msgpack-python==0.5.6
numpy==1.19.5
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pandas==1.1.5
partd==1.2.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
six==1.17.0
sortedcontainers==2.4.0
tblib==1.7.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
toolz==0.12.0
tornado==6.1
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
zict==2.1.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: dask
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - click==8.0.4
      - cloudpickle==2.2.1
      - distributed==1.20.2
      - heapdict==1.0.1
      - locket==1.0.0
      - msgpack-python==0.5.6
      - numpy==1.19.5
      - pandas==1.1.5
      - partd==1.2.0
      - psutil==7.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - six==1.17.0
      - sortedcontainers==2.4.0
      - tblib==1.7.0
      - toolz==0.12.0
      - tornado==6.1
      - zict==2.1.0
prefix: /opt/conda/envs/dask

dask/array/tests/test_percentiles.py::test_unknown_chunk_sizes
dask/array/tests/test_percentiles.py::test_percentile dask/array/tests/test_percentiles.py::test_percentiles_with_empty_arrays dask/array/tests/test_percentiles.py::test_percentiles_with_scaler_percentile[5] dask/array/tests/test_percentiles.py::test_percentiles_with_scaler_percentile[5.00] dask/array/tests/test_percentiles.py::test_percentiles_with_scaler_percentile[q2] dask/array/tests/test_percentiles.py::test_percentiles_with_scaler_percentile[5.01]
docs/source/changelog.rst dask/dataframe/core.py dask/dataframe/partitionquantiles.py dask/array/percentile.py
docs/source/changelog.rst dask/dataframe/core.py dask/dataframe/partitionquantiles.py dask/array/percentile.py