Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions tests/_support/gen_sortties_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

Run with the spike-venv interpreter and WITHOUT the vendored tree on the path:
.caches/spike-venv/bin/python tests/_support/gen_sortties_oracle.py
Writes tests/ops/sortties_oracle.json.
Writes tests/ops/sortties_oracle.json (macOS, libc++), or on Linux
tests/ops/sortties_oracle_linux.json (the manylinux wheel, libstdc++; issue
#81). The Linux file was recorded in Docker python:3.13 with torch==2.13.0
from the PyTorch CPU index, on linux/arm64 and linux/amd64: the two runs were
byte-identical, so one file serves both architectures.
"""
import json
import os
Expand All @@ -21,7 +25,8 @@
out["cells"][c["id"]] = sc.encode(sc.run_cell(torch, c))
except RuntimeError as e:
out["cells"][c["id"]] = {"error": str(e)}
dest = os.path.join(HERE, "..", "ops", "sortties_oracle.json")
name = "sortties_oracle_linux.json" if sys.platform.startswith("linux") else "sortties_oracle.json"
dest = os.path.join(HERE, "..", "ops", name)
with open(dest, "w") as f:
json.dump(out, f, separators=(",", ":"))
print(len(out["cells"]), "cells", torch.__version__)
26 changes: 15 additions & 11 deletions tests/golden/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -11435,26 +11435,30 @@ def squeeze_dim_cases(torch_module, c_module, torch_call) -> list[Case]:

# Upstream's *unstable* tie order is its C++ runtime's `std::sort`: libc++ on
# macOS (and Android), libstdc++ on Linux, MSVC's STL on Windows. The shim
# reproduces libc++ only (issue #33), so exact tie order is compared only where
# the reference upstream is libc++; elsewhere the shim keeps the stable order
# and that is a known divergence (issue #81), compared by values and pairing.
# reproduces libc++ (issue #33) and libstdc++ (issue #81), so exact tie order is
# compared wherever the reference upstream is one of those; on Windows the shim
# keeps the stable order and that is a known divergence (#81's MSVC half),
# compared by values and pairing.
_LIBCXX_HOST = sys.platform == "darwin"
_LIBSTDCXX_HOST = sys.platform.startswith("linux")
_TIE_ORDER_HOST = _LIBCXX_HOST or _LIBSTDCXX_HOST


def _tie_order_check(t_res, c_res) -> tuple[bool, str]:
if _LIBCXX_HOST:
if _TIE_ORDER_HOST:
return _pair_result_check(t_res, c_res)
ok, why = _topk_multiset_check(t_res, c_res)
return ok, why + " (tie order not compared off libc++, issue #81)"
return ok, why + " (tie order not compared off libc++/libstdc++, issue #81)"


def _tie_heavy():
"""Tie-heavy rows long enough that upstream's unstable sort permutes them
(>= 24 elements; issue #33). Seeded, so both sides see the same numbers.

Empty off a libc++ host (`_LIBCXX_HOST`, issue #81): every case built from
these rows pins the unstable order, which is libc++'s only."""
if not _LIBCXX_HOST:
Empty where the shim does not reproduce upstream's runtime
(`_TIE_ORDER_HOST`, issue #81): every case built from these rows pins the
unstable order."""
if not _TIE_ORDER_HOST:
return []
import random

Expand Down Expand Up @@ -11600,7 +11604,7 @@ def sort_cases(torch_module, c_module, torch_call) -> list[Case]:
op, _pair(torch_module, c_module, f, (len(f),), dn)[1], -1, d
),
value_check=_pair_result_check,
note="indices among tied values come back in upstream's libc++ introsort order",
note="indices among tied values come back in upstream's runtime's introsort order",
)
)
for flat, shape, note in [([5.0], (), "0-d: value and index both 0-d"), ([], (0,), "empty")]:
Expand Down Expand Up @@ -11669,10 +11673,10 @@ def sort_stable_cases(torch_module, c_module, torch_call) -> list[Case]:
stable=st, dim=-1, descending=d,
),
value_check=_pair_result_check,
note="stable=True keeps index order; stable=False is libc++'s introsort order",
note="stable=True keeps index order; stable=False is the runtime's introsort order",
)
)
for dim in (0, 1) if _LIBCXX_HOST else ():
for dim in (0, 1) if _TIE_ORDER_HOST else ():
cases.append(
Case(
name=f"sort.stable(float32, (2,40), stable=False, dim={dim})",
Expand Down
19 changes: 10 additions & 9 deletions tests/golden/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import dtypes as dt
from cases import _LIBCXX_HOST, CASE_BUILDERS, Case
from cases import _TIE_ORDER_HOST, CASE_BUILDERS, Case
from loader import ShimLoadError, load_shim, resolve_torch_overload


Expand Down Expand Up @@ -613,15 +613,16 @@ def _run_one_body(case: Case, inject_fault: str | None, tag_box: list[str]) -> O
("_topk_multiset_check", "permute-all"): "upstream's own order under ties / sorted=False is a partition artefact, not a promise -- pinning it would pin an implementation detail. Note this is `permute-all` (values and indices moved together); plain `permute` breaks the value/index pairing and IS caught",
}

# `_tie_order_check` compares tie order exactly only where upstream's unstable
# sort is libc++'s (macOS); elsewhere it falls back to values and pairing,
# because the shim does not reproduce libstdc++/MSVC order yet (issue #81). So
# it is blind to `permute-all` there and only there; on a libc++ host this
# entry is absent and the fault must be caught.
if not _LIBCXX_HOST:
# `_tie_order_check` compares tie order exactly where upstream's unstable sort
# is libc++'s (macOS) or libstdc++'s (Linux); elsewhere (Windows, MSVC's STL)
# it falls back to values and pairing, because the shim does not reproduce
# that order yet (issue #81). So it is blind to `permute-all` there and only
# there; on a libc++ or libstdc++ host this entry is absent and the fault must
# be caught.
if not _TIE_ORDER_HOST:
BLIND_BY_DESIGN[("_tie_order_check", "permute-all")] = (
"off libc++ the tie order of unstable sort/topk is the platform C++ "
"runtime's, which the shim does not reproduce yet (issue #81), so only "
"off libc++/libstdc++ the tie order of unstable sort/topk is the platform "
"C++ runtime's, which the shim does not reproduce yet (issue #81), so only "
"values and the value/index pairing are compared; plain `permute` "
"breaks the pairing and IS caught"
)
Expand Down
1 change: 1 addition & 0 deletions tests/ops/sortties_oracle_linux.json

Large diffs are not rendered by default.

45 changes: 36 additions & 9 deletions tests/ops/test_sortties.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
decides which token can be sampled. The cpu kernels therefore have to put
tied indices where upstream puts them, not merely return the same values.

The oracle (`sortties_oracle.json`) is upstream torch 2.13.0 on the cpu,
The oracle (`sortties_oracle.json`) is upstream torch 2.13.0 on the cpu
(macOS arm64, libc++); `sortties_oracle_linux.json` is the manylinux wheel
(libstdc++, GCC 13.3.1), recorded identically on aarch64 and x86_64 (#81),
recorded by `tests/_support/gen_sortties_oracle.py` over the cells that
`tests/_support/sortties_cells.py` enumerates: sort / argsort / topk, every
direction and flag, dtypes f32 f16 bf16 f64 i32 i64 u8 bool, tie patterns
Expand Down Expand Up @@ -36,7 +38,8 @@
_REPO = os.path.abspath(os.path.join(_HERE, "..", ".."))
_SUPPORT = os.path.join(_REPO, "tests", "_support")
_VENDOR = os.path.join(_REPO, "torchnative", "python")
_ORACLE = os.path.join(_HERE, "sortties_oracle.json")
_ORACLE_LIBCXX = os.path.join(_HERE, "sortties_oracle.json")
_ORACLE_LIBSTDCXX = os.path.join(_HERE, "sortties_oracle_linux.json")

sys.path.insert(0, _SUPPORT)
import sortties_cells as sc # noqa: E402
Expand Down Expand Up @@ -81,20 +84,27 @@ def _oracle():
return _cache["o"]


# The oracle was measured on macOS arm64, where upstream's unstable order is
# libc++'s. Off libc++ the reference upstream orders ties with its own C++
# runtime and the shim keeps the stable order (issue #81), so every test that
# pins the unstable order is skipped by name there. Stable order and the bool
# refusal hold on every platform and always run.
# Upstream's unstable order is its C++ runtime's `std::sort`: libc++ on Apple
# and Android (`sortties_oracle.json`, macOS arm64), libstdc++ on Linux
# (`sortties_oracle_linux.json`, manylinux aarch64 and x86_64, issue #81). The
# shim transcribes both. On Windows (MSVC's STL, #81's MSVC half) the shim
# keeps the stable order, so every test that pins the unstable order is
# skipped by name there. Stable order and the bool refusal hold on every
# platform; they are checked against this host's oracle, or the libc++ one
# where none exists.
_LIBCXX_HOST = sys.platform == "darwin" or hasattr(sys, "getandroidapilevel")
_LIBSTDCXX_HOST = sys.platform.startswith("linux") and not _LIBCXX_HOST
_ORACLE = _ORACLE_LIBSTDCXX if _LIBSTDCXX_HOST else _ORACLE_LIBCXX


def _libcxx_order(fn):
def run():
if not _LIBCXX_HOST:
if not (_LIBCXX_HOST or _LIBSTDCXX_HOST):
sys.path.insert(0, _SUPPORT)
import _skip
raise _skip.Skip(f"upstream's unstable tie order on {sys.platform} is not libc++'s (issue #81)")
raise _skip.Skip(
f"upstream's unstable tie order on {sys.platform} is MSVC's STL, "
"not transcribed (issue #81)")
fn()
run.__name__ = fn.__name__
return run
Expand Down Expand Up @@ -197,6 +207,23 @@ def test_every_cell_is_checked():
_check(lambda c: True, "all cells")


def test_linux_oracle_is_libstdcxx_not_libcxx():
# The two oracles must disagree on the unstable cells, or the Linux one
# would not be measuring a different runtime; and agree on every stable
# cell, because stable order is not the runtime's.
with open(_ORACLE_LIBCXX) as f:
mac = json.load(f)["cells"]
with open(_ORACLE_LIBSTDCXX) as f:
lin = json.load(f)
assert lin["torch"].startswith("2.13.0"), lin["torch"]
lin = lin["cells"]
assert set(mac) == set(lin) == {c["id"] for c in sc.cells()}
stable = [k for k in mac if "stable=True" in k]
assert all(mac[k] == lin[k] for k in stable), "stable order differs between runtimes"
differ = sum(mac[k] != lin[k] for k in mac)
assert differ > 1000, f"only {differ} cells separate libstdc++ from libc++"


if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--worker":
_worker()
Expand Down
17 changes: 11 additions & 6 deletions tests/ops/test_tail1.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,15 @@ def test_eighty_identical_elements_come_back_in_index_order():
64 and 80 ties all come back permuted, 16 do not, and float64 and int64
behave the same) and answered `[50, 59, 58, ...]` for these eighty.

So where upstream's default is index order, the shim is compared with it
exactly as before. Where it is not, upstream's own *stable* answer is the
reference -- which is index order there too -- and upstream's default is
still required to be a permutation, so a sort that lost or duplicated an
index is not excused as "unspecified order".
That `[50, 59, 58, ...]` is libstdc++'s introsort, not a vector network:
issue #81 transcribed GCC 13.3's `std::sort` (the compiler of upstream's
manylinux wheel) and it reproduces those eighty exactly. So on macOS and
Android (libc++, #33) and on Linux (libstdc++, #81) the shim's default is
compared with upstream's default directly. Only where no runtime is
transcribed (Windows, MSVC's STL) is upstream's own *stable* answer the
reference, and upstream's default is still required to be a permutation,
so a sort that lost or duplicated an index is not excused as "unspecified
order".
"""
if not _have_upstream():
return
Expand All @@ -391,7 +395,8 @@ def test_eighty_identical_elements_come_back_in_index_order():
ours = _C._aten_dispatch("aten.argsort.default", a_c, -1, False)
theirs = _flat(torch.ops.aten.argsort(a_t, -1, False))
assert sorted(theirs) == list(range(80)), theirs
if theirs == list(range(80)):
transcribed = sys.platform == "darwin" or sys.platform.startswith("linux")
if transcribed or theirs == list(range(80)):
_same(torch.ops.aten.argsort(a_t, -1, False), ours, "argsort(80 ties)")
return
stable = torch.ops.aten.argsort.stable(a_t, stable=True, dim=-1, descending=False)
Expand Down
78 changes: 55 additions & 23 deletions torchnative/rust/torch_c/src/aten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19932,23 +19932,24 @@ fn cmp_torch_f64(a: f64, b: f64) -> std::cmp::Ordering {
/// directions, NaN is the greatest value. Every non-cpu device keeps this
/// too: it is what those paths did before, and their tie order is a separate
/// measurement.
/// * `LibcxxSort`: `stable=False`, and `sort`/`argsort` without `stable`.
/// libc++'s `std::sort` over `(value, index)` pairs with a comparator on the
/// value only. Stable below 24 elements, introsort above it.
/// * `TopK { sorted }`: libc++'s `std::partial_sort` when `k * 64 <= n`,
/// * `UnstableSort`: `stable=False`, and `sort`/`argsort` without `stable`.
/// The upstream runtime's `std::sort` (`UPSTREAMS_STD`) over
/// `(value, index)` pairs with a comparator on the value only. libc++ is
/// stable below 24 elements, libstdc++ to 16; introsort above.
/// * `TopK { sorted }`: that runtime's `std::partial_sort` when `k * 64 <= n`,
/// otherwise `std::nth_element(k - 1)` followed, when `sorted`, by
/// `std::sort` of the first `k - 1`.
#[derive(Clone, Copy, PartialEq, Eq)]
enum OrderAlgo {
Stable,
LibcxxSort,
UnstableSort,
TopK { sorted: bool },
}

/// Order one lane: `order` is left holding the lane's indices, best first, at
/// least `keep` of them valid. `at(j)` reads element `j`, `desc` is
/// "descending" (or `largest`).
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn order_lane<K: Copy>(
order: &mut Vec<usize>,
at: impl Fn(usize) -> K,
Expand All @@ -19959,7 +19960,22 @@ fn order_lane<K: Copy>(
lt: impl Fn(K, K) -> bool,
cmp: impl Fn(K, K) -> std::cmp::Ordering,
) {
use crate::libcxx_sort;
use crate::{libcxx_sort, libstdcxx_sort};
let (sort, partial_sort, nth_element): (
fn(&mut [(K, usize)], &mut dyn FnMut(&(K, usize), &(K, usize)) -> bool),
fn(&mut [(K, usize)], usize, &mut dyn FnMut(&(K, usize), &(K, usize)) -> bool),
fn(&mut [(K, usize)], usize, &mut dyn FnMut(&(K, usize), &(K, usize)) -> bool),
) = match UPSTREAMS_STD {
Some(StdRuntime::Libstdcxx) => (
libstdcxx_sort::sort,
libstdcxx_sort::partial_sort,
libstdcxx_sort::nth_element,
),
// `None` never reaches here: `Stable` is chosen instead.
Some(StdRuntime::Libcxx) | None => {
(libcxx_sort::sort, libcxx_sort::partial_sort, libcxx_sort::nth_element)
}
};
let n = order.len();
if algo == OrderAlgo::Stable {
order.sort_by(|&a, &b| if desc { cmp(at(b), at(a)) } else { cmp(at(a), at(b)) });
Expand All @@ -19973,15 +19989,15 @@ fn order_lane<K: Copy>(
let comp: &mut dyn FnMut(&(K, usize), &(K, usize)) -> bool =
if desc { &mut dsc } else { &mut asc };
match algo {
OrderAlgo::LibcxxSort => libcxx_sort::sort(&mut pairs, comp),
OrderAlgo::UnstableSort => sort(&mut pairs, comp),
OrderAlgo::TopK { sorted } => {
if keep > 0 && n > 0 {
if keep * 64 <= n {
libcxx_sort::partial_sort(&mut pairs, keep, comp);
partial_sort(&mut pairs, keep, comp);
} else {
libcxx_sort::nth_element(&mut pairs, keep - 1, comp);
nth_element(&mut pairs, keep - 1, comp);
if sorted {
libcxx_sort::sort(&mut pairs[..keep - 1], comp);
sort(&mut pairs[..keep - 1], comp);
}
}
}
Expand Down Expand Up @@ -20010,7 +20026,8 @@ struct Ordered {
/// and `topk` is `partial_sort` / `nth_element`: they agree with a stable sort
/// on short rows and on all-equal rows, and permute ties on longer ones.
/// `tests/ops/test_sortties.py` carries the measurement (every cell of
/// `sortties_oracle.json`); `libcxx_sort.rs` is the transcription. Top-p
/// `sortties_oracle.json`, and `sortties_oracle_linux.json` on Linux);
/// `libcxx_sort.rs` and `libstdcxx_sort.rs` are the transcriptions. Top-p
/// sampling sorts a whole vocabulary row, so the tie order there decides which
/// token sits on the nucleus boundary.
fn order_along(
Expand Down Expand Up @@ -20185,19 +20202,34 @@ fn sort_stable(
fn unstable_algo(device: &Device) -> OrderAlgo {
// Upstream's unstable tie order is its C++ runtime's `std::sort`, so it is
// per platform: libc++ on Apple and Android, libstdc++ on Linux, MSVC's STL
// on Windows. Only libc++ is transcribed (`libcxx_sort.rs`, measured
// against upstream on macOS arm64); elsewhere the stable order stays, which
// is a known divergence from that platform's upstream (#81), not a match.
if matches!(device, Device::Cpu) && LIBCXX_IS_UPSTREAMS_RUNTIME {
OrderAlgo::LibcxxSort
// on Windows. libc++ (`libcxx_sort.rs`, measured on macOS arm64) and
// libstdc++ (`libstdcxx_sort.rs`, measured on manylinux aarch64 and
// x86_64) are transcribed; on Windows the stable order stays, which is a
// known divergence from that platform's upstream (#81's MSVC half).
if matches!(device, Device::Cpu) && UPSTREAMS_STD.is_some() {
OrderAlgo::UnstableSort
} else {
OrderAlgo::Stable
}
}

/// Whether upstream torch on this target is built against libc++ (#33, #81).
const LIBCXX_IS_UPSTREAMS_RUNTIME: bool =
cfg!(any(target_vendor = "apple", target_os = "android"));
/// The C++ standard library whose `std::sort` upstream torch on this target
/// is built with (#33, #81). `None` (Windows, MSVC's STL) is not transcribed.
#[derive(Clone, Copy, PartialEq, Eq)]
enum StdRuntime {
Libcxx,
Libstdcxx,
}

const UPSTREAMS_STD: Option<StdRuntime> =
if cfg!(any(target_vendor = "apple", target_os = "android")) {
Some(StdRuntime::Libcxx)
} else if cfg!(all(target_os = "linux", target_env = "gnu")) {
// the manylinux wheel, GCC 13.3.1 (`readelf -p .comment libtorch_cpu.so`)
Some(StdRuntime::Libstdcxx)
} else {
None
};

/// `aten::topk(Tensor self, SymInt k, int dim=-1, bool largest=True,
/// bool sorted=True) -> (Tensor values, Tensor indices)`
Expand Down Expand Up @@ -20237,9 +20269,9 @@ fn topk_default(
"topk does not support bool dtypes on CPU",
));
}
// libc++'s partial_sort / nth_element only where libc++ is upstream's
// runtime (#81); the bool refusal above is upstream's on every platform.
if LIBCXX_IS_UPSTREAMS_RUNTIME {
// the runtime's partial_sort / nth_element only where that runtime is
// transcribed (#81); the bool refusal above is upstream's everywhere.
if UPSTREAMS_STD.is_some() {
OrderAlgo::TopK { sorted }
} else {
OrderAlgo::Stable
Expand Down
1 change: 1 addition & 0 deletions torchnative/rust/torch_c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod flash;
mod info;
mod layout;
mod libcxx_sort;
mod libstdcxx_sort;
mod quant;
mod reduced;
mod rng;
Expand Down
2 changes: 1 addition & 1 deletion torchnative/rust/torch_c/src/libcxx_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! but it is what `TopPLogitsWarper` and `multinomial` see, so the cpu path
//! reproduces it instead of answering with a stable order. The tie order is the
//! one libc++ gives; a libstdc++ build of upstream (Linux) orders ties
//! differently, which this module does not model.
//! differently, which `libstdcxx_sort.rs` models (issue #81).
//!
//! `less` is the strict-weak "comes before" comparator, exactly as the C++
//! algorithms take it. Positions are slice indices; no iterator proxies.
Expand Down
Loading
Loading