@DiyouS as discussed this morning.
This is a simple microbenchmark I've used before to test the sharing. It's just an adapted dot product and mixes dot product / scalar on some kernel. Amount of scalar work is given by NUM_SCALAR_CYCLES, so you can sweep scalar vs vector processing. Ideally, spatz utilization should rise util it's fully utilized and then you should see the snitch cores stalling.
It's still quite basic and rough, but maybe you can use it as starting point / for inspiration.
(delay_cycles actually needs a better implementation, it just adds some configurable delay. checking the global cycle counter is much more exact but might not work for very small delays)
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Author: Johannes Pfau <johannes.pfau@h-partners.com> (based on dp-fdotp test)
#include <benchmark.h>
#include <debug.h>
#include <snrt.h>
#include <stdio.h>
// Set to 0 to remove __release_vector instructions
// __release_vector adds 1026 extra cycles in total, which is 1.02 cycles per call
#define RELEASE_CORE 1
#define VECTOR_LEN 16
#define NUM_ITERATIONS 1000
// dot product time is approx v=21.5362177929 cycles
// calc cycles as cycles = v*(1-scale)/scale
// 194 <=> 10% vector op
// 86 <=> 20% vector op
// 50 <=> 30% vector op
// 32 <=> 40% vector op
// 22 <=> 50% vector op
// 14 <=> 60% vector op
// 9 <=> 70% vector op
// 5 <=> 80% vector op
// 2 <=> 90% vector op
#define NUM_SCALAR_CYCLES 194
static inline void delay_cycles(size_t n)
{
// Emit n NOPs at compile time for small values
if (__builtin_constant_p(n) && n < 20) {
switch (n) {
case 19: asm volatile("nop");
case 18: asm volatile("nop");
case 17: asm volatile("nop");
case 16: asm volatile("nop");
case 15: asm volatile("nop");
case 14: asm volatile("nop");
case 13: asm volatile("nop");
case 12: asm volatile("nop");
case 11: asm volatile("nop");
case 10: asm volatile("nop");
case 9: asm volatile("nop");
case 8: asm volatile("nop");
case 7: asm volatile("nop");
case 6: asm volatile("nop");
case 5: asm volatile("nop");
case 4: asm volatile("nop");
case 3: asm volatile("nop");
case 2: asm volatile("nop");
case 1: asm volatile("nop");
default: break;
}
return;
}
size_t start = benchmark_get_cycle();
while (benchmark_get_cycle() - start < n) {
// busy wait
}
}
static inline void __release_vector()
{
#if RELEASE_CORE == 1
// CUSTOM-0 instruction, u-type
asm volatile(".insn u 0x0B, x0, 0");
#endif
}
// 32-bit dot-product: a * b, 16 elements
int32_t fdotp_v32b(const float *a, const float *b)
{
// Set the vector length. 16 floats should fit on spatz. Tail and Mask are agnostic
unsigned int vl = VECTOR_LEN;
asm volatile("vsetvli %0, %1, e32, m1, ta, ma" : "=r"(vl) : "r"(vl));
// Load chunk a and b
asm volatile("vle32.v v8, (%0)" ::"r"(a));
asm volatile("vle32.v v16, (%0)" ::"r"(b));
asm volatile("vfmul.vv v24, v8, v16");
// Reduce and return
asm volatile("vmv.s.x v0, zero");
asm volatile("vfredusum.vs v0, v24, v0");
float fres;
asm volatile("vfmv.f.s %0, v0" : "=f"(fres));
// This cast also needs to be done before we call __release_vector,
// to ensure the value is in integer reg file (because float RF is in spatz. GVSoC does not model this though)
int32_t res = (int32_t)fres;
__release_vector();
return res;
}
// Put in shared memory, as stacks are local to cores
float *a;
float *b;
static volatile uint32_t printMutex = 0;
// FIXME: Should not use float. Not sure if init code will execute FPU code on spatz... But in theory it's only memory
int main()
{
const uint32_t num_cores = snrt_cluster_core_num();
const uint32_t cid = snrt_cluster_core_idx();
const uint32_t global_num_cores = snrt_global_core_num();
const uint32_t global_cid = snrt_global_core_idx();
if (cid == 0)
{
snrt_mutex_lock(&printMutex);
PRINTF("################################################################################\n");
PRINTF("# share-simple: global_core_num=%d cluster_core_num=%d\n", global_num_cores, num_cores);
PRINTF("################################################################################\n\n");
snrt_mutex_release(&printMutex);
}
snrt_mutex_lock(&printMutex);
PRINTF("Core %d: cluster_core_idx=%d "
"global_core_base_hartid=%d global_cluster_base_hartid=%d\n",
global_cid, cid, snrt_global_core_base_hartid(), snrt_cluster_core_base_hartid());
snrt_mutex_release(&printMutex);
if (cid == 0)
{
// Set the vector length. 16 floats should fit on spatz. Tail and Mask are agnostic
uint32_t vl = VECTOR_LEN;
asm volatile("vsetvli %0, %1, e32, m1, ta, ma" : "=r"(vl) : "r"(vl));
__release_vector();
PRINTF("\nVector length selected: vl=%d\n", vl);
a = (float *)snrt_l1alloc(num_cores * VECTOR_LEN * sizeof(float));
b = (float *)snrt_l1alloc(num_cores * VECTOR_LEN * sizeof(float));
PRINTF("Allocated L1 memory: a=0x%p b=0x%p\n", a, b);
for (size_t core = 0; core < num_cores; core++)
{
for (size_t i = 0; i < VECTOR_LEN; i++) {
// So that all vectors have one different, non-0 element
// Dot product will be 1240 + (1*2) = 1242 for core 0,
// 1240 + (3*4) = 1252 for core 1 and so on
if (i == 0) {
a[core * VECTOR_LEN + i] = 2*core + 1;
b[core * VECTOR_LEN + i] = 2*core + 2;
}
else {
a[core * VECTOR_LEN + i] = i;
b[core * VECTOR_LEN + i] = i;
}
}
PRINTF("Initialized L1 memory: C=%d a[0]=%f b[15]=%f\n", core, a[core * VECTOR_LEN], b[core * VECTOR_LEN]);
}
PRINTF("\n");
}
snrt_cluster_hw_barrier();
// Start benchmark
unsigned int timer = (unsigned int)-1;
if (cid == 0) {
start_kernel();
timer = benchmark_get_cycle();
}
snrt_cluster_hw_barrier();
// Benchmark function
int32_t res = 0;
float* ma = &a[cid * VECTOR_LEN];
float* mb = &b[cid * VECTOR_LEN];
for (size_t i = 0; i < NUM_ITERATIONS; i++) {
int32_t val = fdotp_v32b(ma, mb);
res += val;
delay_cycles(NUM_SCALAR_CYCLES);
}
// Stop benchmark
snrt_cluster_hw_barrier();
if (cid == 0) {
stop_kernel();
timer = benchmark_get_cycle() - timer;
}
// Result should be 1240
snrt_mutex_lock(&printMutex);
uint32_t cval = 1240 + (2*cid + 1) * (2*cid + 2);
PRINTF("Core %d: calculated = %d, expected = %d\n", cid, res, NUM_ITERATIONS * cval);
snrt_mutex_release(&printMutex);
snrt_cluster_hw_barrier();
if (cid == 0)
{
snrt_mutex_lock(&printMutex);
PRINTF("\nshare-simple: Finished in %d cycles.\n", timer);
snrt_mutex_release(&printMutex);
}
return 0;
}
@DiyouS as discussed this morning.
This is a simple microbenchmark I've used before to test the sharing. It's just an adapted dot product and mixes dot product / scalar on some kernel. Amount of scalar work is given by NUM_SCALAR_CYCLES, so you can sweep scalar vs vector processing. Ideally, spatz utilization should rise util it's fully utilized and then you should see the snitch cores stalling.
It's still quite basic and rough, but maybe you can use it as starting point / for inspiration.
(
delay_cyclesactually needs a better implementation, it just adds some configurable delay. checking the global cycle counter is much more exact but might not work for very small delays)