Files
TheRock/rocm-libraries/shared/tensile/Tensile/Source/client/source/ReferenceValidator.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

660 lines
27 KiB
C++

/*******************************************************************************
*
* MIT License
*
* Copyright (C) 2019-2024 Advanced Micro Devices, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*******************************************************************************/
#include "ReferenceValidator.hpp"
#include "DataInitializationTyped.hpp"
#include "ResultComparison.hpp"
#include "ResultReporter.hpp"
#include "Reference.hpp"
#include <Tensile/DataTypes.hpp>
#include <Tensile/hip/HipUtils.hpp>
#include <cstddef>
namespace Tensile
{
namespace Client
{
ReferenceValidator::ReferenceValidator(po::variables_map const& args,
std::shared_ptr<DataInitialization> dataInit,
hipStream_t stream)
: m_dataInit(dataInit)
, m_stream(stream)
{
m_elementsToValidate = args["num-elements-to-validate"].as<int>();
m_printValids = args["print-valids"].as<bool>();
m_printMax = args["print-max"].as<int>();
m_printTensorA = args["print-tensor-a"].as<bool>();
m_printTensorB = args["print-tensor-b"].as<bool>();
m_printTensorC = args["print-tensor-c"].as<bool>();
m_printTensorD = args["print-tensor-d"].as<bool>();
m_printTensorRef = args["print-tensor-ref"].as<bool>();
m_printAny = m_printTensorA || m_printTensorB || m_printTensorC || m_printTensorD
|| m_printTensorRef;
m_convolutionVsContraction = args["convolution-vs-contraction"].as<bool>();
if(args.count("convolution-identifier"))
m_convolutionProblem.FromIdentifier(
args["convolution-identifier"].as<std::string>());
m_enabled = m_elementsToValidate != 0 || m_printAny;
}
bool ReferenceValidator::needMoreBenchmarkRuns() const
{
if(m_enabled && m_numBenchmarkRuns == 0)
return true;
return false;
}
void ReferenceValidator::preBenchmarkRun() {}
void ReferenceValidator::postBenchmarkRun()
{
m_numBenchmarkRuns++;
}
void ReferenceValidator::preProblem(ContractionProblem const& problem)
{
if(m_enabled)
{
m_problem = problem;
m_referenceInputs = m_dataInit->prepareCPUInputs(problem);
m_validationStride = 1;
if(m_elementsToValidate > 0
&& m_elementsToValidate < problem.d().totalLogicalElements())
m_validationStride
= NextPrime(problem.d().totalAllocatedElements() / m_elementsToValidate);
SolveCPU(problem, *m_referenceInputs, m_validationStride);
if(m_convolutionVsContraction)
{
SolveCPUConvolution(
m_convolutionProblem, problem, *(m_dataInit->cpuConvInputs()));
// std::cout << "ValidateConv--Start\n";
m_errorInConvolutionVsContraction = validateSolution(
m_dataInit->cpuConvInputs()); // validate conv against reference
// TODO - print problem dimensions??
std::cout << m_convolutionProblem << " vs " << problem.operationIdentifier()
<< " : ";
if(m_errorInConvolutionVsContraction)
{
std::cout << "FAILED_CONV";
}
else
{
std::cout << "PASSED_CONV";
}
std::cout << "\n";
}
}
}
void ReferenceValidator::preSolution(ContractionSolution const& solution)
{
m_validatedSolution = false;
m_errorInSolution = false;
}
bool ReferenceValidator::needMoreRunsInSolution() const
{
if(m_enabled && !m_validatedSolution)
return true;
return false;
}
size_t ReferenceValidator::numWarmupRuns()
{
if(m_enabled && !m_validatedSolution)
return 1;
return 0;
}
void ReferenceValidator::setNumWarmupRuns(size_t count) {}
void ReferenceValidator::preWarmup() {}
void ReferenceValidator::postWarmup() {}
template <typename ManagedInputs>
bool ReferenceValidator::validateSolutionCast(std::shared_ptr<ContractionInputs> inputs)
{
auto const& typedReference = dynamic_cast<ManagedInputs const&>(*m_referenceInputs);
auto const& typedResult = dynamic_cast<ManagedInputs const&>(*inputs);
auto rv = validateTyped(typedReference, typedResult);
if(0 and inputs == m_dataInit->cpuConvInputs())
{
m_reporter->logTensor(
LogLevel::Verbose, "Aval-conv", typedResult.a, m_problem.a(), nullptr);
m_reporter->logTensor(
LogLevel::Verbose, "Bval-conv", typedResult.b, m_problem.b(), nullptr);
m_reporter->logTensor(
LogLevel::Verbose, "Dval-conv", typedResult.d, m_problem.d(), nullptr);
m_reporter->logTensor(LogLevel::Verbose,
"Bval-contraction",
typedReference.b,
m_problem.b(),
nullptr);
m_reporter->logTensor(LogLevel::Verbose,
"Dval-contraction",
typedReference.d,
m_problem.d(),
nullptr);
}
return rv;
}
bool ReferenceValidator::validateSolution(std::shared_ptr<ContractionInputs> inputs)
{
// retreive alpha/beta type set via setAlpha/BetaType()
auto alphaType = m_problem.alphaType();
auto betaType = m_problem.betaType();
// Backward-compatible: when setAlpha/BetaType() wasn't called, use the old way
// Could remove after rocBLAS is updated
if(alphaType == DataType::None)
{
alphaType = m_problem.a().dataType() == DataType::BFloat16
? DataType::Float
: m_problem.d().dataType();
}
if(betaType == DataType::None)
{
betaType = alphaType;
}
auto contractionInputsTypeId = ContractionInputs::TypeId(m_problem.a().dataType(),
m_problem.b().dataType(),
m_problem.c().dataType(),
m_problem.d().dataType(),
alphaType,
betaType);
switch(contractionInputsTypeId)
{
case ManagedContractionInputs_S_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_S_S_S>(inputs);
}
case ManagedContractionInputs_D_D_D::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_D_D_D>(inputs);
}
case ManagedContractionInputs_C_C_C::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_C_C_C>(inputs);
}
case ManagedContractionInputs_Z_Z_Z::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_Z_Z_Z>(inputs);
}
#ifdef TENSILE_USE_HALF
case ManagedContractionInputs_H_H_H::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_H_H_H>(inputs);
}
case ManagedContractionInputs_H_H_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_H_H_S>(inputs);
}
case ManagedContractionInputs_H_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_H_S_S>(inputs);
}
#endif // TENSILE_USE_HALF
case ManagedContractionInputs_I8x4_I32_I32::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_I8x4_I32_I32>(inputs);
}
case ManagedContractionInputs_I32_I32_I32::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_I32_I32_I32>(inputs);
}
case ManagedContractionInputs_I8_I32_I32::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_I8_I32_I32>(inputs);
}
#ifdef TENSILE_USE_BF16
case ManagedContractionInputs_B_B_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B_B_S>(inputs);
}
case ManagedContractionInputs_B_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B_S_S>(inputs);
}
#endif // TENSILE_USE_BF16
#ifdef TENSILE_USE_FP8_BF8
case ManagedContractionInputs_F8_F8_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8_F8_S>(inputs);
}
case ManagedContractionInputs_F8_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8_S_S>(inputs);
}
case ManagedContractionInputs_B8_B8_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8_B8_S>(inputs);
}
case ManagedContractionInputs_B8_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8_S_S>(inputs);
}
// hybrid cases: F8B8SS, B8F8SS
case ManagedContractionInputs_F8B8_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8B8_S_S>(inputs);
}
case ManagedContractionInputs_B8F8_S_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8F8_S_S>(inputs);
}
// hybrid cases with To = BF8
case ManagedContractionInputs_F8B8_B8_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8B8_B8_S>(inputs);
}
case ManagedContractionInputs_B8F8_B8_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8F8_B8_S>(inputs);
}
// cases with To = F16
case ManagedContractionInputs_F8_H_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8_H_S>(inputs);
}
case ManagedContractionInputs_B8_H_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8_H_S>(inputs);
}
case ManagedContractionInputs_F8B8_H_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_F8B8_H_S>(inputs);
}
case ManagedContractionInputs_B8F8_H_S::TypeId():
{
return validateSolutionCast<ManagedContractionInputs_B8F8_H_S>(inputs);
}
#endif // TENSILE_USE_FP8_BF8
default:;
}
throw std::runtime_error("Data type not implemented.");
}
void ReferenceValidator::validateWarmups(std::shared_ptr<ContractionInputs> inputs,
TimingEvents const& startEvents,
TimingEvents const& stopEvents)
{
if(m_enabled && !m_validatedSolution)
{
validateSolution(inputs);
m_validatedSolution = true;
}
}
template <typename ManagedInputs>
bool ReferenceValidator::validateTyped(ManagedInputs const& reference,
ManagedInputs const& result)
{
bool rv = false;
if(!m_enabled)
return rv;
if(m_printAny)
printTensorsTyped(reference, result);
if(m_elementsToValidate != 0)
{
// RMSComparison<typename ManagedInputs::DType> compareValid(1e-7,
// m_printMax > 0);
PointwiseComparison<typename ManagedInputs::DType> compareValid(
m_printValids, m_printMax, m_printMax > 0);
InvalidComparison<typename ManagedInputs::DType> compareInvalid(m_printMax,
m_printMax > 0);
rv = checkResultsTyped(reference, result, compareValid, compareInvalid);
}
return rv;
}
void ReferenceValidator::allocateResultBuffer(size_t bytes)
{
if(m_cpuResultBufferSize == bytes)
return;
m_cpuResultBuffer.reset();
uint8_t* buffer;
HIP_CHECK_EXC(hipHostMalloc(&buffer, bytes, 0));
m_cpuResultBuffer.reset(buffer, hipFree);
m_cpuResultBufferSize = bytes;
}
template <typename ManagedInputs>
void ReferenceValidator::printTensorsTyped(ManagedInputs const& reference,
ManagedInputs const& result)
{
size_t requiredBufferSize = 0;
std::cout << "reference alpha: " << reference.alpha << ", beta: " << reference.beta
<< std::endl;
std::cout << "result alpha: " << result.alpha << ", beta: " << result.beta
<< std::endl;
if(m_printTensorA)
requiredBufferSize
= std::max(requiredBufferSize, m_problem.a().totalAllocatedBytes());
if(m_printTensorB)
requiredBufferSize
= std::max(requiredBufferSize, m_problem.b().totalAllocatedBytes());
if(m_printTensorC)
requiredBufferSize
= std::max(requiredBufferSize, m_problem.c().totalAllocatedBytes());
if(m_printTensorD)
requiredBufferSize
= std::max(requiredBufferSize, m_problem.d().totalAllocatedBytes());
if(m_printTensorRef)
requiredBufferSize
= std::max(requiredBufferSize, m_problem.d().totalAllocatedBytes());
if(m_cpuResultBufferSize < requiredBufferSize)
allocateResultBuffer(requiredBufferSize);
if(m_printTensorA)
{
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.a,
m_problem.a().totalAllocatedBytes(),
hipMemcpyDeviceToHost,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
auto const* buffer = reinterpret_cast<typename ManagedInputs::AType const*>(
m_cpuResultBuffer.get());
m_reporter->logTensor(LogLevel::Verbose, "A", buffer, m_problem.a(), result.a);
}
if(m_printTensorB)
{
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.b,
m_problem.b().totalAllocatedBytes(),
hipMemcpyDeviceToHost,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
auto const* buffer = reinterpret_cast<typename ManagedInputs::BType const*>(
m_cpuResultBuffer.get());
m_reporter->logTensor(LogLevel::Verbose, "B", buffer, m_problem.b(), result.b);
}
if(result.c == result.d && (m_printTensorC || m_printTensorD))
{
// If the pointers are the same, only print the buffer once.
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.c,
m_problem.c().totalAllocatedBytes(),
hipMemcpyDeviceToHost,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
auto const* buffer = reinterpret_cast<typename ManagedInputs::CType const*>(
m_cpuResultBuffer.get());
m_reporter->logTensor(LogLevel::Verbose, "C_D", buffer, m_problem.c(), result.c);
}
else
{
if(m_printTensorC)
{
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.c,
m_problem.c().totalAllocatedBytes(),
hipMemcpyDeviceToHost,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
auto const* buffer = reinterpret_cast<typename ManagedInputs::CType const*>(
m_cpuResultBuffer.get());
m_reporter->logTensor(LogLevel::Verbose, "C", buffer, m_problem.c(), result.c);
}
if(m_printTensorD)
{
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.d,
m_problem.d().totalAllocatedBytes(),
hipMemcpyDeviceToHost,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
auto const* buffer = reinterpret_cast<typename ManagedInputs::DType const*>(
m_cpuResultBuffer.get());
m_reporter->logTensor(LogLevel::Verbose, "D", buffer, m_problem.d(), result.d);
}
}
if(m_printTensorRef)
{
m_reporter->logTensor(
LogLevel::Verbose, "Ref", reference.d, m_problem.d(), reference.d);
}
}
template <typename ManagedInputs, typename CompareValid, typename CompareInvalid>
bool ReferenceValidator::checkResultsTyped(ManagedInputs const& reference,
ManagedInputs const& result,
CompareValid& compareValid,
CompareInvalid& compareInvalid)
{
using Type = typename ManagedInputs::DType;
auto const& tensor = m_problem.d();
size_t elementsToCopy = tensor.totalAllocatedElements();
size_t elementsOffsetToCopy = 0;
size_t elementsBeforeData = 0;
size_t elementsAfterData = 0;
BoundsCheckMode boundsCheck = m_dataInit->getCurBoundsCheck();
if(boundsCheck == BoundsCheckMode::NaN)
elementsToCopy = result.dElements;
size_t bytesToCopy = elementsToCopy * sizeof(Type);
if(m_cpuResultBufferSize < bytesToCopy)
allocateResultBuffer(bytesToCopy);
if(boundsCheck == BoundsCheckMode::GuardPageBack)
elementsOffsetToCopy = result.dElements - tensor.totalAllocatedElements();
auto copykind = result.gpu ? hipMemcpyDeviceToHost : hipMemcpyHostToHost;
HIP_CHECK_EXC(hipMemcpyAsync(m_cpuResultBuffer.get(),
result.managedD.get() + elementsOffsetToCopy,
bytesToCopy,
copykind,
m_stream));
HIP_CHECK_EXC(hipStreamSynchronize(m_stream));
if(boundsCheck == BoundsCheckMode::NaN)
{
elementsBeforeData = result.d - result.managedD.get();
elementsAfterData
= elementsToCopy - (tensor.totalAllocatedElements() + elementsBeforeData);
}
// If there was extra data allocated before the tensor to do bounds
// checking, resultBuffer is the whole allocation, while resultData
// points directly to the result.
Type const* resultBuffer = reinterpret_cast<Type const*>(m_cpuResultBuffer.get());
Type const* resultData = resultBuffer + elementsBeforeData;
Type const* resultAfterData = resultData + tensor.totalAllocatedElements();
size_t boundsCheckElements = 0;
for(ptrdiff_t i = 0; i < elementsBeforeData; i++)
{
boundsCheckElements++;
compareInvalid.before(resultBuffer[i], i, elementsBeforeData);
}
if(m_validationStride == 1)
{
std::vector<size_t> coord(tensor.dimensions());
size_t outerCount = CoordCount(tensor.sizes().begin() + 1, tensor.sizes().end());
size_t prevBaseIndex = 0;
const size_t innerDimSize = tensor.sizes()[0];
const size_t initialStride = tensor.strides()[0];
for(size_t i = 0; i < outerCount; i++)
{
CoordNumbered(i,
coord.begin() + 1,
coord.end(),
tensor.sizes().begin() + 1,
tensor.sizes().end());
size_t baseElemIndex = tensor.index(coord);
if(boundsCheck == BoundsCheckMode::NaN && baseElemIndex != 0
&& baseElemIndex != prevBaseIndex + innerDimSize)
{
for(auto innerIndex = prevBaseIndex + innerDimSize;
innerIndex < baseElemIndex;
innerIndex++)
{
compareInvalid.inside(
resultData[innerIndex], innerIndex, baseElemIndex);
}
}
prevBaseIndex = baseElemIndex;
for(size_t j = 0; j < innerDimSize; j++)
{
size_t elemIndex = baseElemIndex + (j * initialStride);
Type referenceValue = reference.d[elemIndex];
Type resultValue = resultData[elemIndex];
compareValid(
referenceValue, resultValue, elemIndex, (i * tensor.sizes()[0]) + j);
}
}
}
else
{
std::vector<size_t> coord(tensor.dimensions());
for(size_t elemNumber = 0; elemNumber < tensor.totalLogicalElements();
elemNumber += m_validationStride)
{
CoordNumbered(elemNumber,
coord.begin(),
coord.end(),
tensor.sizes().begin(),
tensor.sizes().end());
size_t elemIndex = tensor.index(coord);
Type referenceValue = reference.d[elemIndex];
Type resultValue = resultData[elemIndex];
compareValid(referenceValue, resultValue, elemIndex, elemNumber);
}
}
for(ptrdiff_t i = 0; i < elementsAfterData; i++)
{
compareInvalid.after(resultAfterData[i], i, elementsAfterData);
}
if(boundsCheckElements > 0)
std::cout << "Performed bounds check on " << boundsCheckElements << " elements ("
<< elementsBeforeData << " before data)" << std::endl;
compareValid.report();
compareInvalid.report();
if(compareValid.error() || compareInvalid.error())
{
m_errorInSolution = true;
m_error = true;
return true;
}
return false;
}
void ReferenceValidator::postSolution()
{
if(m_enabled && !m_validatedSolution)
return;
if(m_elementsToValidate != 0)
{
if(m_errorInConvolutionVsContraction)
{
m_errorsReported++;
m_reporter->report(ResultKey::Validation, "FAILED_CONV");
}
else if(m_errorInSolution)
{
m_errorsReported++;
m_reporter->report(ResultKey::Validation, "FAILED");
}
else
m_reporter->report(ResultKey::Validation, "PASSED");
}
else
{
m_reporter->report(ResultKey::Validation, "NO_CHECK");
}
m_errorInSolution = false;
}
void ReferenceValidator::postProblem() {}
void ReferenceValidator::finalizeReport() {}
int ReferenceValidator::error() const
{
return m_errorsReported;
}
} // namespace Client
} // namespace Tensile