+
Skip to content

Updating permute to use Axes #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 28, 2022
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
35 changes: 12 additions & 23 deletions src/devices/permute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,14 @@
//! [permuted_loop2] takes in a function that receives the unpermuted set of
//! indices, and the permuted set of indices. This type of function enables
//! only specifying the looping & indexing logic once. Both
//! [DevicePermute2D::permute] and [DevicePermute2D::inverse_permute] share
//! [DevicePermute::permute] and [DevicePermute::inverse_permute] share
//! this looping logic, but only differ in what they do with the indices.

use super::Cpu;
use crate::arrays::{Axes2, Axes3, Axes4};

/// Permutes axes of `A` resulting in `B`.
pub trait DevicePermute2D<A, B, const I: isize, const J: isize> {
fn permute(a: &A, b: &mut B);
fn inverse_permute(a: &mut A, b: &B);
}

/// Permutes axes of `A` resulting in `B`.
pub trait DevicePermute3D<A, B, const I: isize, const J: isize, const K: isize> {
fn permute(a: &A, b: &mut B);
fn inverse_permute(a: &mut A, b: &B);
}

/// Permutes axes of `A` resulting in `B`.
pub trait DevicePermute4D<A, B, const I: isize, const J: isize, const K: isize, const L: isize> {
pub trait DevicePermute<A, B, Axes> {
fn permute(a: &A, b: &mut B);
fn inverse_permute(a: &mut A, b: &B);
}
Expand All @@ -56,7 +45,7 @@ macro_rules! array {
macro_rules! impl_permute {
($Ax0:tt, $Ax1:tt) => {
impl<const M: usize, const N: usize>
DevicePermute2D<array!(0, 1), array!($Ax0, $Ax1), $Ax0, $Ax1> for Cpu
DevicePermute<array!(0, 1), array!($Ax0, $Ax1), Axes2<$Ax0, $Ax1>> for Cpu
{
fn permute(a: &array!(0, 1), b: &mut array!($Ax0, $Ax1)) {
permuted_loop2::<M, N, $Ax0, $Ax1, _>(&mut |[m, n], [i, j]| {
Expand All @@ -72,7 +61,7 @@ impl<const M: usize, const N: usize>
};
($Ax0:tt, $Ax1:tt, $Ax2:tt) => {
impl<const M: usize, const N: usize, const O: usize>
DevicePermute3D<array!(0, 1, 2), array!($Ax0, $Ax1, $Ax2), $Ax0, $Ax1, $Ax2> for Cpu
DevicePermute<array!(0, 1, 2), array!($Ax0, $Ax1, $Ax2), Axes3<$Ax0, $Ax1, $Ax2>> for Cpu
{
fn permute(a: &array!(0, 1, 2), b: &mut array!($Ax0, $Ax1, $Ax2)) {
permuted_loop3::<M, N, O, $Ax0, $Ax1, $Ax2, _>(&mut |[m, n, o], [i, j, k]| {
Expand All @@ -88,7 +77,7 @@ impl<const M: usize, const N: usize, const O: usize>
};
($Ax0:tt, $Ax1:tt, $Ax2:tt, $Ax3:tt) => {
impl<const M: usize, const N: usize, const O: usize, const P: usize>
DevicePermute4D<array!(0,1,2,3), array!($Ax0,$Ax1,$Ax2,$Ax3), $Ax0,$Ax1,$Ax2,$Ax3> for Cpu
DevicePermute<array!(0,1,2,3), array!($Ax0,$Ax1,$Ax2,$Ax3), Axes4<$Ax0,$Ax1,$Ax2,$Ax3>> for Cpu
{
fn permute(a: &array!(0, 1, 2, 3), b: &mut array!($Ax0, $Ax1, $Ax2, $Ax3)) {
permuted_loop4::<M, N, O, P, $Ax0, $Ax1, $Ax2, $Ax3, _>(
Expand Down Expand Up @@ -235,23 +224,23 @@ mod tests {
fn test_2d_permute() {
let a = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let mut b = [[0.0; 2]; 3];
<Cpu as DevicePermute2D<_, _, 1, 0>>::permute(&a, &mut b);
<Cpu as DevicePermute<_, _, Axes2<1, 0>>>::permute(&a, &mut b);
assert_eq!(b, [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]);

let mut c = [[0.0; 3]; 2];
<Cpu as DevicePermute2D<_, _, 1, 0>>::inverse_permute(&mut c, &b);
<Cpu as DevicePermute<_, _, Axes2<1, 0>>>::inverse_permute(&mut c, &b);
assert_eq!(a, c);
}

#[test]
fn test_3d_permute() {
let a = [[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]];
let mut b = [[[0.0; 1]; 2]; 3];
<Cpu as DevicePermute3D<_, _, 2, 1, 0>>::permute(&a, &mut b);
<Cpu as DevicePermute<_, _, Axes3<2, 1, 0>>>::permute(&a, &mut b);
assert_eq!(b, [[[1.0], [4.0]], [[2.0], [5.0]], [[3.0], [6.0]]]);

let mut c = [[[0.0; 3]; 2]; 1];
<Cpu as DevicePermute3D<_, _, 2, 1, 0>>::inverse_permute(&mut c, &b);
<Cpu as DevicePermute<_, _, Axes3<2, 1, 0>>>::inverse_permute(&mut c, &b);
assert_eq!(a, c);
}

Expand All @@ -262,11 +251,11 @@ mod tests {
Cpu::fill(&mut a, &mut |v| *v = rng.gen());

let mut b = [[[[0.0; 3]; 5]; 9]; 7];
<Cpu as DevicePermute4D<_, _, 2, 3, 1, 0>>::permute(&a, &mut b);
<Cpu as DevicePermute<_, _, Axes4<2, 3, 1, 0>>>::permute(&a, &mut b);
assert_ne!(b, [[[[0.0; 3]; 5]; 9]; 7]);

let mut c = [[[[0.0; 9]; 7]; 5]; 3];
<Cpu as DevicePermute4D<_, _, 2, 3, 1, 0>>::inverse_permute(&mut c, &b);
<Cpu as DevicePermute<_, _, Axes4<2, 3, 1, 0>>>::inverse_permute(&mut c, &b);

assert_eq!(a, c);
}
Expand Down
27 changes: 26 additions & 1 deletion src/tensor_ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@
//! add(a, &big);
//! ```
//!
//! # Permutating axes
//!
//! Permutating axes is done via [Permute2D], [Permute3D], and [Permute4D]:
//!
//! 2D version:
//! ```rust
//! # use dfdx::prelude::*;
//! let t: Tensor2D<2, 3> = TensorCreator::zeros();
//! let _: Tensor2D<3, 2> = t.permute_axes::<1, 0>();
//! ```
//!
//! 3D version:
//! ```rust
//! # use dfdx::prelude::*;
//! let t: Tensor3D<2, 3, 4> = TensorCreator::zeros();
//! let _: Tensor3D<3, 4, 2> = t.permute_axes::<1, 2, 0>();
//! ```
//!
//! 4D version:
//! ```rust
//! # use dfdx::prelude::*;
//! let t: Tensor4D<2, 3, 4, 5> = TensorCreator::zeros();
//! let _: Tensor4D<3, 5, 2, 4> = t.permute_axes::<1, 3, 0, 2>();
//! ```
//!
//! # Selects/Indexing
//!
//! Selecting or indexing into a tensor is done via [Select1::select()]. This traits enables
Expand Down Expand Up @@ -140,7 +165,7 @@ pub use impl_sum::*;
pub use impl_sum_axis::*;
pub use map::*;
pub use matmul::*;
pub use permute::{Permute2DSugar, Permute3DSugar, Permute4DSugar};
pub use permute::{Permute2D, Permute3D, Permute4D};
pub use select::*;

#[cfg(feature = "nightly")]
Expand Down
141 changes: 63 additions & 78 deletions src/tensor_ops/permute.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,29 @@
//! Implementation of permute operation
//!
//! There are two traits used here:
//! 1. Permute`2,3,4`d
//! 2. Permute`2,3,4`dSugar
//! 1. [Permute]
//! 2. [Permute2D], [Permute3D], and [Permute4D]
//!
//! The Permute versions have the axes specified on the
//! trait itself (i.e. `Permute2d::<1, 0>::permute()`).
//! The [Permute] versions have the axes specified on the
//! trait itself (i.e. `Permute::<Axes2<1, 0>>::permute()`).
//!
//! The PermuteSugar versions have the axes specified on the
//! function inside the trait (i.e. `Permute2dSugar::permute_axis::<1, 0>()`)
//! [Permute2D], [Permute3D], and [Permute4D] have the axes specified on the
//! function inside the trait (i.e. `Permute2D::permute_axis::<1, 0>()`)

use super::utils::move_tape_and_add_backward_op;
use crate::prelude::*;

/// Permute 2d tensor that are specified at the trait level. See
/// [Permute2dSugar] for a more ergonomic version.
pub trait Permute2D<const I: isize, const J: isize> {
/// The resulting type after being permuted
type Permuted;
fn permute(self) -> Self::Permuted;
}

/// Permute 3d tensor that are specified at the trait level. See
/// [Permute3DSugar] for a more ergonomic version.
pub trait Permute3D<const I: isize, const J: isize, const K: isize> {
/// The resulting type after being permuted
type Permuted;
fn permute(self) -> Self::Permuted;
}

/// Permute 4d tensor that are specified at the trait level. See
/// [Permute4DSugar] for a more ergonomic version.
pub trait Permute4D<const I: isize, const J: isize, const K: isize, const L: isize> {
/// Permute axes that are specified at the trait level. See
/// [Permute2D], [Permute3D], and [Permute4D]
/// for a more ergonomic version.
pub trait Permute<Axes> {
/// The resulting type after being permuted
type Permuted;
fn permute(self) -> Self::Permuted;
}

/// Permute 2d tensor with new axes order specified at the function call level.
pub trait Permute2DSugar: Sized {
pub trait Permute2D: Sized {
/// Generics:
/// - `I` the index of the new 1st dimension, can be 0 or 1
/// - `J` the index of the new 2nd dimension, can be 0 or 1
Expand All @@ -51,15 +36,15 @@ pub trait Permute2DSugar: Sized {
/// ```
fn permute_axes<const I: isize, const J: isize>(self) -> Self::Permuted
where
Self: Permute2D<I, J>,
Self: Permute<Axes2<I, J>>,
{
Permute2D::<I, J>::permute(self)
self.permute()
}
}
impl<const M: usize, const N: usize, H> Permute2DSugar for Tensor2D<M, N, H> {}
impl<const M: usize, const N: usize, H> Permute2D for Tensor2D<M, N, H> {}

/// Permute 3d tensor with new axes order specified at the function call level.
pub trait Permute3DSugar: Sized {
pub trait Permute3D: Sized {
/// Generics:
/// - `I` the index of the new 1st dimension, can be 0, 1, or 2
/// - `J` the index of the new 2nd dimension, can be 0, 1, or 2
Expand All @@ -74,15 +59,15 @@ pub trait Permute3DSugar: Sized {
/// ```
fn permute_axes<const I: isize, const J: isize, const K: isize>(self) -> Self::Permuted
where
Self: Permute3D<I, J, K>,
Self: Permute<Axes3<I, J, K>>,
{
Permute3D::<I, J, K>::permute(self)
self.permute()
}
}
impl<const M: usize, const N: usize, const O: usize, H> Permute3DSugar for Tensor3D<M, N, O, H> {}
impl<const M: usize, const N: usize, const O: usize, H> Permute3D for Tensor3D<M, N, O, H> {}

/// Permute 4d tensor with new axes order specified at the function call level.
pub trait Permute4DSugar: Sized {
pub trait Permute4D: Sized {
/// Generics:
/// - `I` the index of the new 1st dimension, can be 0, 1, 2, or 3
/// - `J` the index of the new 2nd dimension, can be 0, 1, 2, or 3
Expand All @@ -101,12 +86,12 @@ pub trait Permute4DSugar: Sized {
self,
) -> Self::Permuted
where
Self: Permute4D<I, J, K, L>,
Self: Permute<Axes4<I, J, K, L>>,
{
Permute4D::<I, J, K, L>::permute(self)
self.permute()
}
}
impl<const M: usize, const N: usize, const O: usize, const P: usize, H> Permute4DSugar
impl<const M: usize, const N: usize, const O: usize, const P: usize, H> Permute4D
for Tensor4D<M, N, O, P, H>
{
}
Expand All @@ -130,44 +115,44 @@ macro_rules! tensor {
#[rustfmt::skip]
macro_rules! impl_permute {
($Ax0:tt, $Ax1:tt) => {
impl<const M: usize, const N: usize, H: Tape> Permute2D<$Ax0, $Ax1> for tensor!(0, 1) {
impl<const M: usize, const N: usize, H: Tape> Permute<Axes2<$Ax0, $Ax1>> for tensor!(0, 1) {
type Permuted = tensor!($Ax0, $Ax1);
fn permute(self) -> Self::Permuted {
let mut result: <Self::Permuted as Tensor>::NoTape = TensorCreator::zeros();
<Cpu as DevicePermute2D<_, _, $Ax0, $Ax1>>::permute(self.data(), result.mut_data());
<Cpu as DevicePermute<_, _, Axes2<$Ax0, $Ax1>>>::permute(self.data(), result.mut_data());
move_tape_and_add_backward_op(self, result, move |mut t, result, grads| {
let (t_grad, result_grad) = grads.mut_and_ref(&t, &result);
<Cpu as DevicePermute2D<_, _, $Ax0, $Ax1>>::inverse_permute(t.mut_data(), result_grad);
<Cpu as DevicePermute<_, _, Axes2<$Ax0, $Ax1>>>::inverse_permute(t.mut_data(), result_grad);
Cpu::add(t_grad, t.data());
})
}
}
};
($Ax0:tt, $Ax1:tt, $Ax2:tt) => {
impl<const M: usize, const N: usize, const O: usize, H: Tape> Permute3D<$Ax0, $Ax1, $Ax2> for tensor!(0, 1, 2) {
impl<const M: usize, const N: usize, const O: usize, H: Tape> Permute<Axes3<$Ax0, $Ax1, $Ax2>> for tensor!(0, 1, 2) {
type Permuted = tensor!($Ax0, $Ax1, $Ax2);
fn permute(self) -> Self::Permuted {
let mut result: <Self::Permuted as Tensor>::NoTape = TensorCreator::zeros();
<Cpu as DevicePermute3D<_, _, $Ax0, $Ax1, $Ax2>>::permute(self.data(), result.mut_data());
<Cpu as DevicePermute<_, _, Axes3<$Ax0, $Ax1, $Ax2>>>::permute(self.data(), result.mut_data());
move_tape_and_add_backward_op(self, result, move |mut t, result, grads| {
let (t_grad, result_grad) = grads.mut_and_ref(&t, &result);
<Cpu as DevicePermute3D<_, _, $Ax0, $Ax1, $Ax2>>::inverse_permute(t.mut_data(), result_grad);
<Cpu as DevicePermute<_, _, Axes3<$Ax0, $Ax1, $Ax2>>>::inverse_permute(t.mut_data(), result_grad);
Cpu::add(t_grad, t.data());
})
}
}
};
($Ax0:tt, $Ax1:tt, $Ax2:tt, $Ax3:tt) => {
impl<const M: usize, const N: usize, const O: usize, const P: usize, H: Tape>
Permute4D<$Ax0, $Ax1, $Ax2, $Ax3> for tensor!(0, 1, 2, 3)
Permute<Axes4<$Ax0, $Ax1, $Ax2, $Ax3>> for tensor!(0, 1, 2, 3)
{
type Permuted = tensor!($Ax0, $Ax1, $Ax2, $Ax3);
fn permute(self) -> Self::Permuted {
let mut result: <Self::Permuted as Tensor>::NoTape = TensorCreator::zeros();
<Cpu as DevicePermute4D<_, _, $Ax0, $Ax1, $Ax2, $Ax3>>::permute(self.data(), result.mut_data());
<Cpu as DevicePermute<_, _, Axes4<$Ax0, $Ax1, $Ax2, $Ax3>>>::permute(self.data(), result.mut_data());
move_tape_and_add_backward_op(self, result, move |mut t, result, grads| {
let (t_grad, result_grad) = grads.mut_and_ref(&t, &result);
<Cpu as DevicePermute4D<_, _, $Ax0, $Ax1, $Ax2, $Ax3>>::inverse_permute(t.mut_data(), result_grad);
<Cpu as DevicePermute<_, _, Axes4<$Ax0, $Ax1, $Ax2, $Ax3>>>::inverse_permute(t.mut_data(), result_grad);
Cpu::add(t_grad, t.data());
})
}
Expand Down Expand Up @@ -295,39 +280,39 @@ mod tests {

#[test]
fn test_valid_permutations() {
let _ = <Tensor2D<3, 5> as Permute2D<0, 1>>::permute;
let _ = <Tensor2D<3, 5> as Permute2D<1, 0>>::permute;
let _ = <Tensor2D<3, 5> as Permute<Axes2<0, 1>>>::permute;
let _ = <Tensor2D<3, 5> as Permute<Axes2<1, 0>>>::permute;

let _ = <Tensor3D<3, 5, 7> as Permute3D<0, 1, 2>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute3D<0, 2, 1>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute3D<1, 0, 2>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute3D<1, 2, 0>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute3D<2, 0, 1>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute3D<2, 1, 0>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<0, 1, 2>>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<0, 2, 1>>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<1, 0, 2>>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<1, 2, 0>>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<2, 0, 1>>>::permute;
let _ = <Tensor3D<3, 5, 7> as Permute<Axes3<2, 1, 0>>>::permute;

let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 1, 2, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 1, 3, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 2, 1, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 2, 3, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 3, 2, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<0, 3, 1, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 0, 2, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 0, 3, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 2, 0, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 2, 3, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 3, 0, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<1, 3, 2, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 0, 1, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 0, 3, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 1, 0, 3>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 1, 3, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 3, 0, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<2, 3, 1, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 0, 1, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 0, 2, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 1, 0, 2>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 1, 2, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 2, 0, 1>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute4D<3, 2, 1, 0>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 1, 2, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 1, 3, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 2, 1, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 2, 3, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 3, 2, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<0, 3, 1, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 0, 2, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 0, 3, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 2, 0, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 2, 3, 0>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 3, 0, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<1, 3, 2, 0>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 0, 1, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 0, 3, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 1, 0, 3>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 1, 3, 0>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 3, 0, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<2, 3, 1, 0>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 0, 1, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 0, 2, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 1, 0, 2>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 1, 2, 0>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 2, 0, 1>>>::permute;
let _ = <Tensor4D<3, 5, 7, 9> as Permute<Axes4<3, 2, 1, 0>>>::permute;
}
}
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载