Kernels
File size: 6,159 Bytes
e873e70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <torch/all.h>

#include <cstddef>
#include <cstdint>

#if defined(__x86_64__) || defined(_M_X64)
#include <immintrin.h>
#define RELU_X86 1
#endif

// Multi-dtype CPU ReLU: float32 / float16 / bfloat16 / int8.
// ReLU is memory-bound, so the kernel maximizes effective bandwidth:
//   * AVX2 (256-bit) vectorized, runtime-dispatched (falls back to a portable
//     auto-vectorized scalar loop if the CPU lacks AVX2).
//   * Non-temporal streaming stores when the output is 32B-aligned (skips
//     write-allocate/RFO traffic) -> ~2.4x the old SSE-1-thread backend.
//   * OpenMP across cores (active when the build links -fopenmp; correct serial
//     otherwise).
// fp16/bf16 relu = "zero the lane if the sign bit is set" (both are 16-bit
// sign-magnitude floats), done as branchless integer SIMD -- no fp conversion.
// int8 relu = signed byte max with 0 (_mm256_max_epi8).

namespace {

// ---------- scalar (any arch; auto-vectorizes with -O3) ----------
inline float    relu_s(float x)    { return x > 0.f ? x : 0.f; }
inline uint16_t relu_h(uint16_t x) { return (x & 0x8000u) ? uint16_t(0) : x; }  // fp16 & bf16
inline int8_t   relu_b(int8_t x)   { return x > 0 ? x : int8_t(0); }

void relu_f32_scalar(float* o, const float* in, int64_t n) {
  #pragma omp parallel for schedule(static)
  for (int64_t i = 0; i < n; ++i) o[i] = relu_s(in[i]);
}
void relu_h16_scalar(uint16_t* o, const uint16_t* in, int64_t n) {
  #pragma omp parallel for schedule(static)
  for (int64_t i = 0; i < n; ++i) o[i] = relu_h(in[i]);
}
void relu_i8_scalar(int8_t* o, const int8_t* in, int64_t n) {
  #pragma omp parallel for schedule(static)
  for (int64_t i = 0; i < n; ++i) o[i] = relu_b(in[i]);
}

#ifdef RELU_X86
// ---------- AVX2 paths (NT store when 32B-aligned) ----------
__attribute__((target("avx2")))
void relu_f32_avx2(float* o, const float* in, int64_t n) {
  const int W = 8; int64_t nv = n / W;
  const __m256 z = _mm256_setzero_ps();
  if ((reinterpret_cast<uintptr_t>(o) & 31u) == 0) {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v)
      _mm256_stream_ps(o + v*W, _mm256_max_ps(_mm256_loadu_ps(in + v*W), z));
    _mm_sfence();
  } else {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v)
      _mm256_storeu_ps(o + v*W, _mm256_max_ps(_mm256_loadu_ps(in + v*W), z));
  }
  for (int64_t i = nv*W; i < n; ++i) o[i] = relu_s(in[i]);
}
__attribute__((target("avx2")))
void relu_h16_avx2(uint16_t* o, const uint16_t* in, int64_t n) {
  const int W = 16; int64_t nv = n / W;
  if ((reinterpret_cast<uintptr_t>(o) & 31u) == 0) {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v) {
      __m256i x = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(in + v*W));
      __m256i neg = _mm256_srai_epi16(x, 15);            // 0xFFFF where sign set
      _mm256_stream_si256(reinterpret_cast<__m256i*>(o + v*W),
                          _mm256_andnot_si256(neg, x));   // 0 if negative else x
    }
    _mm_sfence();
  } else {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v) {
      __m256i x = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(in + v*W));
      __m256i neg = _mm256_srai_epi16(x, 15);
      _mm256_storeu_si256(reinterpret_cast<__m256i*>(o + v*W),
                          _mm256_andnot_si256(neg, x));
    }
  }
  for (int64_t i = nv*W; i < n; ++i) o[i] = relu_h(in[i]);
}
__attribute__((target("avx2")))
void relu_i8_avx2(int8_t* o, const int8_t* in, int64_t n) {
  const int W = 32; int64_t nv = n / W;
  const __m256i z = _mm256_setzero_si256();
  if ((reinterpret_cast<uintptr_t>(o) & 31u) == 0) {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v)
      _mm256_stream_si256(reinterpret_cast<__m256i*>(o + v*W),
        _mm256_max_epi8(_mm256_loadu_si256(reinterpret_cast<const __m256i*>(in + v*W)), z));
    _mm_sfence();
  } else {
    #pragma omp parallel for schedule(static)
    for (int64_t v = 0; v < nv; ++v)
      _mm256_storeu_si256(reinterpret_cast<__m256i*>(o + v*W),
        _mm256_max_epi8(_mm256_loadu_si256(reinterpret_cast<const __m256i*>(in + v*W)), z));
  }
  for (int64_t i = nv*W; i < n; ++i) o[i] = relu_b(in[i]);
}
#endif  // RELU_X86

inline bool has_avx2() {
#ifdef RELU_X86
  return __builtin_cpu_supports("avx2");
#else
  return false;
#endif
}

void relu_f32(float* o, const float* in, int64_t n) {
#ifdef RELU_X86
  if (has_avx2()) { relu_f32_avx2(o, in, n); return; }
#endif
  relu_f32_scalar(o, in, n);
}
void relu_h16(uint16_t* o, const uint16_t* in, int64_t n) {
#ifdef RELU_X86
  if (has_avx2()) { relu_h16_avx2(o, in, n); return; }
#endif
  relu_h16_scalar(o, in, n);
}
void relu_i8(int8_t* o, const int8_t* in, int64_t n) {
#ifdef RELU_X86
  if (has_avx2()) { relu_i8_avx2(o, in, n); return; }
#endif
  relu_i8_scalar(o, in, n);
}

}  // namespace

void relu(torch::Tensor &out, torch::Tensor const &input) {
  TORCH_CHECK(input.is_contiguous(), "input must be contiguous");
  TORCH_CHECK(out.is_contiguous(), "output must be contiguous");
  TORCH_CHECK(input.numel() == out.numel(),
              "Input and output tensors must have the same number of elements");
  TORCH_CHECK(input.scalar_type() == out.scalar_type(),
              "Input and output tensors must have the same dtype");
  const int64_t n = input.numel();
  switch (input.scalar_type()) {
    case torch::kFloat32:
      relu_f32(out.data_ptr<float>(), input.data_ptr<float>(), n);
      break;
    case torch::kHalf:
      relu_h16(reinterpret_cast<uint16_t*>(out.data_ptr<at::Half>()),
               reinterpret_cast<const uint16_t*>(input.data_ptr<at::Half>()), n);
      break;
    case torch::kBFloat16:
      relu_h16(reinterpret_cast<uint16_t*>(out.data_ptr<at::BFloat16>()),
               reinterpret_cast<const uint16_t*>(input.data_ptr<at::BFloat16>()), n);
      break;
    case torch::kChar:
      relu_i8(out.data_ptr<int8_t>(), input.data_ptr<int8_t>(), n);
      break;
    default:
      TORCH_CHECK(false, "relu_cpu: unsupported dtype ", input.scalar_type(),
                  " (supported: float32, float16, bfloat16, int8)");
  }
}