summaryrefslogtreecommitdiffstats
path: root/Whisper/CPU/MlContextCpu.cpp
diff options
context:
space:
mode:
authorKonstantin <const@const.me>2023-01-16 14:52:43 +0100
committerKonstantin <const@const.me>2023-01-16 14:52:43 +0100
commit8c4603c73675958efc960fbd4bb599a2909d106a (patch)
tree714dc6fc9a1672d5fd7f89676b97e10959662abc /Whisper/CPU/MlContextCpu.cpp
parent990a8d0dbaefc996244097397259e92758b15cce (diff)
Source codes
Diffstat (limited to 'Whisper/CPU/MlContextCpu.cpp')
-rw-r--r--Whisper/CPU/MlContextCpu.cpp597
1 files changed, 597 insertions, 0 deletions
diff --git a/Whisper/CPU/MlContextCpu.cpp b/Whisper/CPU/MlContextCpu.cpp
new file mode 100644
index 0000000..e34a823
--- /dev/null
+++ b/Whisper/CPU/MlContextCpu.cpp
@@ -0,0 +1,597 @@
+#include "stdafx.h"
+#include "MlContext.h"
+#include "simdUtils.h"
+#include "mulMat.h"
+using namespace CpuCompute;
+
+MlContext::MlContext( int threads ) : pfor( threads )
+{
+}
+
+Tensor MlContext::createTensor( eDataType type, const std::array<uint32_t, 4>& size )
+{
+ Tensor res;
+ check( res.create( type, size, allocator ) );
+ return res;
+}
+
+Tensor MlContext::createTensor( eDataType type, std::initializer_list<uint32_t> size )
+{
+ Tensor res;
+ check( res.create( type, size, allocator ) );
+ return res;
+}
+
+namespace
+{
+ inline const uint16_t* getRow16( const Tensor& t, size_t index )
+ {
+ const uint16_t* rsi = t.fp16();
+ rsi += index * t.nb[ 1 ];
+ return rsi;
+ }
+ inline const float* getRow32( const Tensor& t, size_t index )
+ {
+ const float* rsi = t.fp32();
+ rsi += index * t.nb[ 1 ];
+ return rsi;
+ }
+}
+
+Tensor MlContext::addRows( const Tensor& d_te, const Tensor& d_pe, const int* tokens, const int n_tokens, const int n_past )
+{
+ if( d_te.type() != eDataType::FP16 || d_pe.type() != eDataType::FP32 )
+ throw E_INVALIDARG;
+ if( d_te.ne[ 0 ] != d_pe.ne[ 0 ] )
+ throw E_INVALIDARG;
+ if( n_tokens <= 0 )
+ throw E_BOUNDS;
+
+ Tensor res = createTensor( eDataType::FP32, { d_te.ne[ 0 ], (uint32_t)n_tokens } );
+
+ const size_t inner = (size_t)d_te.ne[ 0 ];
+ const size_t outer = (size_t)n_tokens;
+ float* rdi = res.fp32();
+ for( size_t i = 0; i < outer; i++, rdi += inner, tokens++ )
+ {
+ const uint16_t* const source1 = getRow16( d_te, *(const uint32_t*)tokens );
+ const float* const source2 = getRow32( d_pe, i + (size_t)n_past );
+ addF16to32( rdi, source1, source2, inner );
+ }
+ return res;
+}
+
+namespace
+{
+ class DispatchHelper3
+ {
+ std::array<uint32_t, 3> ne;
+
+ public:
+ DispatchHelper3() = default;
+ DispatchHelper3( uint32_t x, uint32_t y, uint32_t z )
+ {
+ assert( x > 0 && y > 0 && z > 0 );
+ ne[ 0 ] = x;
+ ne[ 1 ] = y;
+ ne[ 2 ] = z;
+ }
+ size_t groupsCount() const
+ {
+ size_t res = ne[ 0 ];
+ res *= ne[ 1 ];
+ res *= ne[ 2 ];
+ return res;
+ }
+ std::array<uint32_t, 3> unpack( size_t idx ) const
+ {
+ assert( idx < groupsCount() );
+ std::array<uint32_t, 3> res;
+ res[ 0 ] = (uint32_t)( idx % ne[ 0 ] );
+ idx = idx / ne[ 0 ];
+ res[ 1 ] = (uint32_t)( idx % ne[ 1 ] );
+ res[ 2 ] = (uint32_t)( idx / ne[ 1 ] );
+ return res;
+ }
+ void next( std::array<uint32_t, 3>& i ) const
+ {
+ i[ 0 ]++;
+ if( i[ 0 ] < ne[ 0 ] )
+ return;
+ i[ 0 ] = 0;
+ i[ 1 ]++;
+ if( i[ 1 ] < ne[ 1 ] )
+ return;
+ i[ 1 ] = 0;
+ i[ 2 ]++;
+ }
+ };
+
+ inline const float* sourceRow( const float* rsi, const std::array<uint32_t, 3>& idx, size_t nb0, size_t nb1, size_t nb2 )
+ {
+ const size_t r0 = idx[ 0 ] * nb0;
+ const size_t r1 = idx[ 1 ] * nb1;
+ const size_t r2 = idx[ 2 ] * nb2;
+ rsi = rsi + r0 + r1 + r2;
+ return rsi;
+ }
+
+ struct NormContext : public iComputeRange
+ {
+ const float* source;
+ float* result;
+ size_t inner;
+ DispatchHelper3 threads;
+ std::array<uint32_t, 3> nbInput;
+
+ HRESULT __stdcall compute( size_t i, size_t end ) const override final
+ {
+ ALIGNED_SPAN( temp, inner );
+
+ std::array<uint32_t, 3> idx = threads.unpack( i );
+ float* rdi = result + i * inner;
+ for( ; i < end; i++, rdi += inner, threads.next( idx ) )
+ {
+ const float* rsi = sourceRow( source, idx, nbInput[ 0 ], nbInput[ 1 ], nbInput[ 2 ] );
+ norm( rdi, temp, rsi, inner );
+ }
+ return S_OK;
+ }
+ };
+}
+
+Tensor MlContext::norm( const Tensor& arg )
+{
+ if( arg.type() != eDataType::FP32 || arg.nb[ 0 ] != 1 )
+ throw E_INVALIDARG;
+ Tensor res = createTensor( eDataType::FP32, arg.ne );
+
+ NormContext context;
+ context.source = arg.fp32();
+ context.result = res.fp32();
+ context.inner = arg.ne[ 0 ];
+ context.threads = DispatchHelper3( arg.ne[ 1 ], arg.ne[ 2 ], arg.ne[ 3 ] );
+ context.nbInput = { arg.nb[ 1 ], arg.nb[ 2 ], arg.nb[ 3 ] };
+
+ check( pfor.parallelFor( context, context.threads.groupsCount() ) );
+ return res;
+}
+
+void MlContext::fmaRepeat( Tensor& cur, const Tensor& w, const Tensor& b )
+{
+ if( !( cur.isContinuous() && w.isContinuous() && b.isContinuous() ) )
+ throw E_INVALIDARG;
+
+ if( !( cur.type() == eDataType::FP32 && w.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ if( !isSameShape( w, b ) )
+ throw E_INVALIDARG;
+
+ DispatchHelper3 helper{ cur.ne[ 1 ], cur.ne[ 2 ], cur.ne[ 3 ] };
+ std::array<uint32_t, 3> idx = { 0, 0, 0 };
+ const size_t countRows = helper.groupsCount();
+
+ const size_t innerRes = cur.ne[ 0 ];
+ const size_t innerPattern = w.ne[ 0 ];
+
+ float* rdi = cur.fp32();
+ for( size_t i = 0; i < countRows; i++, helper.next( idx ), rdi += innerRes )
+ {
+ std::array<uint32_t, 3> idxPattern;
+ idxPattern[ 0 ] = idx[ 0 ] % w.ne[ 1 ];
+ idxPattern[ 1 ] = idx[ 1 ] % w.ne[ 2 ];
+ idxPattern[ 2 ] = idx[ 2 ] % w.ne[ 3 ];
+
+ const float* s1 = sourceRow( w.fp32(), idxPattern, w.nb[ 1 ], w.nb[ 2 ], w.nb[ 3 ] );
+ const float* s2 = sourceRow( b.fp32(), idxPattern, b.nb[ 1 ], b.nb[ 2 ], b.nb[ 3 ] );
+ fmaRepeatRow( rdi, innerRes, s1, s2, innerPattern );
+ }
+}
+
+Tensor MlContext::mulMat( const Tensor& a, const Tensor& b )
+{
+ if( !DirectCompute::canMulMat( a, b ) )
+ throw E_INVALIDARG;
+
+ std::array<uint32_t, 4> ne{ a.ne[ 1 ], b.ne[ 1 ], a.ne[ 2 ], b.ne[ 3 ] };
+ Tensor result = createTensor( eDataType::FP32, ne );
+
+ check( CpuCompute::mulMat( result, a, b, pfor ) );
+ return result;
+}
+
+// cur = add( repeat( b, cur ), cur ); cur = scale(cur, scaling)
+void MlContext::addRepeatScale( Tensor& cur, const Tensor& b, float scaling )
+{
+ if( !( cur.isContinuous() && b.isContinuous() ) )
+ throw E_INVALIDARG;
+ if( !( cur.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ DispatchHelper3 helper{ cur.ne[ 1 ], cur.ne[ 2 ], cur.ne[ 3 ] };
+ std::array<uint32_t, 3> idx = { 0, 0, 0 };
+ const size_t countRows = helper.groupsCount();
+
+ const size_t innerRes = (uint32_t)cur.ne[ 0 ];
+ const size_t innerPattern = (uint32_t)b.ne[ 0 ];
+
+ float* rdi = cur.fp32();
+ const __m256 scale = _mm256_set1_ps( scaling );
+ for( size_t i = 0; i < countRows; i++, helper.next( idx ), rdi += innerRes )
+ {
+ std::array<uint32_t, 3> idxPattern;
+ idxPattern[ 0 ] = idx[ 0 ] % (uint32_t)b.ne[ 1 ];
+ idxPattern[ 1 ] = idx[ 1 ] % (uint32_t)b.ne[ 2 ];
+ idxPattern[ 2 ] = idx[ 2 ] % (uint32_t)b.ne[ 3 ];
+
+ const float* source = sourceRow( b.fp32(), idxPattern, b.nb[ 1 ], b.nb[ 2 ], b.nb[ 3 ] );
+ addRepeatScaleRow( rdi, innerRes, source, innerPattern, scale );
+ }
+}
+
+void MlContext::addRepeat( Tensor& cur, const Tensor& b )
+{
+ if( !( cur.isContinuous() && b.isContinuous() ) )
+ throw E_INVALIDARG;
+ if( !( cur.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ DispatchHelper3 helper{ cur.ne[ 1 ], cur.ne[ 2 ], cur.ne[ 3 ] };
+ std::array<uint32_t, 3> idx = { 0, 0, 0 };
+ const size_t countRows = helper.groupsCount();
+
+ const size_t innerRes = (uint32_t)cur.ne[ 0 ];
+ const size_t innerPattern = (uint32_t)b.ne[ 0 ];
+
+ float* rdi = cur.fp32();
+ for( size_t i = 0; i < countRows; i++, helper.next( idx ), rdi += innerRes )
+ {
+ std::array<uint32_t, 3> idxPattern;
+ idxPattern[ 0 ] = idx[ 0 ] % (uint32_t)b.ne[ 1 ];
+ idxPattern[ 1 ] = idx[ 1 ] % (uint32_t)b.ne[ 2 ];
+ idxPattern[ 2 ] = idx[ 2 ] % (uint32_t)b.ne[ 3 ];
+
+ const float* source = sourceRow( b.fp32(), idxPattern, b.nb[ 1 ], b.nb[ 2 ], b.nb[ 3 ] );
+ addRepeatRow( rdi, innerRes, source, innerPattern );
+ }
+}
+
+// cur = scale(cur, scaling)
+void MlContext::scale( Tensor& cur, float scaling )
+{
+ if( !( cur.isContinuous() && cur.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ const size_t len = cur.countElements();
+ const __m256 scale = _mm256_set1_ps( scaling );
+ scaleRow( cur.fp32(), len, scale );
+}
+
+void MlContext::diagMaskInf( Tensor& cur, uint32_t n_past )
+{
+ if( !( cur.isContinuous() && cur.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ const size_t n = cur.countRows();
+ const size_t nc = cur.ne[ 0 ];
+ const size_t nr = cur.ne[ 1 ];
+ const size_t nz = n / nr;
+
+ for( size_t k = 0; k < nz; k++ )
+ {
+ for( size_t j = 0; j < nr; j++ )
+ {
+ float* const rdi = cur.fp32() + k * cur.nb[ 2 ] + j * cur.nb[ 1 ];
+ // +1 because the original code checked for `if( i > n_past + j )`
+ // That's why the first index to write is ( n_past + j + 1 )
+ const size_t start = n_past + j + 1;
+ const ptrdiff_t len = (ptrdiff_t)nc - (ptrdiff_t)start;
+ if( len <= 0 )
+ continue;
+
+ // Generates a store string instruction (rep stosd).
+ // The magic number is negative infinity in FP32: https://www.h-schmidt.net/FloatConverter/IEEE754.html
+ __stosd( (DWORD*)( rdi + start ), 0xff800000u, (size_t)len );
+ }
+ }
+}
+
+void MlContext::softMax( Tensor& cur, float inputScale )
+{
+ if( !( cur.isContinuous() && cur.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ struct SoftMaxContext : public iComputeRange
+ {
+ float* data;
+ float inputScale;
+ size_t length, stride;
+
+ HRESULT __stdcall compute( size_t i, size_t end ) const override final
+ {
+ float* rdi = data + stride * i;
+ for( ; i < end; i++, rdi += stride )
+ ::softMax( rdi, length, inputScale );
+ return S_OK;
+ }
+ };
+
+ SoftMaxContext context;
+ context.data = cur.fp32();
+ context.inputScale = inputScale;
+ context.length = cur.ne[ 0 ];
+ context.stride = cur.nb[ 1 ];
+
+ const size_t n = cur.countRows();
+ pfor.parallelFor( context, n );
+}
+
+namespace
+{
+ template<class R, class S>
+ __forceinline void copyElement( R* rdi, const S* rsi )
+ {
+ static_assert( std::is_same<R, S>() );
+ *rdi = *rsi;
+ }
+ template<>
+ __forceinline void copyElement<float, uint16_t>( float* rdi, const uint16_t* rsi )
+ {
+ __m128i iv = _mm_cvtsi32_si128( *rsi );
+ __m128 fv = _mm_cvtph_ps( iv );
+ _mm_store_ss( rdi, fv );
+ }
+ template<>
+ __forceinline void copyElement<uint16_t, float>( uint16_t* rdi, const float* rsi )
+ {
+ __m128 fv = _mm_load_ss( rsi );
+ __m128i iv = _mm_cvtps_ph( fv, 0 );
+ *rdi = (uint16_t)(uint32_t)_mm_cvtsi128_si32( iv );
+ }
+
+ template<class R, class S>
+ __forceinline void copyRow( R* rdi, const S* rsi, size_t length )
+ {
+ static_assert( std::is_same<R, S>() );
+ memcpy( rdi, rsi, length * sizeof( R ) );
+ }
+ template<>
+ __forceinline void copyRow<uint16_t, float>( uint16_t* rdi, const float* rsi, size_t length )
+ {
+ floatsDowncast( rdi, rsi, length );
+ }
+ template<>
+ __forceinline void copyRow<float, uint16_t>( float* rdi, const uint16_t* rsi, size_t length )
+ {
+ floatsUpcast( rdi, rsi, length );
+ }
+
+ template<class R, class S>
+ static void __declspec( noinline ) copyImpl( R* rdi, const S* rsi, const TensorShape& shape )
+ {
+ const bool continuousRows = shape.nb[ 0 ] == 1;
+
+ for( size_t i03 = 0; i03 < shape.ne[ 3 ]; i03++, rsi += shape.nb[ 3 ] )
+ {
+ const S* source2 = rsi;
+ for( size_t i02 = 0; i02 < shape.ne[ 2 ]; i02++, source2 += shape.nb[ 2 ] )
+ {
+ const S* source1 = source2;
+ for( size_t i01 = 0; i01 < shape.ne[ 1 ]; i01++, source1 += shape.nb[ 1 ] )
+ {
+ // Performance optimization here: when the rows are dense, we can copy them much faster with memcpy()
+ // Or at least with AVX, when we need to convert between numeric types
+ if( continuousRows )
+ {
+ // This branch is very predictable, same outcome for all loop iterations
+ copyRow( rdi, source1, shape.ne[ 0 ] );
+ rdi += shape.ne[ 0 ];
+ }
+ else
+ {
+ const S* source0 = source1;
+ for( size_t i00 = 0; i00 < shape.ne[ 0 ]; i00++, source0 += shape.nb[ 0 ] )
+ {
+ copyElement( rdi, source0 );
+ rdi++;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+HRESULT MlContext::copyImpl( Tensor& result, const Tensor& source )
+{
+ if( !( result.isContinuous() && ( result.countElements() == source.countElements() ) ) )
+ return E_INVALIDARG;
+
+ const eDataType typeResult = result.type();
+ const eDataType typeSource = source.type();
+ if( source.isContinuous() )
+ {
+ const size_t elts = result.countElements();
+ if( typeResult == typeSource )
+ {
+ const size_t bytes = elts * elementSize( typeResult );
+ memcpy( result.data(), source.data(), bytes );
+ return S_OK;
+ }
+ if( typeSource == eDataType::FP16 && typeResult == eDataType::FP32 )
+ {
+ floatsUpcast( result.fp32(), source.fp16(), elts );
+ return S_OK;
+ }
+ if( typeSource == eDataType::FP32 && typeResult == eDataType::FP16 )
+ {
+ floatsDowncast( result.fp16(), source.fp32(), elts );
+ return S_OK;
+ }
+ return E_UNEXPECTED;
+ }
+ else
+ {
+ if( typeSource == eDataType::FP16 && typeResult == eDataType::FP16 )
+ {
+ ::copyImpl( result.fp16(), source.fp16(), source );
+ return S_OK;
+ }
+ if( typeSource == eDataType::FP32 && typeResult == eDataType::FP32 )
+ {
+ ::copyImpl( result.fp32(), source.fp32(), source );
+ return S_OK;
+ }
+ if( typeSource == eDataType::FP16 && typeResult == eDataType::FP32 )
+ {
+ ::copyImpl( result.fp32(), source.fp16(), source );
+ return S_OK;
+ }
+ if( typeSource == eDataType::FP32 && typeResult == eDataType::FP16 )
+ {
+ ::copyImpl( result.fp16(), source.fp32(), source );
+ return S_OK;
+ }
+ return E_UNEXPECTED;
+ }
+}
+
+Tensor MlContext::copy( const Tensor& a, eDataType type, std::initializer_list<uint32_t> size )
+{
+ const size_t dims = size.size();
+ if( 0 == dims || dims > 4 )
+ throw E_BOUNDS;
+
+ size_t nRequested = 1;
+ for( size_t i = 0; i < dims; i++ )
+ {
+ uint32_t n = size.begin()[ i ];
+ nRequested *= n;
+ }
+ if( nRequested != a.countElements() )
+ throw E_INVALIDARG;
+
+ if( a.type() == type && a.isContinuous() )
+ {
+ // Same type, and it's dense - no need to move data, equal to reshape
+ Tensor res{ a };
+ for( size_t i = 0; i < dims; i++ )
+ res.ne[ i ] = size.begin()[ i ];;
+ for( size_t i = dims; i < 4; i++ )
+ res.ne[ i ] = 1;
+ res.setDenseStrides();
+ return res;
+ }
+ else
+ {
+ // Need to convert types, and/or transpose the tensor. Make another tensor for the output
+ Tensor res = createTensor( type, size );
+ check( copyImpl( res, a ) );
+ return res;
+ }
+}
+
+Tensor MlContext::permute( const Tensor& a, uint8_t axis0, uint8_t axis1, uint8_t axis2, uint8_t axis3 )
+{
+ assert( axis0 < 4 );
+ assert( axis1 < 4 );
+ assert( axis2 < 4 );
+ assert( axis3 < 4 );
+
+ assert( axis0 != axis1 );
+ assert( axis0 != axis2 );
+ assert( axis0 != axis3 );
+ assert( axis1 != axis2 );
+ assert( axis1 != axis3 );
+ assert( axis2 != axis3 );
+
+ Tensor res = a;
+ res.ne[ axis0 ] = a.ne[ 0 ];
+ res.ne[ axis1 ] = a.ne[ 1 ];
+ res.ne[ axis2 ] = a.ne[ 2 ];
+ res.ne[ axis3 ] = a.ne[ 3 ];
+
+ res.nb[ axis0 ] = a.nb[ 0 ];
+ res.nb[ axis1 ] = a.nb[ 1 ];
+ res.nb[ axis2 ] = a.nb[ 2 ];
+ res.nb[ axis3 ] = a.nb[ 3 ];
+
+ return res;
+}
+
+void MlContext::copyInPlace( Tensor& dest, const Tensor& a, eDataType type, std::initializer_list<uint32_t> size )
+{
+ assert( type == dest.type() );
+
+ const size_t dims = size.size();
+ if( 0 == dims || dims > 4 )
+ throw E_BOUNDS;
+
+ size_t nRequested = 1;
+ for( size_t i = 0; i < dims; i++ )
+ {
+ uint32_t n = size.begin()[ i ];
+ nRequested *= n;
+ }
+ if( nRequested != a.countElements() || nRequested != dest.countElements() )
+ throw E_INVALIDARG;
+
+ // Reshape the destination
+ for( size_t i = 0; i < dims; i++ )
+ dest.ne[ i ] = size.begin()[ i ];
+ for( size_t i = dims; i < 4; i++ )
+ dest.ne[ i ] = 1;
+ dest.setDenseStrides();
+
+ // Copy the data
+ check( copyImpl( dest, a ) );
+}
+
+void MlContext::addInPlace( Tensor& a, const Tensor& b )
+{
+ if( !( a.isContinuous() && b.isContinuous() && a.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_NOTIMPL;
+
+ const size_t length = a.countElements();
+ addRowInPlace( a.fp32(), b.fp32(), length );
+}
+
+Tensor MlContext::add( const Tensor& a, const Tensor& b )
+{
+ if( !( a.isContinuous() && b.isContinuous() && a.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_NOTIMPL;
+
+ Tensor res = createTensor( eDataType::FP32, a.ne );
+ const size_t length = a.countElements();
+ addRow( res.fp32(), a.fp32(), b.fp32(), length );
+ return res;
+}
+
+void MlContext::addRepeatGelu( Tensor& cur, const Tensor& b )
+{
+ if( !( cur.isContinuous() && b.isContinuous() ) )
+ throw E_INVALIDARG;
+ if( !( cur.type() == eDataType::FP32 && b.type() == eDataType::FP32 ) )
+ throw E_INVALIDARG;
+
+ DispatchHelper3 helper{ cur.ne[ 1 ], cur.ne[ 2 ], cur.ne[ 3 ] };
+ std::array<uint32_t, 3> idx = { 0, 0, 0 };
+ const size_t countRows = helper.groupsCount();
+
+ const size_t innerRes = (uint32_t)cur.ne[ 0 ];
+ const size_t innerPattern = (uint32_t)b.ne[ 0 ];
+ float* rdi = cur.fp32();
+ auto& lookupTables = getLookupTables();
+ for( size_t i = 0; i < countRows; i++, helper.next( idx ), rdi += innerRes )
+ {
+ std::array<uint32_t, 3> idxPattern;
+ idxPattern[ 0 ] = idx[ 0 ] % (uint32_t)b.ne[ 1 ];
+ idxPattern[ 1 ] = idx[ 1 ] % (uint32_t)b.ne[ 2 ];
+ idxPattern[ 2 ] = idx[ 2 ] % (uint32_t)b.ne[ 3 ];
+
+ const float* source = sourceRow( b.fp32(), idxPattern, b.nb[ 1 ], b.nb[ 2 ], b.nb[ 3 ] );
+ addRepeatGeluRow( rdi, innerRes, source, innerPattern, lookupTables );
+ }
+ return;
+} \ No newline at end of file